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
72 lines
3.1 KiB
TypeScript
72 lines
3.1 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { join } from "node:path";
|
|
import { checkSetup, setupManifest, validateBundledBridgeIdentity } from "../setup.js";
|
|
import { CLAUDE_CODE_CLI_ACP_BINARY, bundledClaudeBridgeBinPath } from "../cli-spawn.js";
|
|
import type { AcpProbeStatus, ProbeOptions } from "../probe.js";
|
|
|
|
function ctx(settings: Record<string, unknown> = {}) {
|
|
return { settings } as never;
|
|
}
|
|
|
|
function probe(status: AcpProbeStatus) {
|
|
return async (_opts: ProbeOptions) => status;
|
|
}
|
|
|
|
describe("ACP setup manifest", () => {
|
|
it("describes the bundled Claude bridge", () => {
|
|
expect(setupManifest.binaryName).toBe(CLAUDE_CODE_CLI_ACP_BINARY);
|
|
expect(setupManifest.channel).toBe("beta");
|
|
});
|
|
});
|
|
|
|
describe("validateBundledBridgeIdentity", () => {
|
|
it("accepts the plugin-owned node_modules bin shim", () => {
|
|
expect(validateBundledBridgeIdentity(bundledClaudeBridgeBinPath())).toBeUndefined();
|
|
});
|
|
|
|
it("rejects a PATH-resolved substitute outside plugin node_modules", () => {
|
|
const pluginRoot = "/repo/plugins/fusion-plugin-acp-runtime";
|
|
const err = validateBundledBridgeIdentity("/usr/local/bin/claude-code-cli-acp", pluginRoot);
|
|
expect(err).toContain("must come from this plugin's node_modules");
|
|
});
|
|
|
|
it("accepts nested package files inside plugin node_modules", () => {
|
|
const pluginRoot = "/repo/plugins/fusion-plugin-acp-runtime";
|
|
const packageBin = join(pluginRoot, "node_modules", "claude-code-cli-acp", "bin", "claude-code-cli-acp.js");
|
|
expect(validateBundledBridgeIdentity(packageBin, pluginRoot)).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("checkSetup", () => {
|
|
it("reports installed when the bridge handshakes", async () => {
|
|
const result = await checkSetup(ctx(), { probe: probe({ ok: true, reason: "ok", authRequired: false }) });
|
|
expect(result.status).toBe("installed");
|
|
expect(result.binaryPath).toContain("claude-code-cli-acp");
|
|
});
|
|
|
|
it("maps missing_binary to a not-installed install hint", async () => {
|
|
const result = await checkSetup(ctx(), {
|
|
probe: probe({ ok: false, reason: "missing_binary", detail: "ENOENT" }),
|
|
});
|
|
expect(result.status).toBe("not-installed");
|
|
expect(result.error).toContain("Install bundled dependency");
|
|
});
|
|
|
|
it("maps authRequired ok status to the claude auth hint", async () => {
|
|
const result = await checkSetup(ctx(), { probe: probe({ ok: true, reason: "ok", authRequired: true }) });
|
|
expect(result.status).toBe("error");
|
|
expect(result.error).toContain("run `claude` once to authenticate");
|
|
});
|
|
|
|
it("maps handshake_timeout and incompatible_protocol to distinct errors", async () => {
|
|
const timeout = await checkSetup(ctx(), {
|
|
probe: probe({ ok: false, reason: "handshake_timeout", detail: "initialize timed out" }),
|
|
});
|
|
const incompatible = await checkSetup(ctx(), {
|
|
probe: probe({ ok: false, reason: "incompatible_protocol", detail: "protocol 999", protocolVersion: 999 }),
|
|
});
|
|
expect(timeout).toMatchObject({ status: "error", error: "initialize timed out" });
|
|
expect(incompatible).toMatchObject({ status: "error", error: "protocol 999" });
|
|
});
|
|
});
|