import assert from "node:assert/strict"; import test from "node:test"; import type { ExtensionAPI, ExtensionContext, } from "@earendil-works/pi-coding-agent"; import type { WorkflowCommand, WorkflowView } from "./engine.ts"; import { compactWorkflowDetails, registerPiWorkflowTool, workflowCommandFromTool, } from "./tool.ts"; function workflowView(overrides: Partial = {}): WorkflowView { return { runId: "run-1", revision: 4, status: "waiting", workspace: { state: "ready", sourceRoot: "/repo", root: "/repo/.worktrees/run-1", branch: "feature/search", baseHead: "abc123", }, uiImpactEvidence: [], affectedUiSurfaces: [], stages: [], assignments: [], mutatorProfileNames: [], reviewCycles: 0, verificationCycles: 0, ...overrides, background: overrides.background ?? false, }; } test("tool commands derive active tools for feature and analysis scripts", () => { assert.deepEqual( workflowCommandFromTool( { action: "start", request: "Build search", featureName: "search-page", idempotencyKey: "feature-key", workflow: { mode: "feature-script", source: "return null", args: '{"scope":"routes"}', insertionPoint: "before-design", }, }, ["read", "grep", "bash"], ), { action: "start", request: "Build search", featureName: "search-page", idempotencyKey: "feature-key", script: { source: "return null", args: { scope: "routes" }, insertionPoint: "before-design", activeToolNames: ["read", "grep", "bash"], }, }, ); const analysis = workflowCommandFromTool( { action: "start", request: "Audit persistence", workflow: { mode: "analysis-script", source: "return await assign('review', 'Inspect')", }, idempotencyKey: "analysis-key", }, ["read", "ls"], ); assert.equal(analysis.action, "start-analysis"); assert.deepEqual( analysis.action === "start-analysis" ? analysis.activeToolNames : [], ["read", "ls"], ); }); test("tool commands reject open or incomplete action shapes", () => { assert.throws( () => workflowCommandFromTool( { action: "start", request: "Build search", featureName: "search-page", idempotencyKey: "feature-key", workflow: { mode: "feature-script", source: "return null" }, }, ["read"], ), /requires workflow.source and workflow.insertionPoint/, ); assert.throws( () => workflowCommandFromTool( { action: "decide", runId: "run-1", gateId: "gate-1", expectedRevision: 2, idempotencyKey: "decision-key", decision: undefined, }, ["read"], ), /decision is required/, ); }); test("compact tool details expose only valid continuation intents", () => { const details = compactWorkflowDetails( "inspect", workflowView({ openGate: { id: "gate-1", kind: "implementation-context", status: "open", revision: 4, openedAt: 1, }, planArtifact: { path: "/state/plan.md", revision: 2, digest: "digest" }, implementationContext: { gateId: "gate-1", runRevision: 3, planRevision: 2, preparationId: "prep-1", activeParentSessionId: "session-1", strategy: "fresh-session", status: "handoff-pending", method: "fresh-session", }, }), ); assert.deepEqual(details.continuation, { command: "/workflow continue", runId: "run-1", }); assert.deepEqual(details.openGate?.payload, { preparationId: "prep-1", planRevision: 2, contextRevision: 3, strategy: "fresh-session", status: "handoff-pending", method: "fresh-session", outcome: undefined, error: undefined, }); assert.equal( compactWorkflowDetails( "inspect", workflowView({ openGate: { id: "gate-1", kind: "implementation-context", status: "open", revision: 4, openedAt: 1, }, planArtifact: { path: "/state/plan.md", revision: 2, digest: "digest" }, implementationContext: { gateId: "gate-1", runRevision: 3, planRevision: 2, preparationId: "prep-1", activeParentSessionId: "session-1", strategy: "fresh-session", status: "handoff-pending", method: "fresh-session", }, }), "session-2", ).continuation, undefined, ); assert.equal( compactWorkflowDetails("inspect", workflowView()).continuation, undefined, ); }); test("tool cancellation aborts its active Workflow Run before rejecting", async () => { let definition: Parameters[0] | undefined; const commands: WorkflowCommand[] = []; let finishDecision = (_view: WorkflowView) => {}; const decision = new Promise((resolve) => { finishDecision = resolve; }); const pi = { registerTool(candidate: Parameters[0]) { definition = candidate; }, getActiveTools: () => ["read"], } as unknown as ExtensionAPI; registerPiWorkflowTool(pi, { store: { async loadRun() { return { runId: "run-1", revision: 4, status: "running" }; }, } as never, async engineForContext() { return { async execute(command: WorkflowCommand) { commands.push(structuredClone(command)); if (command.action === "abort") { const aborted = workflowView({ status: "aborted", revision: 5 }); finishDecision(aborted); return aborted; } return decision; }, } as never; }, }); assert.ok(definition); const controller = new AbortController(); const operation = definition.execute( "cancel-call", { action: "decide", runId: "run-1", gateId: "gate-1", expectedRevision: 4, idempotencyKey: "decision-key", decision: { kind: "recover", resolution: "inspect" }, }, controller.signal, undefined, { sessionManager: { getSessionId: () => "session-1" }, } as unknown as ExtensionContext, ); controller.abort(new Error("cancelled by test")); await assert.rejects(operation, /cancelled by test/); assert.equal(commands.some((command) => command.action === "abort"), true); }); test("registered tool executes through the context engine and injects host tools", async () => { let definition: Parameters[0] | undefined; const commands: WorkflowCommand[] = []; const pi = { registerTool(candidate: Parameters[0]) { definition = candidate; }, getActiveTools() { return ["read", "smart_compact"]; }, } as unknown as ExtensionAPI; registerPiWorkflowTool(pi, { store: {} as never, async engineForContext() { return { async execute(command: WorkflowCommand) { commands.push(structuredClone(command)); return workflowView(); }, } as never; }, }); assert.equal(definition?.name, "pi_workflow"); assert.ok(definition); await definition.execute( "call-1", { action: "decide", runId: "run-1", gateId: "gate-1", expectedRevision: 4, idempotencyKey: "context-report", decision: { kind: "report-context-preparation", preparationId: "prep-1", planRevision: 2, method: "smart_compact", outcome: "succeeded", }, }, new AbortController().signal, undefined, { sessionManager: { getSessionId: () => "session-1" }, } as unknown as ExtensionContext, ); assert.deepEqual( commands[0]?.action === "decide" ? commands[0].activeToolNames : undefined, ["read", "smart_compact"], ); });