feat: remap demo replay beats and fix old scene ID references

This commit is contained in:
lda
2026-07-09 21:38:53 +07:00 Verified
parent f19fa25d97
commit 81c5541a61
11 changed files with 122 additions and 85 deletions
@@ -49,7 +49,7 @@ const requireSceneBeat = (sceneId: string, beatId: string) => {
const renderBeat = ( const renderBeat = (
beatId: string, beatId: string,
sceneId = "workflow-demo", sceneId = "run-from-deployment",
options: { options: {
readonly openEvidence?: () => void; readonly openEvidence?: () => void;
readonly approvalActions?: DemoApprovalActions; readonly approvalActions?: DemoApprovalActions;
@@ -89,27 +89,24 @@ describe("DemoWorkflowScene", () => {
expect(screen.getByLabelText("workflow graph")).toBeInTheDocument(); expect(screen.getByLabelText("workflow graph")).toBeInTheDocument();
}); });
it("renders the canonical typed interrupt contract", () => { it("renders the guided interrupt contract via product moment in typed-human-boundary", () => {
renderBeat("interrupt"); renderBeat("interrupt", "typed-human-boundary");
const contract = screen.getByLabelText("typed interrupt contract"); const moment = screen.getByRole("region", { name: /current product moment/i });
expect(contract).toHaveTextContent("issue_review"); expect(moment).toHaveAttribute("data-moment", "approval");
expect(contract).toHaveTextContent("submitted / cancelled"); expect(screen.getByRole("group", { name: /operator resume decision/i })).toBeInTheDocument();
expect(contract).toHaveTextContent("lda.chat Thesis And Project Readiness Report"); expect(screen.getByText("Workflow input")).toBeInTheDocument();
expect(contract).toHaveTextContent("Prepare the defense walkthrough");
expect(contract).toHaveTextContent('"type": "object"');
expect(contract).toHaveTextContent("run_recorded_lda_report");
}); });
it("carries the contract into Scene 10 approval via guided product moment", () => { it("carries the contract into Scene 10 approval via guided product moment", () => {
renderBeat("approval", "interrupt-evidence"); renderBeat("approval", "typed-human-boundary");
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "approval"); expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "approval");
expect(screen.getByRole("group", { name: /operator resume decision/i })).toBeInTheDocument(); expect(screen.getByRole("group", { name: /operator resume decision/i })).toBeInTheDocument();
}); });
it("makes the Scene 10 approval contract the primary visual", () => { it("makes the Scene 10 approval contract the primary visual", () => {
renderBeat("approval", "interrupt-evidence"); renderBeat("approval", "typed-human-boundary");
const stage = screen.getByLabelText("demo workflow stage"); const stage = screen.getByLabelText("demo workflow stage");
expect(stage).toHaveAttribute("data-demo-layout", "approval"); expect(stage).toHaveAttribute("data-demo-layout", "approval");
@@ -117,18 +114,18 @@ describe("DemoWorkflowScene", () => {
}); });
it("marks trace beat as evidence layout via guided product moment", () => { it("marks trace beat as evidence layout via guided product moment", () => {
renderBeat("trace", "interrupt-evidence"); renderBeat("trace", "resume-output-evidence");
expect(screen.getByLabelText("demo workflow stage")).toHaveAttribute("data-demo-layout", "evidence"); expect(screen.getByLabelText("demo workflow stage")).toHaveAttribute("data-demo-layout", "evidence");
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "trace"); expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "trace");
}); });
it("renders resume and trace via guided product moment", () => { it("renders resume and trace via guided product moment", () => {
const { unmount } = renderBeat("resume", "interrupt-evidence"); const { unmount } = renderBeat("resume", "resume-output-evidence");
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "resume"); expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "resume");
unmount(); unmount();
renderBeat("trace", "interrupt-evidence"); renderBeat("trace", "resume-output-evidence");
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "trace"); expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "trace");
}); });
@@ -138,12 +135,12 @@ describe("DemoWorkflowScene", () => {
expect(screen.getByLabelText("workflow graph proof")).toHaveTextContent("9 workflow nodes"); expect(screen.getByLabelText("workflow graph proof")).toHaveTextContent("9 workflow nodes");
unmount(); unmount();
renderBeat("output", "interrupt-evidence"); renderBeat("output", "resume-output-evidence");
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "output"); expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "output");
}); });
it("marks outcome-panel layouts via guided product moment", () => { it("marks outcome-panel layouts via guided product moment", () => {
renderBeat("resume", "interrupt-evidence"); renderBeat("resume", "resume-output-evidence");
expect(screen.getByLabelText("demo workflow stage")).toHaveAttribute("data-demo-layout", "operation"); expect(screen.getByLabelText("demo workflow stage")).toHaveAttribute("data-demo-layout", "operation");
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "resume"); expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "resume");
@@ -159,16 +156,17 @@ describe("DemoWorkflowScene", () => {
expect(screen.getByLabelText("demo continuity")).toHaveTextContent("typed graph"); expect(screen.getByLabelText("demo continuity")).toHaveTextContent("typed graph");
graph.unmount(); graph.unmount();
renderBeat("interrupt"); renderBeat("interrupt", "typed-human-boundary");
expect(screen.getByLabelText("demo continuity")).toHaveTextContent("issue_review"); expect(screen.getByLabelText("demo continuity")).toHaveTextContent("issue_review");
}); });
it("uses compact graph context for interrupt-focused beats", () => { it("renders approval and interrupt beats via guided product moment", () => {
const interrupt = renderBeat("interrupt"); const interrupt = renderBeat("interrupt", "typed-human-boundary");
expect(screen.getByLabelText("workflow graph")).toHaveAttribute("data-graph-variant", "compact"); expect(screen.queryByLabelText("workflow graph")).not.toBeInTheDocument();
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "approval");
interrupt.unmount(); interrupt.unmount();
renderBeat("approval", "interrupt-evidence"); renderBeat("approval", "typed-human-boundary");
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "approval"); expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "approval");
}); });
@@ -177,19 +175,19 @@ describe("DemoWorkflowScene", () => {
expect(screen.getByLabelText("workflow graph")).toHaveAttribute("data-graph-variant", "full"); expect(screen.getByLabelText("workflow graph")).toHaveAttribute("data-graph-variant", "full");
graph.unmount(); graph.unmount();
renderBeat("output", "interrupt-evidence"); renderBeat("output", "resume-output-evidence");
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "output"); expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "output");
}); });
it("keeps approval beat contract via guided product moment", () => { it("keeps approval beat contract via guided product moment", () => {
renderBeat("approval", "interrupt-evidence"); renderBeat("approval", "typed-human-boundary");
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "approval"); expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "approval");
expect(screen.getByRole("group", { name: /operator resume decision/i })).toBeInTheDocument(); expect(screen.getByRole("group", { name: /operator resume decision/i })).toBeInTheDocument();
}); });
it("wires approval actions into the Scene 10 schema approval surface", () => { it("wires approval actions into the Scene 10 schema approval surface", () => {
renderBeat("approval", "interrupt-evidence", { renderBeat("approval", "typed-human-boundary", {
approvalActions: { approvalActions: {
state: "ready", state: "ready",
canSubmit: true, canSubmit: true,
@@ -204,31 +202,32 @@ describe("DemoWorkflowScene", () => {
}); });
it("shows a factual decision form for the approval beat instead of raw schema as the primary visual", () => { it("shows a factual decision form for the approval beat instead of raw schema as the primary visual", () => {
renderBeat("approval", "interrupt-evidence"); renderBeat("approval", "typed-human-boundary");
expect(screen.getByRole("group", { name: /operator resume decision/i })).toBeInTheDocument(); expect(screen.getByRole("group", { name: /operator resume decision/i })).toBeInTheDocument();
expect(screen.getByText("Workflow input")).toBeInTheDocument(); expect(screen.getByText("Workflow input")).toBeInTheDocument();
}); });
it("keeps raw resume schema visible only in interrupt preview mode", () => { it("renders interrupt decision form but no raw schema in guided product moment", () => {
renderBeat("interrupt"); renderBeat("interrupt", "typed-human-boundary");
expect(screen.getByText("Resume schema")).toBeInTheDocument(); expect(screen.queryByText("Resume schema")).not.toBeInTheDocument();
expect(screen.getByRole("group", { name: /operator resume decision/i })).toBeInTheDocument();
cleanup(); cleanup();
renderBeat("approval", "interrupt-evidence"); renderBeat("approval", "typed-human-boundary");
expect(screen.queryByText("Resume schema")).not.toBeInTheDocument(); expect(screen.getByRole("group", { name: /operator resume decision/i })).toBeInTheDocument();
}); });
it("adds guided product moment for resume, output, and trace beats", () => { it("adds guided product moment for resume, output, and trace beats", () => {
const resume = renderBeat("resume", "interrupt-evidence"); const resume = renderBeat("resume", "resume-output-evidence");
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "resume"); expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "resume");
resume.unmount(); resume.unmount();
const output = renderBeat("output", "interrupt-evidence"); const output = renderBeat("output", "resume-output-evidence");
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "output"); expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "output");
output.unmount(); output.unmount();
renderBeat("trace", "interrupt-evidence"); renderBeat("trace", "resume-output-evidence");
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "trace"); expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "trace");
}); });
}); });
@@ -14,6 +14,7 @@ import { GuidedProductMoment } from "./GuidedProductMoment.js";
import { InterruptContractPreview } from "./InterruptContractPreview.js"; import { InterruptContractPreview } from "./InterruptContractPreview.js";
import { NodeSpotlight } from "./NodeSpotlight.js"; import { NodeSpotlight } from "./NodeSpotlight.js";
import { OperationBlock } from "./OperationBlock.js"; import { OperationBlock } from "./OperationBlock.js";
import { RunInputFacts } from "./RunFactsPanel.js";
import { StageCaption } from "./StageCaption.js"; import { StageCaption } from "./StageCaption.js";
import type { SceneBeatDefinition, SceneDefinition } from "./storyboard.js"; import type { SceneBeatDefinition, SceneDefinition } from "./storyboard.js";
import { WorkflowGraphStage } from "./WorkflowGraphStage.js"; import { WorkflowGraphStage } from "./WorkflowGraphStage.js";
@@ -33,7 +34,7 @@ type DemoWorkflowLayout = "operation" | "graph" | "interrupt" | "approval" | "ev
const layoutForBeat = (beatId: string): DemoWorkflowLayout => { const layoutForBeat = (beatId: string): DemoWorkflowLayout => {
if (beatId === "operation" || beatId === "resume") return "operation"; if (beatId === "operation" || beatId === "resume") return "operation";
if (beatId === "interrupt") return "interrupt"; if (beatId === "interrupt") return "interrupt";
if (beatId === "approval") return "approval"; if (beatId === "approval" || beatId === "cancel") return "approval";
if (beatId === "trace" || beatId === "output") return "evidence"; if (beatId === "trace" || beatId === "output") return "evidence";
return "graph"; return "graph";
}; };
@@ -90,7 +91,8 @@ export const DemoWorkflowScene = ({
? "preview" ? "preview"
: null; : null;
const isGuidedScene10 = scene.id === "interrupt-evidence"; const isGuidedDemoMoment = scene.id === "typed-human-boundary"
|| scene.id === "resume-output-evidence";
return ( return (
<> <>
@@ -101,7 +103,7 @@ export const DemoWorkflowScene = ({
<DemoContinuityRail lens={lens} /> <DemoContinuityRail lens={lens} />
<div className="demo-workflow-stage" data-beat={beat.id} data-demo-layout={layout} aria-label="demo workflow stage"> <div className="demo-workflow-stage" data-beat={beat.id} data-demo-layout={layout} aria-label="demo workflow stage">
{isGuidedScene10 ? ( {isGuidedDemoMoment ? (
<GuidedProductMoment <GuidedProductMoment
beat={beat} beat={beat}
demo={demo} demo={demo}
@@ -128,6 +130,12 @@ export const DemoWorkflowScene = ({
/> />
)} )}
{beat.id === "input" && (
<div className="demo-workflow-stage__input-facts">
<RunInputFacts facts={facts} />
</div>
)}
{showGraph && ( {showGraph && (
<div className="demo-workflow-stage__graph"> <div className="demo-workflow-stage__graph">
<WorkflowGraphStage <WorkflowGraphStage
@@ -11,7 +11,7 @@ describe("OperatorChat", () => {
it("maps light chat theme to the editorial presentation surface", () => { it("maps light chat theme to the editorial presentation surface", () => {
const state = { const state = {
...initialPresentationState, ...initialPresentationState,
location: { kind: "main" as const, sceneId: "workflow-demo" as const, beatId: "graph", focusPath: [] }, location: { kind: "main" as const, sceneId: "run-from-deployment" as const, beatId: "graph", focusPath: [] },
}; };
render(<OperatorChat state={state} />); render(<OperatorChat state={state} />);
@@ -37,7 +37,7 @@ describe("PresentationFooter", () => {
/>, />,
); );
const footer = screen.getByRole("contentinfo", { name: /presentation footer/i }); const footer = screen.getByRole("contentinfo", { name: /presentation footer/i });
expect(within(footer).getByText("6 / 12")).toBeInTheDocument(); expect(within(footer).getByText("6 / 14")).toBeInTheDocument();
expect(within(footer).getByText("3 / 4")).toBeInTheDocument(); expect(within(footer).getByText("3 / 4")).toBeInTheDocument();
expect(within(footer).getByText(/workflow\.runs\.trace/)).toBeInTheDocument(); expect(within(footer).getByText(/workflow\.runs\.trace/)).toBeInTheDocument();
}); });
@@ -74,7 +74,7 @@ describe("PresentationRoute", () => {
}); });
it("shows node spotlight when a graph node is selected", async () => { it("shows node spotlight when a graph node is selected", async () => {
window.location.hash = "#scene/workflow-demo/graph"; window.location.hash = "#scene/run-from-deployment/graph";
const { PresentationRoute } = await import("./PresentationRoute.js"); const { PresentationRoute } = await import("./PresentationRoute.js");
render(<PresentationRoute />); render(<PresentationRoute />);
await userEvent.click(screen.getByRole("button", { name: /issue review/i })); await userEvent.click(screen.getByRole("button", { name: /issue review/i }));
@@ -84,7 +84,7 @@ describe("PresentationRoute", () => {
it("can advance replay far enough to show a product operation block", async () => { it("can advance replay far enough to show a product operation block", async () => {
setReplayMode(); setReplayMode();
window.location.hash = "#scene/workflow-demo/operation"; window.location.hash = "#scene/run-from-deployment/operation";
const { PresentationRoute } = await import("./PresentationRoute.js"); const { PresentationRoute } = await import("./PresentationRoute.js");
render(<PresentationRoute />); render(<PresentationRoute />);
expect(await screen.findByLabelText("workflow.runs.start operation")).toBeInTheDocument(); expect(await screen.findByLabelText("workflow.runs.start operation")).toBeInTheDocument();
@@ -118,7 +118,7 @@ describe("PresentationRoute", () => {
}); });
it("shows a receipt without auto-opening evidence on an evidence beat", async () => { it("shows a receipt without auto-opening evidence on an evidence beat", async () => {
window.location.hash = "#scene/interrupt-evidence/trace"; window.location.hash = "#scene/resume-output-evidence/trace";
const { PresentationRoute } = await import("./PresentationRoute.js"); const { PresentationRoute } = await import("./PresentationRoute.js");
render(<PresentationRoute />); render(<PresentationRoute />);
expect(await screen.findByRole("button", { name: /inspect evidence/i })).toBeInTheDocument(); expect(await screen.findByRole("button", { name: /inspect evidence/i })).toBeInTheDocument();
@@ -128,7 +128,7 @@ describe("PresentationRoute", () => {
it("opens the inspector from an explicit operation action", async () => { it("opens the inspector from an explicit operation action", async () => {
setReplayMode(); setReplayMode();
const user = userEvent.setup(); const user = userEvent.setup();
window.location.hash = "#scene/workflow-demo/operation"; window.location.hash = "#scene/run-from-deployment/operation";
const { PresentationRoute } = await import("./PresentationRoute.js"); const { PresentationRoute } = await import("./PresentationRoute.js");
render(<PresentationRoute />); render(<PresentationRoute />);
await user.click(await screen.findByRole("button", { name: /view raw evidence/i })); await user.click(await screen.findByRole("button", { name: /view raw evidence/i }));
@@ -138,7 +138,7 @@ describe("PresentationRoute", () => {
it("closes the inspector from the explicit close action", async () => { it("closes the inspector from the explicit close action", async () => {
setReplayMode(); setReplayMode();
const user = userEvent.setup(); const user = userEvent.setup();
window.location.hash = "#scene/workflow-demo/operation"; window.location.hash = "#scene/run-from-deployment/operation";
const { PresentationRoute } = await import("./PresentationRoute.js"); const { PresentationRoute } = await import("./PresentationRoute.js");
render(<PresentationRoute />); render(<PresentationRoute />);
await user.click(await screen.findByRole("button", { name: /view raw evidence/i })); await user.click(await screen.findByRole("button", { name: /view raw evidence/i }));
@@ -150,7 +150,7 @@ describe("PresentationRoute", () => {
it("returns to the receipt after closing the inspector on a receipt beat", async () => { it("returns to the receipt after closing the inspector on a receipt beat", async () => {
setReplayMode(); setReplayMode();
const user = userEvent.setup(); const user = userEvent.setup();
window.location.hash = "#scene/interrupt-evidence/trace"; window.location.hash = "#scene/resume-output-evidence/trace";
const { PresentationRoute } = await import("./PresentationRoute.js"); const { PresentationRoute } = await import("./PresentationRoute.js");
render(<PresentationRoute />); render(<PresentationRoute />);
await user.click(await screen.findByRole("button", { name: /inspect evidence/i })); await user.click(await screen.findByRole("button", { name: /inspect evidence/i }));
@@ -188,16 +188,16 @@ describe("PresentationRoute", () => {
}); });
it("opens Scene 10 approval from the canonical hash", async () => { it("opens Scene 10 approval from the canonical hash", async () => {
window.location.hash = "#scene/interrupt-evidence/approval"; window.location.hash = "#scene/typed-human-boundary/approval";
const { PresentationRoute } = await import("./PresentationRoute.js"); const { PresentationRoute } = await import("./PresentationRoute.js");
render(<PresentationRoute />); render(<PresentationRoute />);
expect(await screen.findByRole("heading", { name: /Interrupt, Resume, Evidence/i })).toBeInTheDocument(); expect(await screen.findByRole("heading", { name: /Typed Human Boundary/i })).toBeInTheDocument();
expect(screen.getByLabelText("demo workflow stage")).toHaveAttribute("data-demo-layout", "approval"); expect(screen.getByLabelText("demo workflow stage")).toHaveAttribute("data-demo-layout", "approval");
}); });
it("renders replay-backed approval evidence on a direct approval hash", async () => { it("renders replay-backed approval evidence on a direct approval hash", async () => {
window.location.hash = "#scene/interrupt-evidence/approval"; window.location.hash = "#scene/typed-human-boundary/approval";
const { PresentationRoute } = await import("./PresentationRoute.js"); const { PresentationRoute } = await import("./PresentationRoute.js");
render(<PresentationRoute />); render(<PresentationRoute />);
@@ -210,7 +210,7 @@ describe("PresentationRoute", () => {
it("chat run action advances the replay timeline when no live server is configured", async () => { it("chat run action advances the replay timeline when no live server is configured", async () => {
setReplayMode(); setReplayMode();
window.location.hash = "#scene/workflow-demo/operation"; window.location.hash = "#scene/run-from-deployment/operation";
const user = userEvent.setup(); const user = userEvent.setup();
const { PresentationRoute } = await import("./PresentationRoute.js"); const { PresentationRoute } = await import("./PresentationRoute.js");
render(<PresentationRoute />); render(<PresentationRoute />);
@@ -223,7 +223,7 @@ describe("PresentationRoute", () => {
it("submits Scene 10 approval and advances to the resume beat", async () => { it("submits Scene 10 approval and advances to the resume beat", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
setReplayMode(); setReplayMode();
window.location.hash = "#scene/interrupt-evidence/approval"; window.location.hash = "#scene/typed-human-boundary/approval";
const { PresentationRoute } = await import("./PresentationRoute.js"); const { PresentationRoute } = await import("./PresentationRoute.js");
render(<PresentationRoute />); render(<PresentationRoute />);
@@ -234,14 +234,14 @@ describe("PresentationRoute", () => {
await user.click(submitButton); await user.click(submitButton);
}); });
expect(window.location.hash).toBe("#scene/interrupt-evidence/resume"); expect(window.location.hash).toBe("#scene/resume-output-evidence/resume");
expect(screen.getByLabelText("workflow.runs.resume operation")).toBeInTheDocument(); expect(screen.getByLabelText("workflow.runs.resume operation")).toBeInTheDocument();
}); });
it("reopens approval controls after returning from submitted resume", async () => { it("reopens approval controls after returning from submitted resume", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
setReplayMode(); setReplayMode();
window.location.hash = "#scene/interrupt-evidence/approval"; window.location.hash = "#scene/typed-human-boundary/approval";
const { PresentationRoute } = await import("./PresentationRoute.js"); const { PresentationRoute } = await import("./PresentationRoute.js");
render(<PresentationRoute />); render(<PresentationRoute />);
@@ -250,9 +250,9 @@ describe("PresentationRoute", () => {
await act(async () => { await act(async () => {
await user.click(submitButton); await user.click(submitButton);
}); });
expect(window.location.hash).toBe("#scene/interrupt-evidence/resume"); expect(window.location.hash).toBe("#scene/resume-output-evidence/resume");
window.location.hash = "#scene/interrupt-evidence/approval"; window.location.hash = "#scene/typed-human-boundary/approval";
window.dispatchEvent(new HashChangeEvent("hashchange")); window.dispatchEvent(new HashChangeEvent("hashchange"));
expect(await screen.findByRole("button", { name: "Submit" })).toBeEnabled(); expect(await screen.findByRole("button", { name: "Submit" })).toBeEnabled();
@@ -262,7 +262,7 @@ describe("PresentationRoute", () => {
it("cancels Scene 10 approval in replay without applying submitted evidence", async () => { it("cancels Scene 10 approval in replay without applying submitted evidence", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
setReplayMode(); setReplayMode();
window.location.hash = "#scene/interrupt-evidence/approval"; window.location.hash = "#scene/typed-human-boundary/approval";
const { PresentationRoute } = await import("./PresentationRoute.js"); const { PresentationRoute } = await import("./PresentationRoute.js");
render(<PresentationRoute />); render(<PresentationRoute />);
@@ -275,12 +275,12 @@ describe("PresentationRoute", () => {
expect(screen.queryByRole("button", { name: "Submit" })).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Submit" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Cancel" })).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Cancel" })).not.toBeInTheDocument();
expect(window.location.hash).toBe("#scene/interrupt-evidence/approval"); expect(window.location.hash).toBe("#scene/typed-human-boundary/approval");
expect(screen.queryByLabelText("workflow.runs.resume operation")).not.toBeInTheDocument(); expect(screen.queryByLabelText("workflow.runs.resume operation")).not.toBeInTheDocument();
}); });
it("opens approval with enabled approval controls immediately after priming", async () => { it("opens approval with enabled approval controls immediately after priming", async () => {
window.location.hash = "#scene/interrupt-evidence/approval"; window.location.hash = "#scene/typed-human-boundary/approval";
const { PresentationRoute } = await import("./PresentationRoute.js"); const { PresentationRoute } = await import("./PresentationRoute.js");
render(<PresentationRoute />); render(<PresentationRoute />);
@@ -289,7 +289,7 @@ describe("PresentationRoute", () => {
}); });
it("opens resume with resume operation proof immediately after priming", async () => { it("opens resume with resume operation proof immediately after priming", async () => {
window.location.hash = "#scene/interrupt-evidence/resume"; window.location.hash = "#scene/resume-output-evidence/resume";
const { PresentationRoute } = await import("./PresentationRoute.js"); const { PresentationRoute } = await import("./PresentationRoute.js");
render(<PresentationRoute />); render(<PresentationRoute />);
@@ -120,7 +120,7 @@ export const PresentationRoute = () => {
}, [demo.state.mode, demo.primeReplayToStage, state.location]); }, [demo.state.mode, demo.primeReplayToStage, state.location]);
const isApprovalBeat = state.location.kind === "main" const isApprovalBeat = state.location.kind === "main"
&& state.location.sceneId === "interrupt-evidence" && state.location.sceneId === "typed-human-boundary"
&& state.location.beatId === "approval"; && state.location.beatId === "approval";
useEffect(() => { useEffect(() => {
@@ -148,7 +148,7 @@ export const PresentationRoute = () => {
type: "jump", type: "jump",
location: { location: {
kind: "main", kind: "main",
sceneId: "interrupt-evidence", sceneId: "resume-output-evidence",
beatId: "resume", beatId: "resume",
focusPath: [], focusPath: [],
}, },
@@ -59,7 +59,7 @@ describe("SceneBody", () => {
}); });
it("renders the real workflow graph for demo scenes", () => { it("renders the real workflow graph for demo scenes", () => {
const location: PresentationLocation = { kind: "main", sceneId: "workflow-demo", beatId: "graph", focusPath: [] }; const location: PresentationLocation = { kind: "main", sceneId: "run-from-deployment", beatId: "graph", focusPath: [] };
render( render(
<SceneBody <SceneBody
location={location} location={location}
@@ -16,22 +16,22 @@ describe("SceneProgress", () => {
render(<SceneProgress location={location} />); render(<SceneProgress location={location} />);
expect(screen.getByText("6 / 12")).toBeInTheDocument(); expect(screen.getByText("6 / 14")).toBeInTheDocument();
expect(screen.getByText("3 / 4")).toBeInTheDocument(); expect(screen.getByText("3 / 4")).toBeInTheDocument();
}); });
it("shows scene position for workflow-demo/graph", () => { it("shows scene position for run-from-deployment/graph", () => {
const location: MainLocation = { const location: MainLocation = {
kind: "main", kind: "main",
sceneId: "workflow-demo", sceneId: "run-from-deployment",
beatId: "graph", beatId: "graph",
focusPath: [], focusPath: [],
}; };
render(<SceneProgress location={location} />); render(<SceneProgress location={location} />);
expect(screen.getByText("9 / 12")).toBeInTheDocument(); expect(screen.getByText("10 / 14")).toBeInTheDocument();
expect(screen.getByText("2 / 3")).toBeInTheDocument(); expect(screen.getByText("3 / 3")).toBeInTheDocument();
}); });
it("has an accessible label", () => { it("has an accessible label", () => {
@@ -3,13 +3,19 @@ import { requirementForDemoBeat } from "./demo-beat-requirements.js";
describe("requirementForDemoBeat", () => { describe("requirementForDemoBeat", () => {
it.each([ it.each([
["workflow-demo", "operation", "run_start"], ["prepared-lifecycle", "draft", "deployment_check"],
["workflow-demo", "graph", "run_start"], ["prepared-lifecycle", "artifact", "deployment_check"],
["workflow-demo", "interrupt", "interrupt"], ["prepared-lifecycle", "deployment", "deployment_check"],
["interrupt-evidence", "approval", "interrupt"], ["prepared-lifecycle", "ready-run", "run_start"],
["interrupt-evidence", "resume", "run_resume"], ["run-from-deployment", "input", "run_start"],
["interrupt-evidence", "output", "run_resume"], ["run-from-deployment", "operation", "run_start"],
["interrupt-evidence", "trace", "trace_read"], ["run-from-deployment", "graph", "run_start"],
["typed-human-boundary", "interrupt", "interrupt"],
["typed-human-boundary", "approval", "interrupt"],
["typed-human-boundary", "cancel", "interrupt"],
["resume-output-evidence", "resume", "run_resume"],
["resume-output-evidence", "output", "run_resume"],
["resume-output-evidence", "trace", "trace_read"],
] as const)("maps %s/%s to %s", (sceneId, beatId, stage) => { ] as const)("maps %s/%s to %s", (sceneId, beatId, stage) => {
expect(requirementForDemoBeat(sceneId, beatId).requiredStage).toBe(stage); expect(requirementForDemoBeat(sceneId, beatId).requiredStage).toBe(stage);
}); });
@@ -6,31 +6,55 @@ export type DemoBeatRequirement = {
}; };
const requirements: Readonly<Record<string, DemoBeatRequirement>> = { const requirements: Readonly<Record<string, DemoBeatRequirement>> = {
"workflow-demo/operation": { "prepared-lifecycle/draft": {
requiredStage: "deployment_check",
reason: "Prepared draft beat needs deployment inspect evidence for lifecycle context.",
},
"prepared-lifecycle/artifact": {
requiredStage: "deployment_check",
reason: "Artifact beat needs deployment inspect evidence.",
},
"prepared-lifecycle/deployment": {
requiredStage: "deployment_check",
reason: "Deployment beat needs configured source bindings.",
},
"prepared-lifecycle/ready-run": {
requiredStage: "run_start",
reason: "Ready-run beat needs the recorded run id and status.",
},
"run-from-deployment/input": {
requiredStage: "run_start",
reason: "Input beat needs workflow input from run start.",
},
"run-from-deployment/operation": {
requiredStage: "run_start", requiredStage: "run_start",
reason: "Operation beat needs the recorded run start.", reason: "Operation beat needs the recorded run start.",
}, },
"workflow-demo/graph": { "run-from-deployment/graph": {
requiredStage: "run_start", requiredStage: "run_start",
reason: "Graph beat needs the persisted run id.", reason: "Graph beat needs the persisted run id.",
}, },
"workflow-demo/interrupt": { "typed-human-boundary/interrupt": {
requiredStage: "interrupt", requiredStage: "interrupt",
reason: "Interrupt beat needs the typed review payload.", reason: "Interrupt beat needs the typed review payload.",
}, },
"interrupt-evidence/approval": { "typed-human-boundary/approval": {
requiredStage: "interrupt", requiredStage: "interrupt",
reason: "Approval beat needs the typed review payload before controls can act.", reason: "Approval beat needs the typed review payload before controls can act.",
}, },
"interrupt-evidence/resume": { "typed-human-boundary/cancel": {
requiredStage: "interrupt",
reason: "Cancel beat needs the review payload but no submitted replay evidence.",
},
"resume-output-evidence/resume": {
requiredStage: "run_resume", requiredStage: "run_resume",
reason: "Resume beat needs the recorded resume operation.", reason: "Resume beat needs the recorded resume operation.",
}, },
"interrupt-evidence/output": { "resume-output-evidence/output": {
requiredStage: "run_resume", requiredStage: "run_resume",
reason: "Output beat needs the resumed run output.", reason: "Output beat needs the resumed run output.",
}, },
"interrupt-evidence/trace": { "resume-output-evidence/trace": {
requiredStage: "trace_read", requiredStage: "trace_read",
reason: "Trace beat needs the recorded trace read.", reason: "Trace beat needs the recorded trace read.",
}, },
@@ -28,9 +28,9 @@ describe("presentationReducer", () => {
it("jumps to a specific scene and beat", () => { it("jumps to a specific scene and beat", () => {
const jumped = presentationReducer(initialPresentationState, { const jumped = presentationReducer(initialPresentationState, {
type: "jump", type: "jump",
location: { kind: "main", sceneId: "workflow-demo", beatId: "graph", focusPath: [] }, location: { kind: "main", sceneId: "run-from-deployment", beatId: "graph", focusPath: [] },
}); });
expect(jumped.location).toEqual({ kind: "main", sceneId: "workflow-demo", beatId: "graph", focusPath: [] }); expect(jumped.location).toEqual({ kind: "main", sceneId: "run-from-deployment", beatId: "graph", focusPath: [] });
}); });
it("parses a scene hash", () => { it("parses a scene hash", () => {
@@ -149,7 +149,7 @@ describe("presentationReducer", () => {
it("derives chatMode from the current beat", () => { it("derives chatMode from the current beat", () => {
const state = presentationReducer(initialPresentationState, { const state = presentationReducer(initialPresentationState, {
type: "jump", type: "jump",
location: { kind: "main", sceneId: "workflow-demo", beatId: "graph", focusPath: [] }, location: { kind: "main", sceneId: "run-from-deployment", beatId: "graph", focusPath: [] },
}); });
expect(compositionForState(state)).toMatchObject({ expect(compositionForState(state)).toMatchObject({
chatMode: "hidden", chatMode: "hidden",
@@ -169,7 +169,7 @@ describe("presentationReducer", () => {
it("does not reopen evidence on repeated Escape after force-close", () => { it("does not reopen evidence on repeated Escape after force-close", () => {
const stateAtTrace = presentationReducer(initialPresentationState, { const stateAtTrace = presentationReducer(initialPresentationState, {
type: "jump", type: "jump",
location: { kind: "main", sceneId: "interrupt-evidence", beatId: "trace", focusPath: [] }, location: { kind: "main", sceneId: "resume-output-evidence", beatId: "trace", focusPath: [] },
}); });
expect(stateAtTrace.evidencePresentationOverride).toBeNull(); expect(stateAtTrace.evidencePresentationOverride).toBeNull();
@@ -248,7 +248,7 @@ describe("presentationReducer", () => {
it("returns to the beat receipt after closing an explicit inspector", () => { it("returns to the beat receipt after closing an explicit inspector", () => {
const atReceiptBeat = presentationReducer(initialPresentationState, { const atReceiptBeat = presentationReducer(initialPresentationState, {
type: "jump", type: "jump",
location: { kind: "main", sceneId: "interrupt-evidence", beatId: "trace", focusPath: [] }, location: { kind: "main", sceneId: "resume-output-evidence", beatId: "trace", focusPath: [] },
}); });
expect(compositionForState(atReceiptBeat).evidencePresentation).toBe("receipt"); expect(compositionForState(atReceiptBeat).evidencePresentation).toBe("receipt");
const opened = presentationReducer(atReceiptBeat, { const opened = presentationReducer(atReceiptBeat, {