Files

143 lines
4.6 KiB
TypeScript

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> = {}): 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<string, (event: unknown, ctx: ExtensionContext) => Promise<void>>();
const pi = {
on(event: string, handler: (event: unknown, ctx: ExtensionContext) => Promise<void>) {
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);
});