feat(pi-workflow): improve workflow usability and recovery
This commit is contained in:
@@ -8,16 +8,12 @@ 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";
|
||||
import { openWorkflowDashboard } from "./dashboard.ts";
|
||||
|
||||
function run(overrides: Partial<WorkflowRun> = {}): WorkflowRun {
|
||||
return {
|
||||
@@ -86,14 +82,30 @@ function run(overrides: Partial<WorkflowRun> = {}): WorkflowRun {
|
||||
};
|
||||
}
|
||||
|
||||
const theme = {
|
||||
fg: (_color: string, text: string) => text,
|
||||
bold: (text: string) => text,
|
||||
};
|
||||
function prototypeCandidate() {
|
||||
return {
|
||||
id: "candidate-1",
|
||||
revision: 2,
|
||||
assignmentId: "prototype-assignment-1",
|
||||
profileName: "frontend-prototyper",
|
||||
status: "succeeded" as const,
|
||||
path: "/repo/prototypes/candidate-1",
|
||||
route: "/search?variant=1",
|
||||
structuralRationale: "Compact drill-down flow",
|
||||
responsiveChecks: ["mobile", "desktop"],
|
||||
browserEvidence: ["validated in browser"],
|
||||
launchInstructions: "Run the prototype server.",
|
||||
visualDigest: "visual-digest",
|
||||
artifactPath: "/state/artifacts/candidate-1.md",
|
||||
retainedArtifacts: ["/state/artifacts/candidate-1.png"],
|
||||
};
|
||||
}
|
||||
|
||||
function view(workflow: WorkflowRun): WorkflowView {
|
||||
return {
|
||||
runId: workflow.runId,
|
||||
featureName: workflow.featureName,
|
||||
request: workflow.request,
|
||||
revision: workflow.revision,
|
||||
status: workflow.status,
|
||||
background: workflow.background,
|
||||
@@ -113,76 +125,141 @@ function view(workflow: WorkflowRun): WorkflowView {
|
||||
};
|
||||
}
|
||||
|
||||
test("focused dashboard renders compact and narrow cards within width", () => {
|
||||
let selected: string | null | undefined;
|
||||
const dashboard = new FocusedWorkflowDashboard(
|
||||
[run()],
|
||||
theme,
|
||||
(value) => {
|
||||
selected = value;
|
||||
function tuiContext(results: unknown[], sessionId = "session-1") {
|
||||
const notifications: string[] = [];
|
||||
const renders: string[][] = [];
|
||||
let customCalls = 0;
|
||||
const theme = {
|
||||
fg: (_color: string, text: string) => text,
|
||||
bg: (_color: string, text: string) => text,
|
||||
bold: (text: string) => text,
|
||||
};
|
||||
const unsupported = () => {
|
||||
throw new Error("built-in bounded dialog must not be called");
|
||||
};
|
||||
const context = {
|
||||
hasUI: true,
|
||||
mode: "tui",
|
||||
sessionManager: {
|
||||
getSessionId: () => sessionId,
|
||||
},
|
||||
);
|
||||
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");
|
||||
});
|
||||
ui: {
|
||||
custom: async (
|
||||
factory: (
|
||||
tui: { requestRender(): void; terminal: { columns: number; rows: number } },
|
||||
overlayTheme: typeof theme,
|
||||
keybindings: object,
|
||||
done: (value: unknown) => void,
|
||||
) => { render(width: number): string[] },
|
||||
) => {
|
||||
customCalls++;
|
||||
const component = factory(
|
||||
{
|
||||
requestRender() {},
|
||||
terminal: { columns: 100, rows: 24 },
|
||||
},
|
||||
theme,
|
||||
{},
|
||||
() => {},
|
||||
);
|
||||
renders.push(component.render(100));
|
||||
return results.shift();
|
||||
},
|
||||
select: unsupported,
|
||||
confirm: unsupported,
|
||||
input: unsupported,
|
||||
editor: unsupported,
|
||||
notify: (message: string) => notifications.push(message),
|
||||
},
|
||||
} as unknown as ExtensionCommandContext;
|
||||
return { context, notifications, renders, customCalls: () => customCalls };
|
||||
}
|
||||
|
||||
test("dashboard disambiguates duplicate feature labels without changing selection IDs", () => {
|
||||
let selected: string | null | undefined;
|
||||
const dashboard = new FocusedWorkflowDashboard(
|
||||
[
|
||||
run({ runId: "12345678-aaaa", featureName: "search-page" }),
|
||||
run({ runId: "12345678-bbbb", featureName: "search-page" }),
|
||||
function interactiveTuiContext(inputs: string[][], sessionId = "session-1") {
|
||||
let customCalls = 0;
|
||||
const theme = {
|
||||
fg: (_color: string, text: string) => text,
|
||||
bg: (_color: string, text: string) => text,
|
||||
bold: (text: string) => text,
|
||||
};
|
||||
const context = {
|
||||
hasUI: true,
|
||||
mode: "tui",
|
||||
sessionManager: { getSessionId: () => sessionId },
|
||||
ui: {
|
||||
async custom(
|
||||
factory: (
|
||||
tui: { requestRender(): void; terminal: { columns: number; rows: number } },
|
||||
overlayTheme: typeof theme,
|
||||
keybindings: object,
|
||||
done: (value: unknown) => void,
|
||||
) => {
|
||||
render(width: number): string[];
|
||||
handleInput?(data: string): void;
|
||||
},
|
||||
) {
|
||||
customCalls++;
|
||||
let completed: unknown;
|
||||
const component = factory(
|
||||
{
|
||||
requestRender() {},
|
||||
terminal: { columns: 80, rows: 16 },
|
||||
},
|
||||
theme,
|
||||
{},
|
||||
(value) => { completed = value; },
|
||||
);
|
||||
component.render(56);
|
||||
for (const input of inputs.shift() ?? []) component.handleInput?.(input);
|
||||
return completed;
|
||||
},
|
||||
notify() {},
|
||||
},
|
||||
} as unknown as ExtensionCommandContext;
|
||||
return { context, customCalls: () => customCalls };
|
||||
}
|
||||
|
||||
const pi = {
|
||||
getActiveTools: () => ["read"],
|
||||
} as unknown as ExtensionAPI;
|
||||
|
||||
test("dashboard fresh-session action returns an immediate continuation result", 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,
|
||||
},
|
||||
],
|
||||
theme,
|
||||
(value) => { selected = value; },
|
||||
implementationContext: {
|
||||
gateId: "context-gate",
|
||||
runRevision: 12,
|
||||
planRevision: 3,
|
||||
preparationId: "prep-1",
|
||||
activeParentSessionId: "session-1",
|
||||
strategy: "fresh-session",
|
||||
status: "handoff-pending",
|
||||
method: "fresh-session",
|
||||
},
|
||||
});
|
||||
const tui = tuiContext(["run-1", "continue"]);
|
||||
const result = await openWorkflowDashboard(
|
||||
pi,
|
||||
{
|
||||
store: { listRuns: async () => [workflow] } as never,
|
||||
engineForContext: () => Promise.reject(new Error("unused")),
|
||||
},
|
||||
tui.context,
|
||||
);
|
||||
const lines = dashboard.render(100);
|
||||
assert.ok(lines.some((line) => line.includes("search-page · 12345678-a")));
|
||||
assert.ok(lines.some((line) => line.includes("search-page · 12345678-b")));
|
||||
dashboard.handleInput("\r");
|
||||
dashboard.handleInput("\r");
|
||||
assert.equal(selected, "12345678-aaaa");
|
||||
assert.deepEqual(result, { continueRunId: "run-1" });
|
||||
assert.equal(tui.customCalls(), 2);
|
||||
});
|
||||
|
||||
test("dashboard disambiguates collisions outside its visible window at common widths", () => {
|
||||
const runs = Array.from({ length: 30 }, (_, index) =>
|
||||
run({
|
||||
runId:
|
||||
index === 0
|
||||
? "12345678-aaaa"
|
||||
: index === 29
|
||||
? "12345678-bbbb"
|
||||
: `run-${index}`,
|
||||
featureName: index === 0 || index === 29 ? "shared-label" : `feature-${index}`,
|
||||
}),
|
||||
);
|
||||
const dashboard = new FocusedWorkflowDashboard(runs, theme, () => {});
|
||||
for (const width of [80, 60]) {
|
||||
const lines = dashboard.render(width);
|
||||
assert.ok(lines.some((line) => line.includes("shared-label · 12345678-a")));
|
||||
assert.ok(lines.every((line) => visibleWidth(line) <= width));
|
||||
}
|
||||
});
|
||||
|
||||
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 () => {
|
||||
test("dashboard records Plan Approval using only custom TUI overlays", async () => {
|
||||
const workflow = run({
|
||||
planArtifact: {
|
||||
path: "/state/artifacts/plan.md",
|
||||
@@ -191,26 +268,14 @@ test("dashboard records explicit Plan Approval through the engine", async () =>
|
||||
},
|
||||
});
|
||||
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;
|
||||
const tui = tuiContext([
|
||||
"run-1",
|
||||
"decide",
|
||||
"approve",
|
||||
true,
|
||||
undefined,
|
||||
undefined,
|
||||
]);
|
||||
await openWorkflowDashboard(
|
||||
pi,
|
||||
{
|
||||
@@ -223,7 +288,7 @@ test("dashboard records explicit Plan Approval through the engine", async () =>
|
||||
},
|
||||
}) as never,
|
||||
},
|
||||
ctx,
|
||||
tui.context,
|
||||
);
|
||||
const decision = commands[0];
|
||||
assert.equal(decision?.action, "decide");
|
||||
@@ -235,6 +300,188 @@ test("dashboard records explicit Plan Approval through the engine", async () =>
|
||||
acceptedUiImpact: "minor",
|
||||
},
|
||||
);
|
||||
assert.ok(tui.customCalls() >= 4);
|
||||
});
|
||||
|
||||
test("prototype reader Escape returns to candidate actions without accepting", async () => {
|
||||
const candidate = prototypeCandidate();
|
||||
const workflow = run({
|
||||
stages: run().stages.map((stage) =>
|
||||
stage.kind === "prototype"
|
||||
? { ...stage, status: "waiting" as const }
|
||||
: stage,
|
||||
),
|
||||
gates: [
|
||||
{
|
||||
id: "prototype-gate",
|
||||
kind: "prototype-acceptance",
|
||||
status: "open",
|
||||
revision: 12,
|
||||
openedAt: 1,
|
||||
},
|
||||
],
|
||||
prototypeCandidates: [candidate],
|
||||
});
|
||||
const commands: WorkflowCommand[] = [];
|
||||
const tui = tuiContext([
|
||||
"run-1",
|
||||
"decide",
|
||||
{ kind: "candidate", candidate },
|
||||
"review",
|
||||
undefined,
|
||||
"back",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
]);
|
||||
await openWorkflowDashboard(
|
||||
pi,
|
||||
{
|
||||
store: { listRuns: async () => [workflow] } as never,
|
||||
engineForContext: async () =>
|
||||
({
|
||||
async execute(command: WorkflowCommand) {
|
||||
commands.push(structuredClone(command));
|
||||
return view(workflow);
|
||||
},
|
||||
}) as never,
|
||||
},
|
||||
tui.context,
|
||||
);
|
||||
assert.deepEqual(commands, []);
|
||||
assert.ok(
|
||||
tui.renders.filter((lines) => lines.join("\n").includes("Prototype 2"))
|
||||
.length >= 2,
|
||||
);
|
||||
});
|
||||
|
||||
test("prototype acceptance requires the explicit candidate Accept action", async () => {
|
||||
const candidate = prototypeCandidate();
|
||||
const workflow = run({
|
||||
gates: [
|
||||
{
|
||||
id: "prototype-gate",
|
||||
kind: "prototype-acceptance",
|
||||
status: "open",
|
||||
revision: 12,
|
||||
openedAt: 1,
|
||||
},
|
||||
],
|
||||
prototypeCandidates: [candidate],
|
||||
});
|
||||
const commands: WorkflowCommand[] = [];
|
||||
const tui = tuiContext([
|
||||
"run-1",
|
||||
"decide",
|
||||
{ kind: "candidate", candidate },
|
||||
"review",
|
||||
undefined,
|
||||
"accept",
|
||||
true,
|
||||
"",
|
||||
undefined,
|
||||
undefined,
|
||||
]);
|
||||
await openWorkflowDashboard(
|
||||
pi,
|
||||
{
|
||||
store: { listRuns: async () => [workflow] } as never,
|
||||
engineForContext: async () =>
|
||||
({
|
||||
async execute(command: WorkflowCommand) {
|
||||
commands.push(structuredClone(command));
|
||||
return view(workflow);
|
||||
},
|
||||
}) as never,
|
||||
},
|
||||
tui.context,
|
||||
);
|
||||
const decision = commands[0];
|
||||
assert.deepEqual(
|
||||
decision?.action === "decide" ? decision.decision : undefined,
|
||||
{
|
||||
kind: "accept-prototype",
|
||||
candidateId: candidate.id,
|
||||
candidateRevision: candidate.revision,
|
||||
browserReviewed: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("Escape from run detail returns one level to the run list", async () => {
|
||||
const tui = tuiContext(["run-1", undefined, undefined]);
|
||||
const result = await openWorkflowDashboard(
|
||||
pi,
|
||||
{
|
||||
store: { listRuns: async () => [run()] } as never,
|
||||
engineForContext: () => Promise.reject(new Error("unused")),
|
||||
},
|
||||
tui.context,
|
||||
);
|
||||
assert.deepEqual(result, {});
|
||||
assert.equal(tui.customCalls(), 3);
|
||||
});
|
||||
|
||||
test("dashboard handleInput drives selection and Escape back-stack navigation", async () => {
|
||||
const tui = interactiveTuiContext([["\r"], ["\x1b"], ["\x1b"]]);
|
||||
const result = await openWorkflowDashboard(
|
||||
pi,
|
||||
{
|
||||
store: { listRuns: async () => [run()] } as never,
|
||||
engineForContext: () => Promise.reject(new Error("unused")),
|
||||
},
|
||||
tui.context,
|
||||
);
|
||||
assert.deepEqual(result, {});
|
||||
assert.equal(tui.customCalls(), 3);
|
||||
});
|
||||
|
||||
test("dashboard duplicate labels retain their canonical run selection IDs", async () => {
|
||||
const workflows = [
|
||||
run({ runId: "12345678-aaaa", featureName: "search-page" }),
|
||||
run({ runId: "12345678-bbbb", featureName: "search-page" }),
|
||||
];
|
||||
const tui = tuiContext(["12345678-bbbb", undefined, undefined]);
|
||||
await openWorkflowDashboard(
|
||||
pi,
|
||||
{
|
||||
store: { listRuns: async () => workflows } as never,
|
||||
engineForContext: () => Promise.reject(new Error("unused")),
|
||||
},
|
||||
tui.context,
|
||||
);
|
||||
const list = tui.renders[0]?.join("\n") ?? "";
|
||||
assert.match(list, /search-page · 12345678-a/);
|
||||
assert.match(list, /search-page · 12345678-b/);
|
||||
assert.match(tui.renders[1]?.join("\n") ?? "", /search-page · 12345678-b/);
|
||||
});
|
||||
|
||||
test("dashboard disambiguates duplicate labels across the full run list", async () => {
|
||||
const workflows = Array.from({ length: 30 }, (_, index) =>
|
||||
run({
|
||||
runId:
|
||||
index === 0
|
||||
? "12345678-aaaa"
|
||||
: index === 29
|
||||
? "12345678-bbbb"
|
||||
: `run-${index}`,
|
||||
featureName:
|
||||
index === 0 || index === 29 ? "shared-label" : `feature-${index}`,
|
||||
}),
|
||||
);
|
||||
const tui = tuiContext([undefined]);
|
||||
await openWorkflowDashboard(
|
||||
pi,
|
||||
{
|
||||
store: { listRuns: async () => workflows } as never,
|
||||
engineForContext: () => Promise.reject(new Error("unused")),
|
||||
},
|
||||
tui.context,
|
||||
);
|
||||
const rendered = tui.renders[0]?.join("\n") ?? "";
|
||||
assert.match(rendered, /shared-label · 12345678-a/);
|
||||
assert.match(rendered, /1-12 of 30/);
|
||||
assert.doesNotMatch(rendered, /shared-label\s+WAITING/);
|
||||
});
|
||||
|
||||
test("historical parent sessions receive inspection-only dashboard actions", async () => {
|
||||
@@ -242,34 +489,22 @@ test("historical parent sessions receive inspection-only dashboard actions", asy
|
||||
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;
|
||||
const tui = tuiContext(["run-1", undefined, undefined], "session-2");
|
||||
await openWorkflowDashboard(
|
||||
{ getActiveTools: () => ["read"] } as unknown as ExtensionAPI,
|
||||
pi,
|
||||
{
|
||||
store: { listRuns: async () => [workflow] } as never,
|
||||
engineForContext: () => Promise.reject(new Error("unused")),
|
||||
},
|
||||
ctx,
|
||||
tui.context,
|
||||
);
|
||||
assert.equal(actions.includes("Record Gate Decision"), false);
|
||||
assert.equal(actions.includes("Continue context transition"), false);
|
||||
assert.equal(actions.includes("Open plan"), true);
|
||||
const actions = tui.renders[1]?.join("\n") ?? "";
|
||||
assert.match(actions, /Review verified plan/);
|
||||
assert.doesNotMatch(actions, /Record Gate Decision/);
|
||||
assert.doesNotMatch(actions, /Continue context transition/);
|
||||
});
|
||||
|
||||
test("dashboard opens retained report content on demand", async () => {
|
||||
test("dashboard verifies and opens retained report content", async () => {
|
||||
const directory = await mkdtemp(path.join(tmpdir(), "pi-workflow-dashboard-"));
|
||||
try {
|
||||
const reportPath = path.join(directory, "report.md");
|
||||
@@ -284,36 +519,65 @@ test("dashboard opens retained report content on demand", async () => {
|
||||
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;
|
||||
const tui = tuiContext([
|
||||
"run-1",
|
||||
"report",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
]);
|
||||
await openWorkflowDashboard(
|
||||
{ getActiveTools: () => ["read"] } as unknown as ExtensionAPI,
|
||||
pi,
|
||||
{
|
||||
store: { listRuns: async () => [workflow] } as never,
|
||||
engineForContext: () => Promise.reject(new Error("unused")),
|
||||
},
|
||||
ctx,
|
||||
tui.context,
|
||||
);
|
||||
assert.match(opened, /Completed report[\s\S]*Verified/);
|
||||
assert.ok(
|
||||
tui.renders.some((lines) =>
|
||||
lines.join("\n").includes("Completed report"),
|
||||
),
|
||||
);
|
||||
assert.ok(tui.renders.some((lines) => lines.join("\n").includes("Verified.")));
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("dashboard verifies and opens Assignment transcripts on demand", async () => {
|
||||
test("retained artifact reader Escape returns through artifact and candidate selection", async () => {
|
||||
const candidate = prototypeCandidate();
|
||||
const workflow = run({ prototypeCandidates: [candidate] });
|
||||
const tui = tuiContext([
|
||||
"run-1",
|
||||
"prototypes",
|
||||
candidate,
|
||||
candidate.artifactPath,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
]);
|
||||
await openWorkflowDashboard(
|
||||
pi,
|
||||
{
|
||||
store: { listRuns: async () => [workflow] } as never,
|
||||
engineForContext: () => Promise.reject(new Error("unused")),
|
||||
},
|
||||
tui.context,
|
||||
);
|
||||
assert.ok(
|
||||
tui.renders.filter((lines) => lines.join("\n").includes("Retained artifact"))
|
||||
.length >= 2,
|
||||
);
|
||||
assert.ok(
|
||||
tui.renders.filter((lines) => lines.join("\n").includes("Prototype evidence"))
|
||||
.length >= 2,
|
||||
);
|
||||
});
|
||||
|
||||
test("dashboard verifies and opens Assignment transcripts", async () => {
|
||||
const directory = await mkdtemp(path.join(tmpdir(), "pi-workflow-transcript-"));
|
||||
try {
|
||||
const transcriptPath = path.join(directory, "assignment.json");
|
||||
@@ -344,107 +608,82 @@ test("dashboard verifies and opens Assignment transcripts on demand", async () =
|
||||
},
|
||||
],
|
||||
});
|
||||
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;
|
||||
const assignment = workflow.assignments[0];
|
||||
assert.ok(assignment);
|
||||
const tui = tuiContext([
|
||||
"run-1",
|
||||
"transcripts",
|
||||
assignment,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
]);
|
||||
await openWorkflowDashboard(
|
||||
{ getActiveTools: () => ["read"] } as unknown as ExtensionAPI,
|
||||
pi,
|
||||
{
|
||||
store: { listRuns: async () => [workflow] } as never,
|
||||
engineForContext: () => Promise.reject(new Error("unused")),
|
||||
},
|
||||
ctx,
|
||||
tui.context,
|
||||
);
|
||||
assert.ok(
|
||||
tui.renders.some((lines) =>
|
||||
lines.join("\n").includes('"text":"verified"'),
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
tui.renders.filter((lines) =>
|
||||
lines.join("\n").includes("Assignment transcripts"),
|
||||
).length >= 2,
|
||||
);
|
||||
assert.match(opened, /assistant.*verified/);
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("dashboard reports the supplied run ID when direct inspection cannot resolve it", async () => {
|
||||
const notifications: string[] = [];
|
||||
const ctx = {
|
||||
hasUI: true,
|
||||
mode: "tui",
|
||||
sessionManager: { getSessionId: () => "session-1" },
|
||||
ui: {
|
||||
custom: async () => {
|
||||
throw new Error("missing runs must not open the dashboard");
|
||||
test("RPC and print dashboards stay linear and never call custom", async () => {
|
||||
for (const mode of ["rpc", "print", "json"] as const) {
|
||||
const notifications: string[] = [];
|
||||
const ctx = {
|
||||
hasUI: mode === "rpc",
|
||||
mode,
|
||||
sessionManager: { getSessionId: () => "session-1" },
|
||||
ui: {
|
||||
custom: async () => {
|
||||
throw new Error("custom must not be called outside TUI mode");
|
||||
},
|
||||
notify: (message: string) => notifications.push(message),
|
||||
},
|
||||
notify: (message: string) => notifications.push(message),
|
||||
},
|
||||
} as unknown as ExtensionCommandContext;
|
||||
} as unknown as ExtensionCommandContext;
|
||||
assert.deepEqual(
|
||||
await openWorkflowDashboard(
|
||||
pi,
|
||||
{
|
||||
store: { listRuns: async () => [run()] } as never,
|
||||
engineForContext: () => Promise.reject(new Error("unused")),
|
||||
},
|
||||
ctx,
|
||||
),
|
||||
{},
|
||||
);
|
||||
if (mode === "rpc") assert.match(notifications[0] ?? "", /search-page/);
|
||||
}
|
||||
});
|
||||
|
||||
test("dashboard reports the supplied run ID when direct inspection cannot resolve it", async () => {
|
||||
const tui = tuiContext([]);
|
||||
const result = await openWorkflowDashboard(
|
||||
{ getActiveTools: () => ["read"] } as unknown as ExtensionAPI,
|
||||
pi,
|
||||
{
|
||||
store: { listRuns: async () => [run()] } as never,
|
||||
engineForContext: () => Promise.reject(new Error("unused")),
|
||||
},
|
||||
ctx,
|
||||
tui.context,
|
||||
"missing-run-id",
|
||||
);
|
||||
assert.deepEqual(result, {});
|
||||
assert.deepEqual(notifications, [
|
||||
assert.deepEqual(tui.notifications, [
|
||||
"Workflow Run missing-run-id was not found for this session.",
|
||||
]);
|
||||
});
|
||||
|
||||
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" });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user