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", () => {
const { unmount } = renderBeat("graph");
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();
renderBeat("output", "resume-output-evidence");
@@ -73,7 +73,8 @@ export const DemoWorkflowScene = ({
const runProof = {
runId: runStart?.resultingIds.runId ?? null,
traceLabel: "9 workflow nodes",
planLabel: "11 plan nodes",
traceLabel: "3 trace frames",
evidenceLabel: "JSON-RPC evidence",
};
@@ -20,11 +20,55 @@ describe("WorkflowGraphStage", () => {
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", () => {
render(
<WorkflowGraphStage
execution={{
completedNodeIds: ["read_docs", "build_report"],
completedNodeIds: ["read_docs", "reset_board", "analyze", "build_report", "draft_issues"],
currentNodeId: "review_issues",
}}
selectedNodeId={null}
@@ -32,65 +76,64 @@ describe("WorkflowGraphStage", () => {
/>,
);
const readDocs = screen.getByRole("button", { name: /read documents/i });
const buildReport = screen.getByRole("button", { name: /build report/i });
const readDocs = screen.getByRole("button", { name: /read docs/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(buildReport).toHaveAttribute("data-execution-state", "completed");
expect(reviewIssues).toHaveAttribute("data-execution-state", "current");
expect(reviewIssues).toHaveAttribute("data-current-interrupt", "true");
expect(reviewIssues).toHaveTextContent("Current interrupt");
expect(createIssues).toHaveAttribute("data-execution-state", "future");
expect(revisionReq).toHaveAttribute("data-execution-state", "future");
});
it("renders connectors between nodes", () => {
render(
<WorkflowGraphStage
execution={{ completedNodeIds: ["read_docs"], currentNodeId: "build_report" }}
execution={{ completedNodeIds: ["read_docs"], currentNodeId: "reset_board" }}
selectedNodeId={null}
selectNode={vi.fn()}
/>,
);
const connectors = screen.getAllByTestId("workflow-connector");
expect(connectors).toHaveLength(4);
expect(connectors).toHaveLength(10);
expect(connectors.filter((connector) => connector.dataset.active === "true")).toHaveLength(1);
});
it("keeps all graph nodes inside the visible percentage frame", () => {
for (const node of presentationNodes) {
expect(node.x).toBeGreaterThanOrEqual(14);
expect(node.x).toBeLessThanOrEqual(86);
expect(node.y).toBeGreaterThanOrEqual(28);
expect(node.y).toBeLessThanOrEqual(72);
expect(node.x).toBeGreaterThanOrEqual(8);
expect(node.x).toBeLessThanOrEqual(92);
expect(node.y).toBeGreaterThanOrEqual(34);
expect(node.y).toBeLessThanOrEqual(78);
}
});
it("renders compact run proof inside the graph", () => {
render(
<WorkflowGraphStage
execution={{ completedNodeIds: ["read_docs"], currentNodeId: "build_report" }}
execution={{ completedNodeIds: ["read_docs"], currentNodeId: "reset_board" }}
selectedNodeId={null}
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("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");
});
it("marks compact graph mode and suppresses proof chips", () => {
render(
<WorkflowGraphStage
execution={{ completedNodeIds: ["read_docs", "build_report"], currentNodeId: "review_issues" }}
execution={{ completedNodeIds: ["read_docs", "reset_board"], currentNodeId: "analyze" }}
selectedNodeId={null}
selectNode={vi.fn()}
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", () => {
render(
<WorkflowGraphStage
execution={{ completedNodeIds: ["read_docs"], currentNodeId: "build_report" }}
execution={{ completedNodeIds: ["read_docs"], currentNodeId: "reset_board" }}
selectedNodeId={null}
selectNode={vi.fn()}
/>,
@@ -116,7 +159,7 @@ describe("WorkflowGraphStage", () => {
execution={{ completedNodeIds: [], currentNodeId: null }}
selectedNodeId={null}
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> = [
{ id: "read_docs", label: "Read documents", detail: "5 selected", kind: "node", x: 14, y: 58 },
{ id: "build_report", label: "Build report", detail: "Markdown", kind: "node", x: 34, y: 36 },
{ id: "review_issues", label: "Issue review", detail: "Typed interrupt", kind: "interrupt", x: 52, y: 58 },
{ id: "create_issues", label: "Create issues", detail: "Selected only", kind: "node", x: 70, y: 36 },
{ id: "end_completed", label: "Completed", detail: "Persisted run", kind: "end", x: 86, y: 58 },
{ id: "read_docs", label: "Read docs", detail: "document source", kind: "node", x: 8, y: 54 },
{ id: "reset_board", label: "Reset board", detail: "issue board", kind: "node", x: 20, y: 34 },
{ id: "analyze", label: "Analyze", detail: "report source", kind: "node", x: 32, y: 54 },
{ id: "build_report", label: "Build report", detail: "markdown", kind: "node", x: 44, y: 34 },
{ 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];
const presentationEdges: ReadonlyArray<PresentationEdge> = [
["read_docs", "build_report"],
["build_report", "review_issues"],
["read_docs", "reset_board"],
["reset_board", "analyze"],
["analyze", "build_report"],
["build_report", "draft_issues"],
["draft_issues", "review_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";
export type WorkflowGraphProof = {
readonly runId: string | null;
readonly planLabel: string;
readonly traceLabel: string;
readonly evidenceLabel: string;
};
@@ -83,6 +96,7 @@ export const WorkflowGraphStage = ({
{variant === "full" && 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>Plan</b>{proof.planLabel}</span>
<span><b>Trace</b>{proof.traceLabel}</span>
<span><b>Evidence</b>{proof.evidenceLabel}</span>
</div>
@@ -181,22 +181,22 @@ export const graphExecutionForBeat = (
case "operation":
return { completedNodeIds: [], currentNodeId: "read_docs" };
case "graph":
return { completedNodeIds: ["read_docs"], currentNodeId: "build_report" };
return { completedNodeIds: ["read_docs", "reset_board"], currentNodeId: "analyze" };
case "interrupt":
case "approval":
return {
completedNodeIds: ["read_docs", "build_report"],
completedNodeIds: ["read_docs", "reset_board", "analyze", "build_report", "draft_issues"],
currentNodeId: "review_issues",
};
case "resume":
return {
completedNodeIds: ["read_docs", "build_report", "review_issues"],
completedNodeIds: ["read_docs", "reset_board", "analyze", "build_report", "draft_issues", "review_issues"],
currentNodeId: "create_issues",
};
case "output":
case "trace":
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",
};
default: