import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import * as path from "node:path"; import test from "node:test"; import type { ExtensionAPI, ExtensionCommandContext, } from "@earendil-works/pi-coding-agent"; import { DEFAULT_WORKFLOW_CONFIG, type WorkflowRun, } from "./domain.ts"; import type { WorkflowCommand, WorkflowView } from "./engine.ts"; import { openWorkflowDashboard } from "./dashboard.ts"; function run(overrides: Partial = {}): WorkflowRun { return { runId: "run-1", revision: 12, mode: "feature", status: "waiting", background: false, request: "Build search page", featureName: "search-page", parentSessionIds: ["session-1"], lease: { processId: 100, activeParentSessionId: "session-1", heartbeatAt: 1, }, workspace: { state: "ready", sourceRoot: "/repo", root: "/repo/.worktrees/workflow-search-page-run-1", branch: "feature/search-page", baseHead: "abc123", }, effectiveConfig: structuredClone(DEFAULT_WORKFLOW_CONFIG), pinnedProfiles: [], preflightEligibility: [], facts: { stage: "design", taskKind: "feature", requestTerms: ["search"], candidatePaths: [], languages: [], tags: [], riskTags: [], }, stages: [ { id: "discover", kind: "discover", status: "completed" }, { id: "design", kind: "design", status: "waiting" }, { id: "prototype", kind: "prototype", status: "pending" }, { id: "implement", kind: "implement", status: "pending" }, { id: "verify", kind: "verify", status: "pending" }, { id: "review", kind: "review", status: "pending" }, ], gates: [ { id: "gate-1", kind: "plan-approval", status: "open", revision: 12, openedAt: 1, }, ], decisions: [], assignments: [], prototypeCandidates: [], prototypeCleanupCompleted: false, deliveries: [], commandReceipts: [], uiImpact: "minor", uiImpactEvidence: ["One visible field changes"], affectedUiSurfaces: ["Search page"], mutatorProfileNames: [], reviewCycles: 0, verificationCycles: 0, ...overrides, }; } function prototypeCandidate() { return { id: "candidate-1", revision: 2, assignmentId: "prototype-assignment-1", profileName: "frontend-prototyper", status: "succeeded" as const, path: "/repo/prototypes/candidate-1", route: "/search?variant=1", structuralRationale: "Compact drill-down flow", responsiveChecks: ["mobile", "desktop"], browserEvidence: ["validated in browser"], launchInstructions: "Run the prototype server.", visualDigest: "visual-digest", artifactPath: "/state/artifacts/candidate-1.md", retainedArtifacts: ["/state/artifacts/candidate-1.png"], }; } function view(workflow: WorkflowRun): WorkflowView { return { runId: workflow.runId, featureName: workflow.featureName, request: workflow.request, revision: workflow.revision, status: workflow.status, background: workflow.background, workspace: workflow.workspace, openGate: workflow.gates.find((gate) => gate.status === "open"), uiImpact: workflow.uiImpact, uiImpactEvidence: workflow.uiImpactEvidence, affectedUiSurfaces: workflow.affectedUiSurfaces, planArtifact: workflow.planArtifact, reportArtifact: workflow.reportArtifact, implementationContext: workflow.implementationContext, stages: workflow.stages, assignments: workflow.assignments, mutatorProfileNames: workflow.mutatorProfileNames, reviewCycles: workflow.reviewCycles, verificationCycles: workflow.verificationCycles, }; } function tuiContext(results: unknown[], sessionId = "session-1") { const notifications: string[] = []; const renders: string[][] = []; let customCalls = 0; const theme = { fg: (_color: string, text: string) => text, bg: (_color: string, text: string) => text, bold: (text: string) => text, }; const unsupported = () => { throw new Error("built-in bounded dialog must not be called"); }; const context = { hasUI: true, mode: "tui", sessionManager: { getSessionId: () => sessionId, }, ui: { custom: async ( factory: ( tui: { requestRender(): void; terminal: { columns: number; rows: number } }, overlayTheme: typeof theme, keybindings: object, done: (value: unknown) => void, ) => { render(width: number): string[] }, ) => { customCalls++; const component = factory( { requestRender() {}, terminal: { columns: 100, rows: 24 }, }, theme, {}, () => {}, ); renders.push(component.render(100)); return results.shift(); }, select: unsupported, confirm: unsupported, input: unsupported, editor: unsupported, notify: (message: string) => notifications.push(message), }, } as unknown as ExtensionCommandContext; return { context, notifications, renders, customCalls: () => customCalls }; } function interactiveTuiContext(inputs: string[][], sessionId = "session-1") { let customCalls = 0; const theme = { fg: (_color: string, text: string) => text, bg: (_color: string, text: string) => text, bold: (text: string) => text, }; const context = { hasUI: true, mode: "tui", sessionManager: { getSessionId: () => sessionId }, ui: { async custom( factory: ( tui: { requestRender(): void; terminal: { columns: number; rows: number } }, overlayTheme: typeof theme, keybindings: object, done: (value: unknown) => void, ) => { render(width: number): string[]; handleInput?(data: string): void; }, ) { customCalls++; let completed: unknown; const component = factory( { requestRender() {}, terminal: { columns: 80, rows: 16 }, }, theme, {}, (value) => { completed = value; }, ); component.render(56); for (const input of inputs.shift() ?? []) component.handleInput?.(input); return completed; }, notify() {}, }, } as unknown as ExtensionCommandContext; return { context, customCalls: () => customCalls }; } const pi = { getActiveTools: () => ["read"], } as unknown as ExtensionAPI; test("dashboard fresh-session action returns an immediate continuation result", async () => { const workflow = run({ planArtifact: { path: "/state/plan.md", revision: 3, digest: "digest" }, gates: [ { id: "context-gate", kind: "implementation-context", status: "open", revision: 12, openedAt: 1, }, ], implementationContext: { gateId: "context-gate", runRevision: 12, planRevision: 3, preparationId: "prep-1", activeParentSessionId: "session-1", strategy: "fresh-session", status: "handoff-pending", method: "fresh-session", }, }); const tui = tuiContext(["run-1", "continue"]); const result = await openWorkflowDashboard( pi, { store: { listRuns: async () => [workflow] } as never, engineForContext: () => Promise.reject(new Error("unused")), }, tui.context, ); assert.deepEqual(result, { continueRunId: "run-1" }); assert.equal(tui.customCalls(), 2); }); test("dashboard records Plan Approval using only custom TUI overlays", async () => { const workflow = run({ planArtifact: { path: "/state/artifacts/plan.md", revision: 3, digest: "digest", }, }); const commands: WorkflowCommand[] = []; const tui = tuiContext([ "run-1", "decide", "approve", true, undefined, undefined, ]); await openWorkflowDashboard( pi, { store: { listRuns: async () => [workflow] } as never, engineForContext: async () => ({ async execute(command: WorkflowCommand) { commands.push(structuredClone(command)); return view(workflow); }, }) as never, }, tui.context, ); const decision = commands[0]; assert.equal(decision?.action, "decide"); assert.deepEqual( decision?.action === "decide" ? decision.decision : undefined, { kind: "approve-plan", approved: true, acceptedUiImpact: "minor", }, ); assert.ok(tui.customCalls() >= 4); }); test("prototype reader Escape returns to candidate actions without accepting", async () => { const candidate = prototypeCandidate(); const workflow = run({ stages: run().stages.map((stage) => stage.kind === "prototype" ? { ...stage, status: "waiting" as const } : stage, ), gates: [ { id: "prototype-gate", kind: "prototype-acceptance", status: "open", revision: 12, openedAt: 1, }, ], prototypeCandidates: [candidate], }); const commands: WorkflowCommand[] = []; const tui = tuiContext([ "run-1", "decide", { kind: "candidate", candidate }, "review", undefined, "back", undefined, undefined, undefined, ]); await openWorkflowDashboard( pi, { store: { listRuns: async () => [workflow] } as never, engineForContext: async () => ({ async execute(command: WorkflowCommand) { commands.push(structuredClone(command)); return view(workflow); }, }) as never, }, tui.context, ); assert.deepEqual(commands, []); assert.ok( tui.renders.filter((lines) => lines.join("\n").includes("Prototype 2")) .length >= 2, ); }); test("prototype acceptance requires the explicit candidate Accept action", async () => { const candidate = prototypeCandidate(); const workflow = run({ gates: [ { id: "prototype-gate", kind: "prototype-acceptance", status: "open", revision: 12, openedAt: 1, }, ], prototypeCandidates: [candidate], }); const commands: WorkflowCommand[] = []; const tui = tuiContext([ "run-1", "decide", { kind: "candidate", candidate }, "review", undefined, "accept", true, "", undefined, undefined, ]); await openWorkflowDashboard( pi, { store: { listRuns: async () => [workflow] } as never, engineForContext: async () => ({ async execute(command: WorkflowCommand) { commands.push(structuredClone(command)); return view(workflow); }, }) as never, }, tui.context, ); const decision = commands[0]; assert.deepEqual( decision?.action === "decide" ? decision.decision : undefined, { kind: "accept-prototype", candidateId: candidate.id, candidateRevision: candidate.revision, browserReviewed: true, }, ); }); test("Escape from run detail returns one level to the run list", async () => { const tui = tuiContext(["run-1", undefined, undefined]); const result = await openWorkflowDashboard( pi, { store: { listRuns: async () => [run()] } as never, engineForContext: () => Promise.reject(new Error("unused")), }, tui.context, ); assert.deepEqual(result, {}); assert.equal(tui.customCalls(), 3); }); test("dashboard handleInput drives selection and Escape back-stack navigation", async () => { const tui = interactiveTuiContext([["\r"], ["\x1b"], ["\x1b"]]); const result = await openWorkflowDashboard( pi, { store: { listRuns: async () => [run()] } as never, engineForContext: () => Promise.reject(new Error("unused")), }, tui.context, ); assert.deepEqual(result, {}); assert.equal(tui.customCalls(), 3); }); test("dashboard duplicate labels retain their canonical run selection IDs", async () => { const workflows = [ run({ runId: "12345678-aaaa", featureName: "search-page" }), run({ runId: "12345678-bbbb", featureName: "search-page" }), ]; const tui = tuiContext(["12345678-bbbb", undefined, undefined]); await openWorkflowDashboard( pi, { store: { listRuns: async () => workflows } as never, engineForContext: () => Promise.reject(new Error("unused")), }, tui.context, ); const list = tui.renders[0]?.join("\n") ?? ""; assert.match(list, /search-page · 12345678-a/); assert.match(list, /search-page · 12345678-b/); assert.match(tui.renders[1]?.join("\n") ?? "", /search-page · 12345678-b/); }); test("dashboard disambiguates duplicate labels across the full run list", async () => { const workflows = Array.from({ length: 30 }, (_, index) => run({ runId: index === 0 ? "12345678-aaaa" : index === 29 ? "12345678-bbbb" : `run-${index}`, featureName: index === 0 || index === 29 ? "shared-label" : `feature-${index}`, }), ); const tui = tuiContext([undefined]); await openWorkflowDashboard( pi, { store: { listRuns: async () => workflows } as never, engineForContext: () => Promise.reject(new Error("unused")), }, tui.context, ); const rendered = tui.renders[0]?.join("\n") ?? ""; assert.match(rendered, /shared-label · 12345678-a/); assert.match(rendered, /1-12 of 30/); assert.doesNotMatch(rendered, /shared-label\s+WAITING/); }); test("historical parent sessions receive inspection-only dashboard actions", async () => { const workflow = run({ parentSessionIds: ["session-1", "session-2"], planArtifact: { path: "/state/plan.md", revision: 3, digest: "digest" }, }); const tui = tuiContext(["run-1", undefined, undefined], "session-2"); await openWorkflowDashboard( pi, { store: { listRuns: async () => [workflow] } as never, engineForContext: () => Promise.reject(new Error("unused")), }, tui.context, ); const actions = tui.renders[1]?.join("\n") ?? ""; assert.match(actions, /Review verified plan/); assert.doesNotMatch(actions, /Record Gate Decision/); assert.doesNotMatch(actions, /Continue context transition/); }); test("dashboard verifies and opens retained report content", async () => { const directory = await mkdtemp(path.join(tmpdir(), "workflow-dashboard-")); try { const reportPath = path.join(directory, "report.md"); const reportContents = "# Completed report\n\nVerified."; await writeFile(reportPath, reportContents, "utf8"); const workflow = run({ status: "completed", gates: [], reportArtifact: { path: reportPath, digest: createHash("sha256").update(reportContents).digest("hex"), revision: 12, }, }); const tui = tuiContext([ "run-1", "report", undefined, undefined, undefined, ]); await openWorkflowDashboard( pi, { store: { listRuns: async () => [workflow] } as never, engineForContext: () => Promise.reject(new Error("unused")), }, tui.context, ); assert.ok( tui.renders.some((lines) => lines.join("\n").includes("Completed report"), ), ); assert.ok(tui.renders.some((lines) => lines.join("\n").includes("Verified."))); } finally { await rm(directory, { recursive: true, force: true }); } }); test("retained artifact reader Escape returns through artifact and candidate selection", async () => { const candidate = prototypeCandidate(); const workflow = run({ prototypeCandidates: [candidate] }); const tui = tuiContext([ "run-1", "prototypes", candidate, candidate.artifactPath, undefined, undefined, undefined, undefined, undefined, ]); await openWorkflowDashboard( pi, { store: { listRuns: async () => [workflow] } as never, engineForContext: () => Promise.reject(new Error("unused")), }, tui.context, ); assert.ok( tui.renders.filter((lines) => lines.join("\n").includes("Retained artifact")) .length >= 2, ); assert.ok( tui.renders.filter((lines) => lines.join("\n").includes("Prototype evidence")) .length >= 2, ); }); test("dashboard verifies and opens Assignment transcripts", async () => { const directory = await mkdtemp(path.join(tmpdir(), "workflow-transcript-")); try { const transcriptPath = path.join(directory, "assignment.json"); const transcriptContents = '[{"role":"assistant","text":"verified"}]\n'; await writeFile(transcriptPath, transcriptContents, "utf8"); const workflow = run({ assignments: [ { id: "assignment-1", stageId: "review", role: "review", profileName: "independent-reviewer", profileDigest: "digest", selection: { eligibleProfiles: [], selectedProfileName: "independent-reviewer", rationale: "Review", }, status: "succeeded", mutates: false, ownedPaths: [], transcriptArtifact: { path: transcriptPath, digest: createHash("sha256") .update(transcriptContents) .digest("hex"), }, }, ], }); const assignment = workflow.assignments[0]; assert.ok(assignment); const tui = tuiContext([ "run-1", "transcripts", assignment, undefined, undefined, undefined, undefined, ]); await openWorkflowDashboard( pi, { store: { listRuns: async () => [workflow] } as never, engineForContext: () => Promise.reject(new Error("unused")), }, tui.context, ); assert.ok( tui.renders.some((lines) => lines.join("\n").includes('"text":"verified"'), ), ); assert.ok( tui.renders.filter((lines) => lines.join("\n").includes("Assignment transcripts"), ).length >= 2, ); } finally { await rm(directory, { recursive: true, force: true }); } }); test("RPC and print dashboards stay linear and never call custom", async () => { for (const mode of ["rpc", "print", "json"] as const) { const notifications: string[] = []; const ctx = { hasUI: mode === "rpc", mode, sessionManager: { getSessionId: () => "session-1" }, ui: { custom: async () => { throw new Error("custom must not be called outside TUI mode"); }, notify: (message: string) => notifications.push(message), }, } as unknown as ExtensionCommandContext; assert.deepEqual( await openWorkflowDashboard( pi, { store: { listRuns: async () => [run()] } as never, engineForContext: () => Promise.reject(new Error("unused")), }, ctx, ), {}, ); if (mode === "rpc") assert.match(notifications[0] ?? "", /search-page/); } }); test("dashboard reports the supplied run ID when direct inspection cannot resolve it", async () => { const tui = tuiContext([]); const result = await openWorkflowDashboard( pi, { store: { listRuns: async () => [run()] } as never, engineForContext: () => Promise.reject(new Error("unused")), }, tui.context, "missing-run-id", ); assert.deepEqual(result, {}); assert.deepEqual(tui.notifications, [ "Workflow Run missing-run-id was not found for this session.", ]); });