Route ACP-backed planning and validation through a read-only ask-once runner with a pinned Claude bridge foundation. - Add askAcpOnce for single-turn ACP sessions with timeout handling, JSON recovery, clean stop validation, and disposal. - Refactor validation seams to use ACP runtime prompts and require structured pass verdicts. - Resolve the Claude ACP bridge from the plugin bundle and add setup checks for identity, environment, probing, and auth readiness. - Document the ACP Route B plan and update tests for validator, session, runtime, and plugin setup behavior. Files changed: CONCEPTS.md | 6 + docs/acp-contract.md | 36 ++ .../2026-06-14-001-feat-claude-acp-runtime-plan.md | 465 +++++++++++++++++++++ .../engine/src/__tests__/cli-agent-ask.test.ts | 104 +++++ .../src/__tests__/cli-agent-validator.test.ts | 137 +++--- .../src/__tests__/interactive-ai-session.test.ts | 96 +++-- packages/engine/src/agent-runtime.ts | 6 +- packages/engine/src/cli-agent-ask.ts | 120 ++++++ packages/engine/src/cli-agent-validator.ts | 65 ++- .../cli-agent/__tests__/one-shot-session.test.ts | 16 +- packages/engine/src/cli-agent/one-shot-session.ts | 17 +- packages/engine/src/index.ts | 8 +- packages/engine/src/interactive-ai-session.ts | 33 +- plugins/fusion-plugin-acp-runtime/AGENTS.md | 14 + plugins/fusion-plugin-acp-runtime/CHANGELOG.md | 6 + plugins/fusion-plugin-acp-runtime/README.md | 13 +- plugins/fusion-plugin-acp-runtime/package.json | 3 +- .../src/__tests__/index.test.ts | 51 ++- .../src/__tests__/process-manager.test.ts | 32 +- .../src/__tests__/runtime-adapter.test.ts | 4 +- .../src/__tests__/setup.test.ts | 71 ++++ plugins/fusion-plugin-acp-runtime/src/cli-spawn.ts | 95 ++++- plugins/fusion-plugin-acp-runtime/src/index.ts | 16 +- .../src/process-manager.ts | 26 +- .../src/runtime-adapter.ts | 11 +- plugins/fusion-plugin-acp-runtime/src/setup.ts | 104 +++++ plugins/fusion-plugin-acp-runtime/src/types.ts | 6 +- pnpm-lock.yaml | 139 ++++-- 28 files changed, 1502 insertions(+), 198 deletions(-) Fusion-Task-Id: FN-6457 Fusion-Task-Lineage: a3364ed7-cb28-4a2b-b898-6ccd0d95fb92
99 lines
3.4 KiB
TypeScript
99 lines
3.4 KiB
TypeScript
import { describe, it, expect, afterEach } from "vitest";
|
|
import os from "node:os";
|
|
import { fileURLToPath } from "node:url";
|
|
import { AcpRuntimeAdapter } from "../runtime-adapter.js";
|
|
import { killAllProcesses, activeProcessCount } from "../process-manager.js";
|
|
import type { AcpSession, AgentRuntimeOptions } from "../types.js";
|
|
|
|
const FIXTURE = fileURLToPath(new URL("./fixtures/echo-agent.mjs", import.meta.url));
|
|
|
|
afterEach(() => {
|
|
killAllProcesses();
|
|
});
|
|
|
|
function makeAdapter(extra: Record<string, unknown> = {}) {
|
|
return new AcpRuntimeAdapter({
|
|
acpBinaryPath: process.execPath,
|
|
acpArgs: [FIXTURE],
|
|
acpModel: "echo-agent",
|
|
...extra,
|
|
});
|
|
}
|
|
|
|
function makeOptions(over: Partial<AgentRuntimeOptions> = {}): AgentRuntimeOptions {
|
|
return {
|
|
cwd: process.cwd(),
|
|
systemPrompt: "be helpful",
|
|
...over,
|
|
};
|
|
}
|
|
|
|
describe("AcpRuntimeAdapter (U3)", () => {
|
|
it("createSession spawns + opens a session with a real sessionId", async () => {
|
|
const adapter = makeAdapter();
|
|
const { session } = await adapter.createSession(makeOptions());
|
|
try {
|
|
expect(session.sessionId.length).toBeGreaterThan(0);
|
|
expect((session as AcpSession).connection).toBeDefined();
|
|
expect(session.lastModelDescription).toBe("acp/echo-agent");
|
|
} finally {
|
|
await adapter.dispose(session);
|
|
}
|
|
});
|
|
|
|
it("createSession persists actionGateContext and cwd on the session", async () => {
|
|
const adapter = makeAdapter();
|
|
const gate = { permissionPolicy: { rules: { command_execution: "allow" as const } } };
|
|
// cwd must be a real, spawnable directory (it is the subprocess cwd too).
|
|
const cwd = os.tmpdir();
|
|
const { session } = await adapter.createSession(
|
|
makeOptions({ cwd, actionGateContext: gate }),
|
|
);
|
|
try {
|
|
// Both reachable from the session object for the U5/U7 handlers to read.
|
|
expect((session as AcpSession).gate).toBe(gate);
|
|
expect(session.cwd).toBe(cwd);
|
|
} finally {
|
|
await adapter.dispose(session);
|
|
}
|
|
});
|
|
|
|
it("promptWithFallback drives a full turn to completion and surfaces stopReason", async () => {
|
|
const adapter = makeAdapter();
|
|
const { session } = await adapter.createSession(makeOptions());
|
|
try {
|
|
await expect(adapter.promptWithFallback(session, "hello")).resolves.toEqual({ stopReason: "end_turn" });
|
|
} finally {
|
|
await adapter.dispose(session);
|
|
}
|
|
});
|
|
|
|
it("dispose tears down the subprocess and is idempotent", async () => {
|
|
const adapter = makeAdapter();
|
|
const { session } = await adapter.createSession(makeOptions());
|
|
expect(activeProcessCount()).toBe(1);
|
|
await adapter.dispose(session);
|
|
expect(activeProcessCount()).toBe(0);
|
|
// second dispose must not throw
|
|
await expect(adapter.dispose(session)).resolves.toBeUndefined();
|
|
expect(activeProcessCount()).toBe(0);
|
|
});
|
|
|
|
it("promptWithFallback rejects when the session has no live connection", async () => {
|
|
const adapter = makeAdapter();
|
|
await expect(
|
|
adapter.promptWithFallback({ sessionId: "x" } as never, "hi"),
|
|
).rejects.toThrow(/no live connection/);
|
|
});
|
|
|
|
it("describeModel returns the session model description", async () => {
|
|
const adapter = makeAdapter();
|
|
const { session } = await adapter.createSession(makeOptions());
|
|
try {
|
|
expect(adapter.describeModel(session)).toBe("acp/echo-agent");
|
|
} finally {
|
|
await adapter.dispose(session);
|
|
}
|
|
});
|
|
});
|