chore: reformat

This commit is contained in:
can1357
2026-07-30 04:25:32 +02:00
parent 2905a23fc1
commit ccd3bb9565
13 changed files with 425 additions and 436 deletions
@@ -4222,60 +4222,60 @@ describe("advisor", () => {
expect(promptInputs[1]).toContain("keep me");
});
it.each([
"success",
"error",
] as const)("releases blocked %s hooks so reset can run replacement work", async hookKind => {
const hookStarted = Promise.withResolvers<void>();
const releaseHook = Promise.withResolvers<void>();
const replacementPromptStarted = Promise.withResolvers<void>();
let promptCalls = 0;
let hookCalls = 0;
const blockHook = async () => {
if (++hookCalls !== 1) return;
hookStarted.resolve();
await releaseHook.promise;
};
const agent: AdvisorAgent = {
prompt: async () => {
promptCalls++;
if (promptCalls === 1 && hookKind === "error") throw new Error("provider failure");
if (promptCalls === 2) replacementPromptStarted.resolve();
},
abort: () => {},
reset: () => {},
state: { messages: [] },
};
const runtime = new AdvisorRuntime(agent, {
snapshotMessages: () => [],
enqueueAdvice: () => {},
...(hookKind === "success"
? { onTurnSuccess: blockHook }
: {
onTurnError: async () => {
await blockHook();
return false;
},
}),
});
it.each(["success", "error"] as const)(
"releases blocked %s hooks so reset can run replacement work",
async hookKind => {
const hookStarted = Promise.withResolvers<void>();
const releaseHook = Promise.withResolvers<void>();
const replacementPromptStarted = Promise.withResolvers<void>();
let promptCalls = 0;
let hookCalls = 0;
const blockHook = async () => {
if (++hookCalls !== 1) return;
hookStarted.resolve();
await releaseHook.promise;
};
const agent: AdvisorAgent = {
prompt: async () => {
promptCalls++;
if (promptCalls === 1 && hookKind === "error") throw new Error("provider failure");
if (promptCalls === 2) replacementPromptStarted.resolve();
},
abort: () => {},
reset: () => {},
state: { messages: [] },
};
const runtime = new AdvisorRuntime(agent, {
snapshotMessages: () => [],
enqueueAdvice: () => {},
...(hookKind === "success"
? { onTurnSuccess: blockHook }
: {
onTurnError: async () => {
await blockHook();
return false;
},
}),
});
runtime.onTurnEnd([{ role: "user", content: "old session", timestamp: 1 } as AgentMessage]);
await hookStarted.promise;
const pause = runtime.pauseForSessionTransition();
const pausedQuickly = await Promise.race([pause.then(() => true), Bun.sleep(50).then(() => false)]);
runtime.reset();
runtime.onTurnEnd([{ role: "user", content: "replacement session", timestamp: 2 } as AgentMessage]);
const replacementRan = await Promise.race([
replacementPromptStarted.promise.then(() => true),
Bun.sleep(50).then(() => false),
]);
releaseHook.resolve();
await pause;
runtime.dispose();
runtime.onTurnEnd([{ role: "user", content: "old session", timestamp: 1 } as AgentMessage]);
await hookStarted.promise;
const pause = runtime.pauseForSessionTransition();
const pausedQuickly = await Promise.race([pause.then(() => true), Bun.sleep(50).then(() => false)]);
runtime.reset();
runtime.onTurnEnd([{ role: "user", content: "replacement session", timestamp: 2 } as AgentMessage]);
const replacementRan = await Promise.race([
replacementPromptStarted.promise.then(() => true),
Bun.sleep(50).then(() => false),
]);
releaseHook.resolve();
await pause;
runtime.dispose();
expect(pausedQuickly).toBe(true);
expect(replacementRan).toBe(true);
});
expect(pausedQuickly).toBe(true);
expect(replacementRan).toBe(true);
},
);
it("aborts retry backoff before pausing for a session transition", async () => {
const recoveryStarted = Promise.withResolvers<void>();
const agent: AdvisorAgent = {
@@ -849,56 +849,57 @@ it("allow_always: caches decision and calls bridge only once for subsequent exec
expect(bashTool.executeCalls).toBe(2);
});
it.each(
boundaryCases,
)("%s permission decisions prompt again after a successful %s session boundary", async (decision, transition) => {
const bashTool = makeFakeTool("bash");
const bridge = makeBridge({ outcome: "selected", optionId: decision, kind: decision });
const permissionSpy = spyOn(bridge, "requestPermission");
session = await createSession([bashTool], bridge, {}, { persist: true });
it.each(boundaryCases)(
"%s permission decisions prompt again after a successful %s session boundary",
async (decision, transition) => {
const bashTool = makeFakeTool("bash");
const bridge = makeBridge({ outcome: "selected", optionId: decision, kind: decision });
const permissionSpy = spyOn(bridge, "requestPermission");
session = await createSession([bashTool], bridge, {}, { persist: true });
await session.setActiveToolsByName(["bash"]);
const wrappedBash = session.agent.state.tools.find(tool => tool.name === "bash");
if (!wrappedBash) throw new Error("Expected wrapped bash tool");
await session.setActiveToolsByName(["bash"]);
const wrappedBash = session.agent.state.tools.find(tool => tool.name === "bash");
if (!wrappedBash) throw new Error("Expected wrapped bash tool");
for (let callIndex = 0; callIndex < 2; callIndex++) {
if (callIndex === 1) {
if (transition === "new") {
expect(await session.newSession()).toBe(true);
for (let callIndex = 0; callIndex < 2; callIndex++) {
if (callIndex === 1) {
if (transition === "new") {
expect(await session.newSession()).toBe(true);
} else {
const targetId = `permission-target-${Bun.nanoseconds()}`;
const targetPath = `${tempDir.path()}/${targetId}.jsonl`;
await Bun.write(
targetPath,
`${JSON.stringify({
type: "session",
version: 3,
id: targetId,
timestamp: new Date().toISOString(),
cwd: tempDir.path(),
})}\n`,
);
expect(await session.switchSession(targetPath)).toBe(true);
}
}
const execution = wrappedBash.execute(
`call-${callIndex}`,
{ command: "echo boundary" },
undefined,
undefined as never,
undefined as never,
);
if (decision === "reject_always") {
await expect(execution).rejects.toThrow(/rejected by user/);
} else {
const targetId = `permission-target-${Bun.nanoseconds()}`;
const targetPath = `${tempDir.path()}/${targetId}.jsonl`;
await Bun.write(
targetPath,
`${JSON.stringify({
type: "session",
version: 3,
id: targetId,
timestamp: new Date().toISOString(),
cwd: tempDir.path(),
})}\n`,
);
expect(await session.switchSession(targetPath)).toBe(true);
await execution;
}
}
const execution = wrappedBash.execute(
`call-${callIndex}`,
{ command: "echo boundary" },
undefined,
undefined as never,
undefined as never,
);
if (decision === "reject_always") {
await expect(execution).rejects.toThrow(/rejected by user/);
} else {
await execution;
}
}
expect(permissionSpy).toHaveBeenCalledTimes(2);
expect(bashTool.executeCalls).toBe(decision === "allow_always" ? 2 : 0);
});
expect(permissionSpy).toHaveBeenCalledTimes(2);
expect(bashTool.executeCalls).toBe(decision === "allow_always" ? 2 : 0);
},
);
// ---------------------------------------------------------------------------
// 4. Read tool not gated: bridge never called even when bridge is set
@@ -203,68 +203,67 @@ describe("AgentSession bash session ownership", () => {
).toBe(true);
});
it.each([
"new",
"switch",
"branch",
] as const)("records a late bash result in its original session after %s", async transition => {
const sessionDir = path.join(tempDir.path(), "sessions");
const { completion, emitUserBash, extensionRunner } = createGatedBashRunner();
createSession(SessionManager.create(tempDir.path(), sessionDir), extensionRunner);
const oldSessionFile = await seedPersistedSession();
const oldSessionId = session.sessionId;
it.each(["new", "switch", "branch"] as const)(
"records a late bash result in its original session after %s",
async transition => {
const sessionDir = path.join(tempDir.path(), "sessions");
const { completion, emitUserBash, extensionRunner } = createGatedBashRunner();
createSession(SessionManager.create(tempDir.path(), sessionDir), extensionRunner);
const oldSessionFile = await seedPersistedSession();
const oldSessionId = session.sessionId;
const bashPromise = session.executeBash("old-session-command");
expect(emitUserBash).toHaveBeenCalledTimes(1);
const bashPromise = session.executeBash("old-session-command");
expect(emitUserBash).toHaveBeenCalledTimes(1);
switch (transition) {
case "new":
await session.newSession();
break;
case "switch": {
const targetManager = SessionManager.create(tempDir.path(), sessionDir);
targetManager.appendMessage({ role: "user", content: "target", timestamp: Date.now() });
targetManager.appendMessage(createAssistantMessage("target reply"));
await targetManager.ensureOnDisk();
const targetFile = targetManager.getSessionFile();
if (!targetFile) throw new Error("Expected target session file");
await targetManager.close();
await session.switchSession(targetFile);
break;
switch (transition) {
case "new":
await session.newSession();
break;
case "switch": {
const targetManager = SessionManager.create(tempDir.path(), sessionDir);
targetManager.appendMessage({ role: "user", content: "target", timestamp: Date.now() });
targetManager.appendMessage(createAssistantMessage("target reply"));
await targetManager.ensureOnDisk();
const targetFile = targetManager.getSessionFile();
if (!targetFile) throw new Error("Expected target session file");
await targetManager.close();
await session.switchSession(targetFile);
break;
}
case "branch": {
const userEntry = session.sessionManager
.getEntries()
.find(entry => entry.type === "message" && entry.message.role === "user");
if (!userEntry) throw new Error("Expected user entry for branch");
await session.branch(userEntry.id);
break;
}
}
case "branch": {
const userEntry = session.sessionManager
.getEntries()
.find(entry => entry.type === "message" && entry.message.role === "user");
if (!userEntry) throw new Error("Expected user entry for branch");
await session.branch(userEntry.id);
break;
}
}
expect(session.sessionId).not.toBe(oldSessionId);
completion.resolve({ result: bashResult });
await bashPromise;
expect(session.sessionId).not.toBe(oldSessionId);
completion.resolve({ result: bashResult });
await bashPromise;
expect(
session.messages.some(
message => message.role === "bashExecution" && message.command === "old-session-command",
),
).toBe(false);
expect(
session.messages.some(
message => message.role === "bashExecution" && message.command === "old-session-command",
),
).toBe(false);
const oldSession = await SessionManager.open(oldSessionFile, sessionDir, undefined, {
initialCwd: tempDir.path(),
suppressBreadcrumb: true,
});
additionalManagers.push(oldSession);
const oldMessages = oldSession.getBranch().flatMap(entry => (entry.type === "message" ? [entry.message] : []));
expect(oldMessages.slice(-3).map(message => message.role)).toEqual(["user", "assistant", "bashExecution"]);
expect(oldMessages.at(-1)).toMatchObject({
role: "bashExecution",
command: "old-session-command",
output: "old-output",
});
});
const oldSession = await SessionManager.open(oldSessionFile, sessionDir, undefined, {
initialCwd: tempDir.path(),
suppressBreadcrumb: true,
});
additionalManagers.push(oldSession);
const oldMessages = oldSession.getBranch().flatMap(entry => (entry.type === "message" ? [entry.message] : []));
expect(oldMessages.slice(-3).map(message => message.role)).toEqual(["user", "assistant", "bashExecution"]);
expect(oldMessages.at(-1)).toMatchObject({
role: "bashExecution",
command: "old-session-command",
output: "old-output",
});
},
);
it("stores minimized bash output with the originating session", async () => {
const sessionDir = path.join(tempDir.path(), "sessions");
@@ -1878,14 +1878,13 @@ describe("InteractiveMode plan review rendering", () => {
// `#approvePlan`'s `finally`. No aborted message_end is required to consume it,
// so a stranded flag could otherwise silence the next unrelated abort. One
// parametrized case per outcome keeps ok/cancelled/failed each covered.
it.each([
"ok",
"cancelled",
"failed",
] as const)("B1-B3: Approve and compact context + %s outcome → flag cleared by finally", async outcome => {
await approveWithCompact(outcome);
expect(session.isPlanInternalAbortPending).toBe(false);
});
it.each(["ok", "cancelled", "failed"] as const)(
"B1-B3: Approve and compact context + %s outcome → flag cleared by finally",
async outcome => {
await approveWithCompact(outcome);
expect(session.isPlanInternalAbortPending).toBe(false);
},
);
it("B4: Approve and compact context + handleCompactCommand throws → showError surfaces the failure AND flag cleared by finally before the outer catch", async () => {
// `handlePlanApproval` wraps `#approvePlan` in a try/catch
@@ -315,27 +315,27 @@ describe("MemoryProtocolHandler", () => {
});
});
it.each([
"memory://root/skills/**/../*.md",
"memory://root/skills/**/%2e%2e/*.md",
])("rejects traversal in a memory glob suffix: %s", async pattern => {
await withMemoryFixture(async ({ cwd }) => {
await expect(createGlobTool(cwd).execute("memory-glob-traversal", { path: pattern })).rejects.toThrow(
/traversal/i,
);
});
});
it.each(["memory://root/skills/**/../*.md", "memory://root/skills/**/%2e%2e/*.md"])(
"rejects traversal in a memory glob suffix: %s",
async pattern => {
await withMemoryFixture(async ({ cwd }) => {
await expect(createGlobTool(cwd).execute("memory-glob-traversal", { path: pattern })).rejects.toThrow(
/traversal/i,
);
});
},
);
it.each([
"memory://root/skills/**/demo%2fnested/*.md",
"memory://root/skills/**/demo%5cnested/*.md",
])("rejects encoded separators in a memory glob suffix: %s", async pattern => {
await withMemoryFixture(async ({ cwd }) => {
await expect(createGlobTool(cwd).execute("memory-glob-separator", { path: pattern })).rejects.toThrow(
/encoded path separator/i,
);
});
});
it.each(["memory://root/skills/**/demo%2fnested/*.md", "memory://root/skills/**/demo%5cnested/*.md"])(
"rejects encoded separators in a memory glob suffix: %s",
async pattern => {
await withMemoryFixture(async ({ cwd }) => {
await expect(createGlobTool(cwd).execute("memory-glob-separator", { path: pattern })).rejects.toThrow(
/encoded path separator/i,
);
});
},
);
it("throws clear error for missing files", async () => {
await withMemoryFixture(async () => {
@@ -97,22 +97,19 @@ describe("task async preflight", () => {
spawns: "scout",
expectation: "Cannot spawn 'task'",
},
])("returns $name policy errors before registering an async job", async ({
name,
params,
settings,
spawns,
expectation,
}) => {
mockDiscovery();
const jobs = manager();
const tool = await TaskTool.create(createSession({ manager: jobs, settings, spawns }));
])(
"returns $name policy errors before registering an async job",
async ({ name, params, settings, spawns, expectation }) => {
mockDiscovery();
const jobs = manager();
const tool = await TaskTool.create(createSession({ manager: jobs, settings, spawns }));
const result = await tool.execute("preflight", params as TaskParams);
const result = await tool.execute("preflight", params as TaskParams);
expect(textOf(result)).toContain(expectation);
expect(jobs.getJob(name)).toBeUndefined();
});
expect(textOf(result)).toContain(expectation);
expect(jobs.getJob(name)).toBeUndefined();
},
);
it("rejects an invalid async batch atomically before dispatching any item", async () => {
mockDiscovery();
@@ -427,25 +427,24 @@ describe("title generator", () => {
expect(title).toBe("Fix login button on mobile");
});
it.each([
"Here's a thinking process:",
"Thinking process:",
"Reasoning process:",
])("rejects a markerless prose thinking preamble: %s", async responseText => {
const model = getModelFor("deepseek", "deepseek-v4-pro");
vi.spyOn(ai, "completeSimple").mockResolvedValue({
stopReason: "stop",
content: [{ type: "text", text: responseText }],
} as never);
it.each(["Here's a thinking process:", "Thinking process:", "Reasoning process:"])(
"rejects a markerless prose thinking preamble: %s",
async responseText => {
const model = getModelFor("deepseek", "deepseek-v4-pro");
vi.spyOn(ai, "completeSimple").mockResolvedValue({
stopReason: "stop",
content: [{ type: "text", text: responseText }],
} as never);
const title = await generateSessionTitle(
"the login button is broken on mobile",
createRegistry(model),
createSettings(model),
);
const title = await generateSessionTitle(
"the login button is broken on mobile",
createRegistry(model),
createSettings(model),
);
expect(title).toBeNull();
});
expect(title).toBeNull();
},
);
it("preserves a markerless title that mentions a <think> tag", async () => {
const model = getModelFor("deepseek", "deepseek-v4-pro");
@@ -114,27 +114,21 @@ describe("default echo/printf redirect rule", () => {
describe("default hub start rules", () => {
const tools = ["hub"];
it.each([
"bun run dev",
"vite --host 0.0.0.0",
"lldb ./app",
"bun test --watch",
"nohup server",
"server &",
])("routes %s to hub start", command => {
const result = checkBashInterception(command, tools, DEFAULT_BASH_INTERCEPTOR_RULES);
expect(result.block).toBe(true);
expect(result.suggestedTool).toBe("hub");
});
it.each(["bun run dev", "vite --host 0.0.0.0", "lldb ./app", "bun test --watch", "nohup server", "server &"])(
"routes %s to hub start",
command => {
const result = checkBashInterception(command, tools, DEFAULT_BASH_INTERCEPTOR_RULES);
expect(result.block).toBe(true);
expect(result.suggestedTool).toBe("hub");
},
);
it.each([
"git diff -w",
"docker compose up -d",
"bun test",
"printf 'server &'",
])("does not misclassify finite command %s", command => {
expect(checkBashInterception(command, tools, DEFAULT_BASH_INTERCEPTOR_RULES).block).toBe(false);
});
it.each(["git diff -w", "docker compose up -d", "bun test", "printf 'server &'"])(
"does not misclassify finite command %s",
command => {
expect(checkBashInterception(command, tools, DEFAULT_BASH_INTERCEPTOR_RULES).block).toBe(false);
},
);
});
describe("BashTool argument validation", () => {