From befab09aa4f50ff574f40fdac447fc6447562922 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Tue, 25 Aug 2026 13:35:04 +0200 Subject: [PATCH] test(agent-manager): cover diff cancellation races --- .../kilo-vscode/tests/unit/git-ops.test.ts | 14 +++++++++++ .../kilo-vscode/tests/unit/local-diff.test.ts | 16 ++++++++++++ .../tests/unit/source-controller.test.ts | 25 +++++++++++++++++++ .../agent-manager/AgentManagerApp.tsx | 1 - 4 files changed, 55 insertions(+), 1 deletion(-) diff --git a/packages/kilo-vscode/tests/unit/git-ops.test.ts b/packages/kilo-vscode/tests/unit/git-ops.test.ts index 05c768c93a..c90aa3a7c0 100644 --- a/packages/kilo-vscode/tests/unit/git-ops.test.ts +++ b/packages/kilo-vscode/tests/unit/git-ops.test.ts @@ -667,6 +667,20 @@ describe("GitOps", () => { }) }) + it("kills an in-flight exec when its request signal aborts", async () => { + await withRepo(async (cwd) => { + const git = new GitOps({ log: () => undefined, binary: async () => process.execPath }) + const ctl = new AbortController() + const pending = git.execGit(["-e", "setTimeout(() => {}, 5000)"], cwd, { signal: ctl.signal }) + await sleep(25) + ctl.abort() + + const result = await pending + expect(result.code).not.toBe(0) + git.dispose() + }) + }) + it("is safe to call multiple times", () => { const git = ops(async () => "ok") git.dispose() diff --git a/packages/kilo-vscode/tests/unit/local-diff.test.ts b/packages/kilo-vscode/tests/unit/local-diff.test.ts index 1e947e34ae..595d0bf349 100644 --- a/packages/kilo-vscode/tests/unit/local-diff.test.ts +++ b/packages/kilo-vscode/tests/unit/local-diff.test.ts @@ -365,6 +365,22 @@ describe("diffFile", () => { }) }) + it("does not cache detail that is aborted before Git completes", async () => { + await withRepo(async (dir, base) => { + await fs.writeFile(path.join(dir, "seed.txt"), "seed\ncached\n") + const local = createLocalDiff(git()) + await local.summary(dir, base) + + const ctl = new AbortController() + const pending = local.file(dir, base, "seed.txt", ctl.signal) + ctl.abort() + await expect(pending).rejects.toThrow() + + const result = await local.file(dir, base, "seed.txt") + expect(result?.after).toBe("seed\ncached\n") + }) + }) + it("invalidates cached detail after the summary stamp changes", async () => { await withRepo(async (dir, base) => { await fs.writeFile(path.join(dir, "seed.txt"), "seed\nfirst\n") diff --git a/packages/kilo-vscode/tests/unit/source-controller.test.ts b/packages/kilo-vscode/tests/unit/source-controller.test.ts index c08673ced3..915cdc13fb 100644 --- a/packages/kilo-vscode/tests/unit/source-controller.test.ts +++ b/packages/kilo-vscode/tests/unit/source-controller.test.ts @@ -489,4 +489,29 @@ describe("SourceController.refresh", () => { controller.stop() }) + + it("shares an in-flight fetch between refresh and polling callers", async () => { + let release!: () => void + const gate = new Promise((resolve) => { + release = resolve + }) + let fetches = 0 + const source: DiffSource = { + descriptor: SESSION_DESC, + async fetch() { + fetches++ + await gate + return { diffs: [] } + }, + } + const { controller } = make({ "session:s1": source }) + controller.setContext({ workspaceRoot: "/repo", sessionId: "s1" }) + const activation = controller.activate("session:s1", { poll: false }) + const refresh = controller.refresh() + release() + await Promise.all([activation, refresh]) + + expect(fetches).toBe(1) + controller.stop() + }) }) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index f076530635..aa879913b5 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -711,7 +711,6 @@ const AgentManagerContent: Component = () => { createEffect(() => { const ids = new Set(worktrees().map((wt) => wt.id)) composers.prune(ids) - composers.prune(ids) setReviewOpenByContext((prev) => { const next = pruneReviewState(prev, currentProjectId() ?? "single", ids) if (Object.keys(next).length === Object.keys(prev).length) return prev