143 lines
4.6 KiB
TypeScript
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, "pi-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);
|
|
});
|