import assert from "node:assert/strict"; import test from "node:test"; import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; import { visibleWidth } from "@earendil-works/pi-tui"; import { DEFAULT_WORKFLOW_CONFIG, type WorkflowRun } from "./domain.ts"; import { projectWorkflowProgress, refreshWorkflowProgress, registerWorkflowProgress, renderWorkflowProgress, } from "./progress.ts"; function run(overrides: Partial = {}): WorkflowRun { return { runId: "run-1", revision: 3, mode: "feature", status: "waiting", background: false, request: "Build dashboard", featureName: "dashboard", parentSessionIds: ["session-1"], lease: { processId: 1, activeParentSessionId: "session-1", heartbeatAt: 10, }, workspace: { state: "ready", sourceRoot: "/repo" }, effectiveConfig: structuredClone(DEFAULT_WORKFLOW_CONFIG), pinnedProfiles: [], preflightEligibility: [], facts: { stage: "design", taskKind: "feature", requestTerms: [], candidatePaths: [], languages: [], tags: [], riskTags: [], }, stages: [ { id: "d", kind: "discover", status: "completed" }, { id: "p", kind: "design", status: "waiting" }, { id: "pt", kind: "prototype", status: "pending" }, { id: "i", kind: "implement", status: "pending" }, { id: "v", kind: "verify", status: "pending" }, { id: "r", kind: "review", status: "pending" }, ], gates: [ { id: "gate", kind: "plan-approval", status: "open", revision: 3, openedAt: 12, }, ], decisions: [], assignments: [], prototypeCandidates: [], prototypeCleanupCompleted: false, deliveries: [], commandReceipts: [], uiImpactEvidence: [], affectedUiSurfaces: [], mutatorProfileNames: [], reviewCycles: 0, verificationCycles: 0, ...overrides, }; } test("progress projects the newest attached active run", () => { const older = run({ runId: "older", featureName: "older", lease: { processId: 1, activeParentSessionId: "session-1", heartbeatAt: 5 } }); const newest = run({ runId: "newest", featureName: "newest", lease: { processId: 1, activeParentSessionId: "session-1", heartbeatAt: 20 } }); const terminal = run({ runId: "terminal", status: "completed", lease: undefined }); const historical = run({ runId: "historical", parentSessionIds: ["session-2"] }); const progress = projectWorkflowProgress([older, terminal, newest, historical], "session-1"); assert.deepEqual(progress, { runId: "newest", label: "newest", stage: "DESIGN", completed: 1, total: 6, gate: "Plan Approval", }); }); test("progress clears when no attached run remains active", () => { assert.equal( projectWorkflowProgress([run({ status: "failed", lease: undefined })], "session-1"), undefined, ); assert.equal(projectWorkflowProgress([run()], "session-2"), undefined); }); test("progress lifecycle refreshes from persistence and clears on terminal state and shutdown", async () => { let runs = [run()]; let widget: unknown; const handlers = new Map Promise>(); const pi = { on(event: string, handler: (event: unknown, ctx: ExtensionContext) => Promise) { handlers.set(event, handler); }, } as unknown as ExtensionAPI; const runtime = { store: { listRuns: async () => runs }, } as never; const ctx = { mode: "tui", sessionManager: { getSessionId: () => "session-1" }, ui: { setWidget(key: string, value: unknown) { assert.equal(key, "workflow-active-run"); widget = value; }, }, } as unknown as ExtensionContext; registerWorkflowProgress(pi, runtime); await handlers.get("session_start")?.({}, ctx); assert.equal(typeof widget, "function"); runs = [run({ status: "completed", lease: undefined })]; await refreshWorkflowProgress(runtime, ctx); assert.equal(widget, undefined); runs = [run()]; await refreshWorkflowProgress(runtime, ctx); assert.equal(typeof widget, "function"); await handlers.get("session_shutdown")?.({}, ctx); assert.equal(widget, undefined); }); test("progress rendering is one cell-width-safe line", () => { const progress = projectWorkflowProgress([run()], "session-1"); assert.ok(progress); const wide = renderWorkflowProgress(progress, 80); assert.match(wide, /workflow dashboard DESIGN 1\/6 Gate: Plan Approval/); const narrow = renderWorkflowProgress(progress, 28); assert.ok(visibleWidth(narrow) <= 28); assert.equal(narrow.includes("\n"), false); });