+77
-93
@@ -91,7 +91,10 @@ async function host() {
|
||||
callback: (editor: {
|
||||
add: (definition: { execute: typeof command }) => void;
|
||||
}) => void,
|
||||
) => callback({ add: (definition) => (command = definition.execute) }),
|
||||
) =>
|
||||
callback({
|
||||
add: (definition) => (command = definition.execute),
|
||||
}),
|
||||
},
|
||||
session: {
|
||||
hook: async (name: string, callback: (event: Event) => Promise<void> | void) => {
|
||||
@@ -106,7 +109,11 @@ async function host() {
|
||||
: { id: sessionID, agent: "build" },
|
||||
create: async (input: { parentID: string; model: object }) => {
|
||||
expect(input.parentID).toBe("ses_root");
|
||||
expect(input.model).toEqual({ providerID: "acme", id: "cheap", variant: "xhigh" });
|
||||
expect(input.model).toEqual({
|
||||
providerID: "acme",
|
||||
id: "cheap",
|
||||
variant: "xhigh",
|
||||
});
|
||||
return { id: "ses_worker" };
|
||||
},
|
||||
generate: async ({ sessionID, prompt }: { sessionID: string; prompt: string }) => {
|
||||
@@ -116,7 +123,10 @@ async function host() {
|
||||
agent: "build",
|
||||
system: [{ type: "text", text: "host system" }],
|
||||
messages: [
|
||||
new Entry({ role: "user", content: [{ type: "text", text: prompt }] }),
|
||||
new Entry({
|
||||
role: "user",
|
||||
content: [{ type: "text", text: prompt }],
|
||||
}),
|
||||
],
|
||||
tools: {},
|
||||
};
|
||||
@@ -137,7 +147,11 @@ async function host() {
|
||||
sessionID,
|
||||
agent: "build",
|
||||
system: [
|
||||
{ type: "text", text: "host one", cache: { type: "ephemeral" } },
|
||||
{
|
||||
type: "text",
|
||||
text: "host one",
|
||||
cache: { type: "ephemeral" },
|
||||
},
|
||||
{ type: "text", text: "host two" },
|
||||
],
|
||||
messages: messages.map((message) => new Entry(message)),
|
||||
@@ -187,7 +201,10 @@ test("optchat is off by default, refuses child sessions, and hides its tools whe
|
||||
|
||||
test("turns see the view and the new message; compactions retry Too long and build the tree", async () => {
|
||||
const h = await host();
|
||||
await h.command()({ sessionID: "ses_root", prompt: { text: "/optchat on" } });
|
||||
await h.command()({
|
||||
sessionID: "ses_root",
|
||||
prompt: { text: "/optchat on" },
|
||||
});
|
||||
expect(h.notices.at(-1)).toStartWith("[optchat] on. 0 messages");
|
||||
const long = "please remember: " + "detail ".repeat(100);
|
||||
h.replies.push("x".repeat(700), "y".repeat(600), "z".repeat(400));
|
||||
@@ -204,7 +221,10 @@ test("turns see the view and the new message; compactions retry Too long and bui
|
||||
expect(first.messages[1]!.content[0]!.text).toBe(long);
|
||||
// System start and end, and the request's end; no whole view block yet.
|
||||
expect(hints(first)).toBe(3);
|
||||
expect(first.messages[1]!.content[0]!.cache).toEqual({ type: "ephemeral", ttlSeconds: 300 });
|
||||
expect(first.messages[1]!.content[0]!.cache).toEqual({
|
||||
type: "ephemeral",
|
||||
ttlSeconds: 300,
|
||||
});
|
||||
await settle();
|
||||
|
||||
// The compaction ran in the worker with the same system prompt, retried twice, and kept the shortest line.
|
||||
@@ -226,7 +246,12 @@ test("turns see the view and the new message; compactions retry Too long and bui
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "reasoning", text: "never logged" },
|
||||
{ type: "tool-call", id: "c1", name: "read", input: { path: "a" } },
|
||||
{
|
||||
type: "tool-call",
|
||||
id: "c1",
|
||||
name: "read",
|
||||
input: { path: "a" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -241,8 +266,16 @@ test("turns see the view and the new message; compactions retry Too long and bui
|
||||
},
|
||||
],
|
||||
},
|
||||
{ id: "m4", role: "assistant", content: [{ type: "text", text: "Read a." }] },
|
||||
{ id: "m5", role: "user", content: [{ type: "text", text: "and now?" }] },
|
||||
{
|
||||
id: "m4",
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "Read a." }],
|
||||
},
|
||||
{
|
||||
id: "m5",
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "and now?" }],
|
||||
},
|
||||
]);
|
||||
// Messages before the new one are summarized before the turn; short ones are their own lines.
|
||||
expect(second.messages.map((message) => message.role)).toEqual(["user", "user"]);
|
||||
@@ -330,7 +363,12 @@ test("dropped reasoning parts and tool messages without ids do not re-log or ski
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "reasoning", text: "" },
|
||||
{ type: "tool-call", id: "c1", name: "read", input: { path: "a" } },
|
||||
{
|
||||
type: "tool-call",
|
||||
id: "c1",
|
||||
name: "read",
|
||||
input: { path: "a" },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -360,9 +398,17 @@ test("dropped reasoning parts and tool messages without ids do not re-log or ski
|
||||
content: message.content.filter((part) => part.type !== "reasoning"),
|
||||
}));
|
||||
await h.turn("ses_root", [
|
||||
{ id: "m0", role: "system", content: [{ type: "text", text: "Instructions updated" }] },
|
||||
{
|
||||
id: "m0",
|
||||
role: "system",
|
||||
content: [{ type: "text", text: "Instructions updated" }],
|
||||
},
|
||||
...after,
|
||||
{ id: "m5", role: "assistant", content: [{ type: "text", text: "ok" }] },
|
||||
{
|
||||
id: "m5",
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "ok" }],
|
||||
},
|
||||
{ id: "m6", role: "user", content: [{ type: "text", text: "again" }] },
|
||||
]);
|
||||
await settle();
|
||||
@@ -378,85 +424,23 @@ test("dropped reasoning parts and tool messages without ids do not re-log or ski
|
||||
]);
|
||||
});
|
||||
|
||||
test("instruction updates before the turn stay after the view", async () => {
|
||||
// Regression: system-role messages before the turn start were dropped.
|
||||
const h = await host();
|
||||
await h.command()({ sessionID: "ses_root", prompt: { text: "on" } });
|
||||
const event = await h.turn("ses_root", [
|
||||
{ id: "m1", role: "user", content: [{ type: "text", text: "hi" }] },
|
||||
{ id: "m2", role: "assistant", content: [{ type: "text", text: "hello" }] },
|
||||
{ id: "m3", role: "system", content: [{ type: "text", text: "Instructions updated" }] },
|
||||
{ id: "m4", role: "user", content: [{ type: "text", text: "next" }] },
|
||||
]);
|
||||
expect(event.messages.map((message) => message.role)).toEqual(["user", "system", "user"]);
|
||||
expect(event.messages[1]!.content[0]!.text).toBe("Instructions updated");
|
||||
if (h.cleanup) await h.cleanup();
|
||||
test("without compactionModel, optchat registers nothing", async () => {
|
||||
const options = { compactionModel: "", dataDir: "" };
|
||||
const registered: string[] = [];
|
||||
const record = (name: string) => async () => {
|
||||
registered.push(name);
|
||||
return { dispose: async () => {} };
|
||||
};
|
||||
const context = {
|
||||
options,
|
||||
tool: { transform: record("tool") },
|
||||
command: { transform: record("command") },
|
||||
session: { hook: record("hook") },
|
||||
};
|
||||
// SAFETY: setup must return before touching any host service; a call is recorded and fails the test.
|
||||
const setup = optchat.setup as (
|
||||
context: Pick<Plugin.Context, "options">,
|
||||
) => ReturnType<typeof optchat.setup>;
|
||||
expect(await setup(context)).toBeUndefined();
|
||||
expect(registered).toEqual([]);
|
||||
});
|
||||
|
||||
test("checkpoints are not logged, provider-executed calls end a turn, and empty replies stop retries", async () => {
|
||||
const h = await host();
|
||||
await h.command()({ sessionID: "ses_root", prompt: { text: "on" } });
|
||||
h.replies.push("");
|
||||
const event = await h.turn("ses_root", [
|
||||
{
|
||||
id: "m1",
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "<conversation-checkpoint>old</conversation-checkpoint>" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "m2",
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool-call",
|
||||
id: "w1",
|
||||
name: "web_search",
|
||||
input: {},
|
||||
providerExecuted: true,
|
||||
},
|
||||
{ type: "text", text: "x".repeat(600) },
|
||||
],
|
||||
},
|
||||
{ id: "m3", role: "user", content: [{ type: "text", text: "next" }] },
|
||||
]);
|
||||
// The turn is the new message; the provider-executed call did not keep the old turn open.
|
||||
expect(event.messages.map((message) => message.content[0]!.text)).toEqual([
|
||||
"<chat>\n0+1|tool: web_search {}\n1+1|(not summarized yet: zoom it)\n</chat>",
|
||||
"next",
|
||||
]);
|
||||
await settle();
|
||||
// The empty reply ended its compaction without a "Too long" retry; the failure is retried at
|
||||
// the next message, as a fresh task.
|
||||
expect(h.generated.map((request) => request.messages.length)).toEqual([2, 2]);
|
||||
if (h.cleanup) await h.cleanup();
|
||||
expect(await logged(h.directory)).toEqual([
|
||||
"tool: web_search {}",
|
||||
`agent: ${"x".repeat(600)}`,
|
||||
"user: next",
|
||||
]);
|
||||
});
|
||||
|
||||
test.each([{}, { compactionModel: "", dataDir: "" }])(
|
||||
"without compactionModel (%j), optchat registers nothing",
|
||||
async (options) => {
|
||||
const registered: string[] = [];
|
||||
const record = (name: string) => async () => {
|
||||
registered.push(name);
|
||||
return { dispose: async () => {} };
|
||||
};
|
||||
const context = {
|
||||
options,
|
||||
tool: { transform: record("tool") },
|
||||
command: { transform: record("command") },
|
||||
session: { hook: record("hook") },
|
||||
};
|
||||
// SAFETY: setup must return before touching any host service; a call is recorded and fails the test.
|
||||
const setup = optchat.setup as (
|
||||
context: Pick<Plugin.Context, "options">,
|
||||
) => ReturnType<typeof optchat.setup>;
|
||||
expect(await setup(context)).toBeUndefined();
|
||||
expect(registered).toEqual([]);
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user