fix: make presentation workflow graph factual

This commit is contained in:
lda
2026-07-09 22:12:03 +07:00 Verified
parent f3132a12ed
commit 77853ac9f9
5 changed files with 92 additions and 33 deletions
@@ -132,7 +132,8 @@ describe("DemoWorkflowScene", () => {
it("passes run proof into full graph beats", () => { it("passes run proof into full graph beats", () => {
const { unmount } = renderBeat("graph"); const { unmount } = renderBeat("graph");
expect(screen.getByLabelText("workflow graph proof")).toHaveTextContent("run_recorded_lda_report"); expect(screen.getByLabelText("workflow graph proof")).toHaveTextContent("run_recorded_lda_report");
expect(screen.getByLabelText("workflow graph proof")).toHaveTextContent("9 workflow nodes"); expect(screen.getByLabelText("workflow graph proof")).toHaveTextContent("11 plan nodes");
expect(screen.getByLabelText("workflow graph proof")).toHaveTextContent("3 trace frames");
unmount(); unmount();
renderBeat("output", "resume-output-evidence"); renderBeat("output", "resume-output-evidence");
@@ -73,7 +73,8 @@ export const DemoWorkflowScene = ({
const runProof = { const runProof = {
runId: runStart?.resultingIds.runId ?? null, runId: runStart?.resultingIds.runId ?? null,
traceLabel: "9 workflow nodes", planLabel: "11 plan nodes",
traceLabel: "3 trace frames",
evidenceLabel: "JSON-RPC evidence", evidenceLabel: "JSON-RPC evidence",
}; };
@@ -20,11 +20,55 @@ describe("WorkflowGraphStage", () => {
expect(selectNode).toHaveBeenCalledWith("review_issues"); expect(selectNode).toHaveBeenCalledWith("review_issues");
}); });
it("renders the prepared report workflow plan nodes", () => {
render(
<WorkflowGraphStage
execution={{ completedNodeIds: [], currentNodeId: "read_docs" }}
selectedNodeId={null}
selectNode={vi.fn()}
/>,
);
const graph = screen.getByRole("group", { name: /workflow graph/i });
expect(graph).toHaveTextContent("Read docs");
expect(graph).toHaveTextContent("Reset board");
expect(graph).toHaveTextContent("Analyze");
expect(graph).toHaveTextContent("Build report");
expect(graph).toHaveTextContent("Draft issues");
expect(graph).toHaveTextContent("Issue review");
expect(graph).toHaveTextContent("Create issues");
expect(graph).toHaveTextContent("Finalise");
expect(graph).toHaveTextContent("Revision requested");
expect(graph).toHaveTextContent("Completed");
expect(graph).toHaveTextContent("Cancelled");
expect(screen.getAllByRole("button", { name: /queued|current|completed|interrupt/i })).toHaveLength(11);
});
it("labels graph proof as plan nodes and trace frames separately", () => {
render(
<WorkflowGraphStage
execution={{ completedNodeIds: ["read_docs"], currentNodeId: "analyze" }}
selectedNodeId={null}
selectNode={vi.fn()}
proof={{
runId: "run_recorded_lda_report",
planLabel: "11 plan nodes",
traceLabel: "3 trace frames",
evidenceLabel: "JSON-RPC evidence",
}}
/>,
);
const proof = screen.getByLabelText("workflow graph proof");
expect(proof).toHaveTextContent("11 plan nodes");
expect(proof).toHaveTextContent("3 trace frames");
});
it("distinguishes completed, current interrupt, and future nodes semantically", () => { it("distinguishes completed, current interrupt, and future nodes semantically", () => {
render( render(
<WorkflowGraphStage <WorkflowGraphStage
execution={{ execution={{
completedNodeIds: ["read_docs", "build_report"], completedNodeIds: ["read_docs", "reset_board", "analyze", "build_report", "draft_issues"],
currentNodeId: "review_issues", currentNodeId: "review_issues",
}} }}
selectedNodeId={null} selectedNodeId={null}
@@ -32,65 +76,64 @@ describe("WorkflowGraphStage", () => {
/>, />,
); );
const readDocs = screen.getByRole("button", { name: /read documents/i }); const readDocs = screen.getByRole("button", { name: /read docs/i });
const buildReport = screen.getByRole("button", { name: /build report/i });
const reviewIssues = screen.getByRole("button", { name: /issue review/i }); const reviewIssues = screen.getByRole("button", { name: /issue review/i });
const createIssues = screen.getByRole("button", { name: /create issues/i }); const revisionReq = screen.getByRole("button", { name: /revision requested/i });
expect(readDocs).toHaveAttribute("data-execution-state", "completed"); expect(readDocs).toHaveAttribute("data-execution-state", "completed");
expect(buildReport).toHaveAttribute("data-execution-state", "completed");
expect(reviewIssues).toHaveAttribute("data-execution-state", "current"); expect(reviewIssues).toHaveAttribute("data-execution-state", "current");
expect(reviewIssues).toHaveAttribute("data-current-interrupt", "true"); expect(reviewIssues).toHaveAttribute("data-current-interrupt", "true");
expect(reviewIssues).toHaveTextContent("Current interrupt"); expect(reviewIssues).toHaveTextContent("Current interrupt");
expect(createIssues).toHaveAttribute("data-execution-state", "future"); expect(revisionReq).toHaveAttribute("data-execution-state", "future");
}); });
it("renders connectors between nodes", () => { it("renders connectors between nodes", () => {
render( render(
<WorkflowGraphStage <WorkflowGraphStage
execution={{ completedNodeIds: ["read_docs"], currentNodeId: "build_report" }} execution={{ completedNodeIds: ["read_docs"], currentNodeId: "reset_board" }}
selectedNodeId={null} selectedNodeId={null}
selectNode={vi.fn()} selectNode={vi.fn()}
/>, />,
); );
const connectors = screen.getAllByTestId("workflow-connector"); const connectors = screen.getAllByTestId("workflow-connector");
expect(connectors).toHaveLength(4); expect(connectors).toHaveLength(10);
expect(connectors.filter((connector) => connector.dataset.active === "true")).toHaveLength(1); expect(connectors.filter((connector) => connector.dataset.active === "true")).toHaveLength(1);
}); });
it("keeps all graph nodes inside the visible percentage frame", () => { it("keeps all graph nodes inside the visible percentage frame", () => {
for (const node of presentationNodes) { for (const node of presentationNodes) {
expect(node.x).toBeGreaterThanOrEqual(14); expect(node.x).toBeGreaterThanOrEqual(8);
expect(node.x).toBeLessThanOrEqual(86); expect(node.x).toBeLessThanOrEqual(92);
expect(node.y).toBeGreaterThanOrEqual(28); expect(node.y).toBeGreaterThanOrEqual(34);
expect(node.y).toBeLessThanOrEqual(72); expect(node.y).toBeLessThanOrEqual(78);
} }
}); });
it("renders compact run proof inside the graph", () => { it("renders compact run proof inside the graph", () => {
render( render(
<WorkflowGraphStage <WorkflowGraphStage
execution={{ completedNodeIds: ["read_docs"], currentNodeId: "build_report" }} execution={{ completedNodeIds: ["read_docs"], currentNodeId: "reset_board" }}
selectedNodeId={null} selectedNodeId={null}
selectNode={vi.fn()} selectNode={vi.fn()}
proof={{ runId: "run_recorded_lda_report", traceLabel: "5 nodes", evidenceLabel: "JSON-RPC captured" }} proof={{ runId: "run_recorded_lda_report", planLabel: "11 plan nodes", traceLabel: "3 trace frames", evidenceLabel: "JSON-RPC captured" }}
/>, />,
); );
expect(screen.getByLabelText("workflow graph proof")).toHaveTextContent("run_recorded_lda_report"); expect(screen.getByLabelText("workflow graph proof")).toHaveTextContent("run_recorded_lda_report");
expect(screen.getByLabelText("workflow graph proof")).toHaveTextContent("5 nodes"); expect(screen.getByLabelText("workflow graph proof")).toHaveTextContent("11 plan nodes");
expect(screen.getByLabelText("workflow graph proof")).toHaveTextContent("3 trace frames");
expect(screen.getByLabelText("workflow graph proof")).toHaveTextContent("JSON-RPC captured"); expect(screen.getByLabelText("workflow graph proof")).toHaveTextContent("JSON-RPC captured");
}); });
it("marks compact graph mode and suppresses proof chips", () => { it("marks compact graph mode and suppresses proof chips", () => {
render( render(
<WorkflowGraphStage <WorkflowGraphStage
execution={{ completedNodeIds: ["read_docs", "build_report"], currentNodeId: "review_issues" }} execution={{ completedNodeIds: ["read_docs", "reset_board"], currentNodeId: "analyze" }}
selectedNodeId={null} selectedNodeId={null}
selectNode={vi.fn()} selectNode={vi.fn()}
variant="compact" variant="compact"
proof={{ runId: "run_recorded_lda_report", traceLabel: "9 workflow nodes", evidenceLabel: "JSON-RPC evidence" }} proof={{ runId: "run_recorded_lda_report", planLabel: "11 plan nodes", traceLabel: "3 trace frames", evidenceLabel: "JSON-RPC evidence" }}
/>, />,
); );
@@ -101,7 +144,7 @@ describe("WorkflowGraphStage", () => {
it("keeps full graph mode as the default", () => { it("keeps full graph mode as the default", () => {
render( render(
<WorkflowGraphStage <WorkflowGraphStage
execution={{ completedNodeIds: ["read_docs"], currentNodeId: "build_report" }} execution={{ completedNodeIds: ["read_docs"], currentNodeId: "reset_board" }}
selectedNodeId={null} selectedNodeId={null}
selectNode={vi.fn()} selectNode={vi.fn()}
/>, />,
@@ -116,7 +159,7 @@ describe("WorkflowGraphStage", () => {
execution={{ completedNodeIds: [], currentNodeId: null }} execution={{ completedNodeIds: [], currentNodeId: null }}
selectedNodeId={null} selectedNodeId={null}
selectNode={vi.fn()} selectNode={vi.fn()}
proof={{ runId: null, traceLabel: "trace label", evidenceLabel: "evidence label" }} proof={{ runId: null, planLabel: "11 plan nodes", traceLabel: "trace label", evidenceLabel: "evidence label" }}
/>, />,
); );
@@ -12,26 +12,39 @@ export type PresentationNode = {
}; };
export const presentationNodes: ReadonlyArray<PresentationNode> = [ export const presentationNodes: ReadonlyArray<PresentationNode> = [
{ id: "read_docs", label: "Read documents", detail: "5 selected", kind: "node", x: 14, y: 58 }, { id: "read_docs", label: "Read docs", detail: "document source", kind: "node", x: 8, y: 54 },
{ id: "build_report", label: "Build report", detail: "Markdown", kind: "node", x: 34, y: 36 }, { id: "reset_board", label: "Reset board", detail: "issue board", kind: "node", x: 20, y: 34 },
{ id: "review_issues", label: "Issue review", detail: "Typed interrupt", kind: "interrupt", x: 52, y: 58 }, { id: "analyze", label: "Analyze", detail: "report source", kind: "node", x: 32, y: 54 },
{ id: "create_issues", label: "Create issues", detail: "Selected only", kind: "node", x: 70, y: 36 }, { id: "build_report", label: "Build report", detail: "markdown", kind: "node", x: 44, y: 34 },
{ id: "end_completed", label: "Completed", detail: "Persisted run", kind: "end", x: 86, y: 58 }, { id: "draft_issues", label: "Draft issues", detail: "proposals", kind: "node", x: 56, y: 54 },
{ id: "review_issues", label: "Issue review", detail: "typed interrupt", kind: "interrupt", x: 68, y: 34 },
{ id: "create_issues", label: "Create issues", detail: "selected only", kind: "node", x: 80, y: 54 },
{ id: "finalise", label: "Finalise", detail: "state output", kind: "node", x: 92, y: 34 },
{ id: "revision_requested", label: "Revision requested", detail: "operator branch", kind: "end", x: 68, y: 78 },
{ id: "end_completed", label: "Completed", detail: "persisted run", kind: "end", x: 92, y: 72 },
{ id: "end_cancelled", label: "Cancelled", detail: "no submitted output", kind: "end", x: 80, y: 78 },
]; ];
type PresentationEdge = readonly [from: string, to: string]; type PresentationEdge = readonly [from: string, to: string];
const presentationEdges: ReadonlyArray<PresentationEdge> = [ const presentationEdges: ReadonlyArray<PresentationEdge> = [
["read_docs", "build_report"], ["read_docs", "reset_board"],
["build_report", "review_issues"], ["reset_board", "analyze"],
["analyze", "build_report"],
["build_report", "draft_issues"],
["draft_issues", "review_issues"],
["review_issues", "create_issues"], ["review_issues", "create_issues"],
["create_issues", "end_completed"], ["review_issues", "revision_requested"],
["review_issues", "end_cancelled"],
["create_issues", "finalise"],
["finalise", "end_completed"],
]; ];
type NodeExecutionState = "completed" | "current" | "future"; type NodeExecutionState = "completed" | "current" | "future";
export type WorkflowGraphProof = { export type WorkflowGraphProof = {
readonly runId: string | null; readonly runId: string | null;
readonly planLabel: string;
readonly traceLabel: string; readonly traceLabel: string;
readonly evidenceLabel: string; readonly evidenceLabel: string;
}; };
@@ -83,6 +96,7 @@ export const WorkflowGraphStage = ({
{variant === "full" && proof && ( {variant === "full" && proof && (
<div className="workflow-graph-stage__proof" aria-label="workflow graph proof"> <div className="workflow-graph-stage__proof" aria-label="workflow graph proof">
<span><b>Run</b><code>{proof.runId ?? "run unavailable"}</code></span> <span><b>Run</b><code>{proof.runId ?? "run unavailable"}</code></span>
<span><b>Plan</b>{proof.planLabel}</span>
<span><b>Trace</b>{proof.traceLabel}</span> <span><b>Trace</b>{proof.traceLabel}</span>
<span><b>Evidence</b>{proof.evidenceLabel}</span> <span><b>Evidence</b>{proof.evidenceLabel}</span>
</div> </div>
@@ -181,22 +181,22 @@ export const graphExecutionForBeat = (
case "operation": case "operation":
return { completedNodeIds: [], currentNodeId: "read_docs" }; return { completedNodeIds: [], currentNodeId: "read_docs" };
case "graph": case "graph":
return { completedNodeIds: ["read_docs"], currentNodeId: "build_report" }; return { completedNodeIds: ["read_docs", "reset_board"], currentNodeId: "analyze" };
case "interrupt": case "interrupt":
case "approval": case "approval":
return { return {
completedNodeIds: ["read_docs", "build_report"], completedNodeIds: ["read_docs", "reset_board", "analyze", "build_report", "draft_issues"],
currentNodeId: "review_issues", currentNodeId: "review_issues",
}; };
case "resume": case "resume":
return { return {
completedNodeIds: ["read_docs", "build_report", "review_issues"], completedNodeIds: ["read_docs", "reset_board", "analyze", "build_report", "draft_issues", "review_issues"],
currentNodeId: "create_issues", currentNodeId: "create_issues",
}; };
case "output": case "output":
case "trace": case "trace":
return { return {
completedNodeIds: ["read_docs", "build_report", "review_issues", "create_issues"], completedNodeIds: ["read_docs", "reset_board", "analyze", "build_report", "draft_issues", "review_issues", "create_issues", "finalise"],
currentNodeId: "end_completed", currentNodeId: "end_completed",
}; };
default: default: