fix: separate Scene 11 interrupt context from approval

This commit is contained in:
lda
2026-07-12 09:40:20 +07:00 Verified
parent 4382bb135c
commit b4c442402b
4 changed files with 67 additions and 7 deletions
@@ -93,9 +93,12 @@ describe("DemoWorkflowScene", () => {
renderBeat("interrupt", "typed-human-boundary");
const moment = screen.getByRole("region", { name: /current product moment/i });
expect(moment).toHaveAttribute("data-moment", "approval");
expect(screen.getByRole("group", { name: /operator resume decision/i })).toBeInTheDocument();
expect(moment).toHaveAttribute("data-moment", "interrupt");
expect(screen.getByText("Workflow input")).toBeInTheDocument();
expect(screen.getByText("Interrupt payload")).toBeInTheDocument();
expect(screen.queryByRole("group", { name: /operator resume decision/i })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Submit" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Request revision" })).not.toBeInTheDocument();
});
it("carries the contract into Scene 10 approval via guided product moment", () => {
@@ -164,7 +167,9 @@ describe("DemoWorkflowScene", () => {
it("renders approval and interrupt beats via guided product moment", () => {
const interrupt = renderBeat("interrupt", "typed-human-boundary");
expect(screen.queryByLabelText("workflow graph")).not.toBeInTheDocument();
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", "interrupt");
expect(screen.queryByRole("button", { name: "Submit" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Request revision" })).not.toBeInTheDocument();
interrupt.unmount();
renderBeat("approval", "typed-human-boundary");
@@ -209,10 +214,13 @@ describe("DemoWorkflowScene", () => {
expect(screen.getByText("Workflow input")).toBeInTheDocument();
});
it("renders interrupt decision form but no raw schema in guided product moment", () => {
it("renders interrupt facts but no decision controls in guided product moment", () => {
renderBeat("interrupt", "typed-human-boundary");
expect(screen.queryByText("Resume schema")).not.toBeInTheDocument();
expect(screen.getByRole("group", { name: /operator resume decision/i })).toBeInTheDocument();
expect(screen.getByText("Interrupt payload")).toBeInTheDocument();
expect(screen.queryByRole("group", { name: /operator resume decision/i })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Submit" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Request revision" })).not.toBeInTheDocument();
cleanup();
renderBeat("approval", "typed-human-boundary");
@@ -53,6 +53,26 @@ const demoWithAppliedCount = (appliedCount: number): DemoTimelineController => (
});
describe("GuidedProductMoment", () => {
it("renders interrupt context without decision controls", () => {
render(
<GuidedProductMoment
beat={findBeat("typed-human-boundary", "interrupt")!}
demo={demo}
contract={contract}
operation={null}
openEvidence={vi.fn()}
/>,
);
const moment = screen.getByRole("region", { name: /current product moment/i });
expect(moment).toHaveAttribute("data-moment", "interrupt");
expect(screen.getByText("Workflow input")).toBeInTheDocument();
expect(screen.getByText("Interrupt payload")).toBeInTheDocument();
expect(screen.queryByRole("group", { name: /operator resume decision/i })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Submit" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Request revision" })).not.toBeInTheDocument();
});
it("makes approval the primary product decision with factual panels", () => {
render(
<GuidedProductMoment
@@ -26,13 +26,20 @@ export type GuidedProductMomentProps = {
readonly openEvidence: () => void;
};
const momentForBeat = (beatId: string): "approval" | "resume" | "output" | "trace" =>
beatId === "resume" || beatId === "output" || beatId === "trace" ? beatId : "approval";
type GuidedProductMoment = "interrupt" | "approval" | "resume" | "output" | "trace";
const momentForBeat = (beatId: string): GuidedProductMoment => {
if (beatId === "interrupt" || beatId === "approval" || beatId === "resume" || beatId === "output" || beatId === "trace") {
return beatId;
}
return "approval";
};
const statusCopy = (
moment: ReturnType<typeof momentForBeat>,
approvalActions?: DemoApprovalActions,
): string => {
if (moment === "interrupt") return "Run paused at the typed interrupt; inspect the context before deciding.";
if (moment !== "approval") return "Same persisted run; inspect the proof below.";
if (approvalActions?.state === "submitted") return "Submitted. Same run resumed.";
if (approvalActions?.state === "revision_requested") {
@@ -43,6 +50,8 @@ const statusCopy = (
const hierarchyForMoment = (moment: ReturnType<typeof momentForBeat>) => {
switch (moment) {
case "interrupt":
return { primary: "interrupt-context", support: "input-facts" };
case "approval":
return { primary: "interrupt-approval", support: "input-facts" };
case "resume":
@@ -88,6 +97,12 @@ export const GuidedProductMoment = ({
</header>
<div className="guided-product-moment__primary">
{moment === "interrupt" ? (
<div className="guided-product-moment__interrupt-grid">
<RunInputFacts facts={facts} density="compact" />
<InterruptPayloadFacts facts={facts} priority="primary" />
</div>
) : null}
{moment === "approval" && contract ? (
<div className="guided-product-moment__approval-grid">
<aside className="guided-product-moment__input-rail" aria-label="workflow input context">
@@ -1244,6 +1244,7 @@
}
.guided-product-moment[data-moment="approval"],
.guided-product-moment[data-moment="interrupt"],
.guided-product-moment[data-moment="resume"],
.guided-product-moment[data-moment="output"],
.guided-product-moment[data-moment="trace"] {
@@ -1260,6 +1261,10 @@
gap: 0.55rem;
}
.guided-product-moment[data-moment="interrupt"] {
gap: 0.55rem;
}
.guided-product-moment[data-moment="approval"] .guided-product-moment__header {
grid-template-columns: minmax(0, 0.42fr) minmax(0, 1fr);
padding: 0.55rem 0.75rem;
@@ -1291,6 +1296,17 @@
min-height: 0;
}
.guided-product-moment__interrupt-grid {
display: grid;
grid-template-columns: minmax(14rem, 0.55fr) minmax(0, 1.45fr);
gap: 0.85rem;
min-height: 0;
}
.guided-product-moment__interrupt-grid > * {
min-width: 0;
}
.guided-product-moment__approval-grid > * {
min-width: 0;
}
@@ -1570,6 +1586,7 @@
@container presentation-canvas (max-width: 1050px) {
.guided-product-moment__approval-grid,
.guided-product-moment__interrupt-grid,
.guided-product-moment__resume-grid,
.guided-product-moment__trace-grid {
grid-template-columns: minmax(0, 1fr);