feat(pi-workflow): improve workflow usability and recovery
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user