feat: remap demo replay beats and fix old scene ID references
This commit is contained in:
@@ -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, {
|
||||||
|
|||||||
Reference in New Issue
Block a user