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 { visibleWidth } from "@earendil-works/pi-tui"; import { DEFAULT_WORKFLOW_CONFIG, type WorkflowRun, } from "./domain.ts"; import type { WorkflowCommand, WorkflowView } from "./engine.ts"; import { FocusedWorkflowDashboard, 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/pi-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, }; } const theme = { fg: (_color: string, text: string) => text, bold: (text: string) => text, }; function view(workflow: WorkflowRun): WorkflowView { return { runId: workflow.runId, 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, }; } test("focused dashboard renders compact and narrow cards within width", () => { let selected: string | null | undefined; const dashboard = new FocusedWorkflowDashboard( [run()], theme, (value) => { selected = value; }, ); dashboard.handleInput("\r"); const wide = dashboard.render(100); assert.ok(wide.some((line) => line.includes("Progress"))); assert.ok(wide.some((line) => line.includes("Worktree"))); assert.ok(wide.every((line) => visibleWidth(line) <= 100)); const narrow = dashboard.render(48); assert.equal(narrow.some((line) => line.includes("Worktree")), false); assert.ok(narrow.every((line) => visibleWidth(line) <= 48)); dashboard.handleInput("\r"); assert.equal(selected, "run-1"); }); test("dashboard windows long-lived session run lists", () => { const runs = Array.from({ length: 30 }, (_, index) => run({ runId: `run-${index}`, featureName: `feature-${index}` }), ); const dashboard = new FocusedWorkflowDashboard(runs, theme, () => {}); const lines = dashboard.render(100); assert.ok(lines.length <= 16); assert.ok(lines.some((line) => line.includes("of 30 runs"))); }); test("dashboard records explicit Plan Approval through the engine", async () => { const workflow = run({ planArtifact: { path: "/state/artifacts/plan.md", revision: 3, digest: "digest", }, }); const commands: WorkflowCommand[] = []; const selections = [ "Record Gate Decision", "Approve plan · UI impact minor", ]; const ctx = { hasUI: true, mode: "tui", sessionManager: { getSessionId: () => "session-1", }, ui: { custom: async () => "run-1", select: async () => selections.shift(), confirm: async () => true, notify: () => {}, }, } as unknown as ExtensionCommandContext; const pi = { getActiveTools: () => ["read"], } as unknown as ExtensionAPI; await openWorkflowDashboard( pi, { store: { listRuns: async () => [workflow] } as never, engineForContext: async () => ({ async execute(command: WorkflowCommand) { commands.push(structuredClone(command)); return view(workflow); }, }) as never, }, ctx, ); const decision = commands[0]; assert.equal(decision?.action, "decide"); assert.deepEqual( decision?.action === "decide" ? decision.decision : undefined, { kind: "approve-plan", approved: true, acceptedUiImpact: "minor", }, ); }); 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" }, }); let actions: string[] = []; const ctx = { hasUI: true, mode: "tui", sessionManager: { getSessionId: () => "session-2" }, ui: { custom: async () => "run-1", select: async (_title: string, choices: string[]) => { actions = choices; return undefined; }, notify: () => {}, }, } as unknown as ExtensionCommandContext; await openWorkflowDashboard( { getActiveTools: () => ["read"] } as unknown as ExtensionAPI, { store: { listRuns: async () => [workflow] } as never, engineForContext: () => Promise.reject(new Error("unused")), }, ctx, ); assert.equal(actions.includes("Record Gate Decision"), false); assert.equal(actions.includes("Continue context transition"), false); assert.equal(actions.includes("Open plan"), true); }); test("dashboard opens retained report content on demand", async () => { const directory = await mkdtemp(path.join(tmpdir(), "pi-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, }, }); let opened = ""; const ctx = { hasUI: true, mode: "tui", sessionManager: { getSessionId: () => "session-1" }, ui: { custom: async () => "run-1", select: async () => "Open report", editor: async (_title: string, contents: string) => { opened = contents; return contents; }, notify: () => {}, }, } as unknown as ExtensionCommandContext; await openWorkflowDashboard( { getActiveTools: () => ["read"] } as unknown as ExtensionAPI, { store: { listRuns: async () => [workflow] } as never, engineForContext: () => Promise.reject(new Error("unused")), }, ctx, ); assert.match(opened, /Completed report[\s\S]*Verified/); } finally { await rm(directory, { recursive: true, force: true }); } }); test("dashboard verifies and opens Assignment transcripts on demand", async () => { const directory = await mkdtemp(path.join(tmpdir(), "pi-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 selections = [ "Browse Assignment transcripts", "1. review/independent-reviewer · succeeded", ]; let opened = ""; const ctx = { hasUI: true, mode: "tui", sessionManager: { getSessionId: () => "session-1" }, ui: { custom: async () => "run-1", select: async () => selections.shift(), editor: async (_title: string, contents: string) => { opened = contents; return contents; }, notify: () => {}, }, } as unknown as ExtensionCommandContext; await openWorkflowDashboard( { getActiveTools: () => ["read"] } as unknown as ExtensionAPI, { store: { listRuns: async () => [workflow] } as never, engineForContext: () => Promise.reject(new Error("unused")), }, ctx, ); assert.match(opened, /assistant.*verified/); } finally { await rm(directory, { recursive: true, force: true }); } }); test("dashboard exposes continuation only for pending handoff intents", 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 ctx = { hasUI: true, mode: "tui", sessionManager: { getSessionId: () => "session-1" }, ui: { custom: async () => "run-1", select: async () => "Continue context transition", notify: () => {}, }, } as unknown as ExtensionCommandContext; const result = await openWorkflowDashboard( { getActiveTools: () => ["read"] } as unknown as ExtensionAPI, { store: { listRuns: async () => [workflow] } as never, engineForContext: () => Promise.reject(new Error("unused")), }, ctx, ); assert.deepEqual(result, { continueRunId: "run-1" }); });