From 71d613d3413a6b860d237a9d94182e054ddaafc8 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Tue, 20 Oct 2026 08:17:44 +0200 Subject: [PATCH] test(server): reproduce false ACP cancellation settlement Refs #3256. A real ACP subprocess that withholds its prompt response reproduces settled cancellation followed by a foreground-turn-active rejection. The settling control passes; the ownership regression intentionally fails. Co-authored-by: Ouyang Xingyuan --- .../agent/acp-cancel-ownership.repro.test.ts | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 packages/server/src/server/agent/acp-cancel-ownership.repro.test.ts diff --git a/packages/server/src/server/agent/acp-cancel-ownership.repro.test.ts b/packages/server/src/server/agent/acp-cancel-ownership.repro.test.ts new file mode 100644 index 0000000000..c7987cf604 --- /dev/null +++ b/packages/server/src/server/agent/acp-cancel-ownership.repro.test.ts @@ -0,0 +1,81 @@ +import { mkdtemp, writeFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; +import { pathToFileURL } from "node:url"; +import { expect, test } from "vitest"; +import { AgentManager } from "./agent-manager.js"; +import { buildProviderRegistry } from "./provider-registry.js"; +import { AgentStorage } from "./agent-storage.js"; +import { createTestLogger } from "../../test-utils/test-logger.js"; + +// A real ACP subprocess: cancel is delivered, but the prompt response is +// deliberately withheld. This models the reported protocol condition rather +// than a particular provider's reason for failing to finish its prompt. +const agentSource = ` +import { Readable, Writable } from 'node:stream'; +const { AgentSideConnection, PROTOCOL_VERSION, ndJsonStream } = await import(process.env.ACP_SDK_URL); +let finishPrompt; +new AgentSideConnection(() => ({ + async initialize() { return { protocolVersion: PROTOCOL_VERSION, agentCapabilities: {} }; }, + async newSession() { return { sessionId: 'cancel-ownership-session' }; }, + async authenticate() {}, + prompt() { return new Promise(resolve => { finishPrompt = resolve; }); }, + async cancel() { + if (process.env.SETTLE_ON_CANCEL === 'true') finishPrompt({ stopReason: 'cancelled' }); + }, +}), ndJsonStream(Writable.toWeb(process.stdout), Readable.toWeb(process.stdin))); +`; + +for (const settles of [true, false]) { + test(`cancel preserves truthful ownership when ACP prompt settles=${settles}`, async () => { + const dir = await mkdtemp(path.join(tmpdir(), "acp-cancel-ownership-")); + const script = path.join(dir, "agent.mjs"); + await writeFile(script, agentSource); + const logger = createTestLogger(); + const registry = buildProviderRegistry(logger, { + providerOverrides: { + "cancel-probe": { + extends: "acp", + label: "Cancel probe", + command: [process.execPath, script], + env: { + ACP_SDK_URL: pathToFileURL( + createRequire(import.meta.url).resolve("@agentclientprotocol/sdk"), + ).href, + SETTLE_ON_CANCEL: String(settles), + }, + }, + }, + }); + const manager = new AgentManager({ + clients: { "cancel-probe": registry["cancel-probe"].createClient(logger) }, + registry: new AgentStorage(path.join(dir, "agents"), logger), + logger, + }); + let agentId: string | undefined; + try { + const agent = await manager.createAgent({ provider: "cancel-probe", cwd: dir }, undefined, { + workspaceId: undefined, + }); + agentId = agent.id; + const first = manager.runAgent(agent.id, "hold this turn until cancellation"); + await manager.waitForAgentRunStart(agent.id); + const cancellation = await manager.cancelAgentRun(agent.id); + // Record the consumer-visible symptom before the regression assertion. + if (!settles && cancellation.status === "settled") { + await first; + expect(manager.getAgent(agent.id)?.lifecycle).toBe("idle"); + await expect(manager.runAgent(agent.id, "try again")).rejects.toThrow( + "A foreground turn is already active", + ); + } + expect(cancellation).toEqual({ status: settles ? "settled" : "refused" }); + if (settles) await first; + else void first.catch(() => undefined); + } finally { + if (agentId) await manager.closeAgent(agentId); + await rm(dir, { recursive: true, force: true }); + } + }); +}