From 1bcf8309ffb522d09d4fd9dc35f7ee957d134e26 Mon Sep 17 00:00:00 2001 From: Drewry Pope Date: Mon, 19 Oct 2026 21:48:07 -0500 Subject: [PATCH 1/2] test(cli): cover ACP follow's stale-attach guard Add a regression test for the guard in turn.follow (packages/cli/src/acp/turn.ts) that stops a stale session/load from interrupting the newer attachment's follower. The test holds load A's MCP registration via the wire fixture's fetch override, lets load B attach and follow, then releases A and asserts a later permission ask is still answered by B's follower. Verified: fails with the guard removed (1 fail, the new test), passes with it; 10/10 under --rerun-each 10; full ACP suite 116/116; cli typecheck clean. Closes #125 --- packages/cli/test/acp/turn.test.ts | 39 ++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/packages/cli/test/acp/turn.test.ts b/packages/cli/test/acp/turn.test.ts index b53be7d67228..4a5ceecddc15 100644 --- a/packages/cli/test/acp/turn.test.ts +++ b/packages/cli/test/acp/turn.test.ts @@ -462,6 +462,45 @@ describe("acp turn events over the wire", () => { expect(acp.server.replies).toEqual([{ sessionID: acp.sessionId, requestID: "perm_after_load", decision: "once" }]) }) + test("a stale load finishing after a newer load does not stop the newer follower", async () => { + let releaseMcp: (() => void) | undefined + const held = new Promise((resolve) => { + releaseMcp = resolve + }) + let mcpHeld = false + await using acp = await startWire({ + // Hold load A's MCP registration so load B attaches and starts following first. + fetch: async (request) => { + if (request.method !== "PUT" || request.path !== "/api/experimental/mcp/tools" || mcpHeld) return undefined + mcpHeld = true + await held + return undefined + }, + permission: () => ({ outcome: { outcome: "selected", optionId: "once" } }), + }) + await acp.initialize() + acp.server.sessions.set("ses_stale", makeSession("ses_stale")) + + const stale = acp.request("session/load", { + sessionId: "ses_stale", + cwd: "/workspace", + mcpServers: [{ name: "tools", command: "bun", args: ["server.ts"], env: [] }], + }) + // The fixture records the request before awaiting the fetch override, so its arrival is observable here. + await acp.until( + () => acp.server.requests.some((item) => item.method === "PUT" && item.path === "/api/experimental/mcp/tools"), + "load A's held MCP registration", + ) + await acp.request("session/load", { sessionId: "ses_stale", cwd: "/workspace", mcpServers: [] }) + releaseMcp!() + await stale + + acp.server.send(permissionAsked("ses_stale", "perm_stale", { action: "read" })) + await acp.until(() => acp.server.replies.length === 1, "the permission reply from the newer follower") + + expect(acp.server.replies).toEqual([{ sessionID: "ses_stale", requestID: "perm_stale", decision: "once" }]) + }) + test("does not name a tool call the follower tracked but never sent", async () => { await using acp = await startSession({ permission: () => ({ outcome: { outcome: "selected", optionId: "once" } }) }) From 0b3373db0661464369666e08fcc6796e28961389 Mon Sep 17 00:00:00 2001 From: Drewry Pope Date: Mon, 19 Oct 2026 21:50:19 -0500 Subject: [PATCH 2/2] test(lint): run ast-grep lint rule tests in CI (#153) --- .github/workflows/test.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 365cf5c3943b..35dac4fa5e49 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -127,6 +127,13 @@ jobs: export PATH="$bin_dir:$PATH" rg --version + - name: Test lint rules + # The main ast-grep rules (script/ast-grep/rules) have their own + # test suite (test:lint-rules) that nothing ran (#153); the + # effect-simplification suite below was already wired up. + if: runner.os == 'Linux' + run: bun run test:lint-rules + - name: Test Effect simplification rules if: runner.os == 'Linux' run: bun run test:effect-simplification-rules