feat: show factual Scene 10 run state
This commit is contained in:
@@ -103,9 +103,7 @@ describe("DemoWorkflowScene", () => {
|
|||||||
renderBeat("approval", "interrupt-evidence");
|
renderBeat("approval", "interrupt-evidence");
|
||||||
|
|
||||||
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.getByLabelText("typed interrupt contract")).toHaveTextContent(
|
expect(screen.getByRole("group", { name: /operator resume decision/i })).toBeInTheDocument();
|
||||||
"run_recorded_lda_report",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("makes the Scene 10 approval contract the primary visual", () => {
|
it("makes the Scene 10 approval contract the primary visual", () => {
|
||||||
@@ -113,9 +111,7 @@ describe("DemoWorkflowScene", () => {
|
|||||||
|
|
||||||
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");
|
||||||
expect(screen.getByLabelText("typed interrupt contract")).toHaveAttribute("data-hero", "true");
|
expect(screen.getByRole("group", { name: /operator resume decision/i })).toBeInTheDocument();
|
||||||
expect(screen.getByLabelText("typed interrupt contract")).toHaveTextContent("Operator decision");
|
|
||||||
expect(screen.getByRole("group", { name: /issue review resume/i })).toHaveTextContent("submitted / cancelled");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("marks trace beat as evidence layout via guided product moment", () => {
|
it("marks trace beat as evidence layout via guided product moment", () => {
|
||||||
@@ -186,8 +182,8 @@ describe("DemoWorkflowScene", () => {
|
|||||||
it("keeps approval beat contract via guided product moment", () => {
|
it("keeps approval beat contract via guided product moment", () => {
|
||||||
renderBeat("approval", "interrupt-evidence");
|
renderBeat("approval", "interrupt-evidence");
|
||||||
|
|
||||||
expect(screen.getByLabelText("typed interrupt contract")).toHaveAttribute("data-hero", "true");
|
|
||||||
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();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("wires approval actions into the Scene 10 schema approval surface", () => {
|
it("wires approval actions into the Scene 10 schema approval surface", () => {
|
||||||
@@ -205,16 +201,11 @@ describe("DemoWorkflowScene", () => {
|
|||||||
expect(screen.getByRole("button", { name: "Cancel" })).toBeEnabled();
|
expect(screen.getByRole("button", { name: "Cancel" })).toBeEnabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows a schema approval surface 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", "interrupt-evidence");
|
||||||
|
|
||||||
const approval = screen.getByRole("group", { name: /issue review resume/i });
|
expect(screen.getByRole("group", { name: /operator resume decision/i })).toBeInTheDocument();
|
||||||
expect(within(approval).getByText("Schema-backed decision")).toBeInTheDocument();
|
expect(screen.getByText("Workflow input")).toBeInTheDocument();
|
||||||
expect(within(approval).getByText("selected_issue_ids")).toBeInTheDocument();
|
|
||||||
expect(within(approval).getByText("[\"risk-1\"]")).toBeInTheDocument();
|
|
||||||
expect(within(approval).queryByText("approved")).not.toBeInTheDocument();
|
|
||||||
expect(within(approval).getByRole("button", { name: /submit/i })).toBeDisabled();
|
|
||||||
expect(within(approval).getByRole("button", { name: /cancel/i })).toBeDisabled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps raw resume schema visible only in interrupt preview mode", () => {
|
it("keeps raw resume schema visible only in interrupt preview mode", () => {
|
||||||
|
|||||||
@@ -16,7 +16,17 @@ const resumeOperation = projectOperationPresentation(runResume);
|
|||||||
const demo = {
|
const demo = {
|
||||||
state: { mode: "replay", phase: "review", events: recording.events, appliedCount: 3, autoplay: false, error: null },
|
state: { mode: "replay", phase: "review", events: recording.events, appliedCount: 3, autoplay: false, error: null },
|
||||||
inFlight: false,
|
inFlight: false,
|
||||||
interruptPayload: null,
|
interruptPayload: {
|
||||||
|
report_markdown: "# lda.chat Thesis And Project Readiness Report\n\nThe workflow substrate is ready for the defense demo.",
|
||||||
|
proposed_issues: [
|
||||||
|
{
|
||||||
|
id: "risk-1",
|
||||||
|
title: "Prepare the defense walkthrough",
|
||||||
|
body: "Review the live and replay paths before the defense.",
|
||||||
|
severity: "medium",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
output: null,
|
output: null,
|
||||||
trace: null,
|
trace: null,
|
||||||
missingDeploymentMessage: null,
|
missingDeploymentMessage: null,
|
||||||
@@ -34,7 +44,7 @@ const demo = {
|
|||||||
} as unknown as DemoTimelineController;
|
} as unknown as DemoTimelineController;
|
||||||
|
|
||||||
describe("GuidedProductMoment", () => {
|
describe("GuidedProductMoment", () => {
|
||||||
it("makes approval the primary product decision", () => {
|
it("makes approval the primary product decision with factual panels", () => {
|
||||||
render(
|
render(
|
||||||
<GuidedProductMoment
|
<GuidedProductMoment
|
||||||
beat={findBeat("interrupt-evidence", "approval")!}
|
beat={findBeat("interrupt-evidence", "approval")!}
|
||||||
@@ -54,7 +64,11 @@ describe("GuidedProductMoment", () => {
|
|||||||
|
|
||||||
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.getByText(/Run is paused/i)).toBeInTheDocument();
|
expect(screen.getByText(/Run is paused/i)).toBeInTheDocument();
|
||||||
expect(screen.getByRole("button", { name: "Submit" })).toBeEnabled();
|
expect(screen.getByText("Workflow input")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("project-brief.md")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("issue-board.json")).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("group", { name: /operator resume decision/i })).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Output not created yet")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("makes resume operation proof primary on resume beat", () => {
|
it("makes resume operation proof primary on resume beat", () => {
|
||||||
@@ -91,6 +105,6 @@ describe("GuidedProductMoment", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveClass("guided-product-moment");
|
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveClass("guided-product-moment");
|
||||||
expect(screen.getByLabelText("typed interrupt contract")).toHaveAttribute("data-hero", "true");
|
expect(screen.getByRole("group", { name: /operator resume decision/i })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import type { DemoTimelineController } from "../demo/useDemoTimeline.js";
|
import type { DemoTimelineController } from "../demo/useDemoTimeline.js";
|
||||||
import type { DemoApprovalActions } from "./demo-approval-actions.js";
|
import type { DemoApprovalActions } from "./demo-approval-actions.js";
|
||||||
|
import { projectDemoRunFacts } from "./demo-run-facts.js";
|
||||||
import type {
|
import type {
|
||||||
InterruptContractPresentation,
|
InterruptContractPresentation,
|
||||||
OperationPresentation,
|
OperationPresentation,
|
||||||
} from "./demo-workflow-model.js";
|
} from "./demo-workflow-model.js";
|
||||||
import { DemoOutcomePanel } from "./DemoOutcomePanel.js";
|
|
||||||
import { InterruptContractPreview } from "./InterruptContractPreview.js";
|
|
||||||
import { OperationBlock } from "./OperationBlock.js";
|
|
||||||
import type { SceneBeatDefinition } from "./storyboard.js";
|
|
||||||
import { demoBeatLensForBeat } from "./demo-workflow-model.js";
|
import { demoBeatLensForBeat } from "./demo-workflow-model.js";
|
||||||
|
import { InterruptDecisionForm } from "./InterruptDecisionForm.js";
|
||||||
|
import { OperationBlock } from "./OperationBlock.js";
|
||||||
|
import { RunInputFacts, RunOutputFacts, RunTraceFacts } from "./RunFactsPanel.js";
|
||||||
|
import type { SceneBeatDefinition } from "./storyboard.js";
|
||||||
|
|
||||||
export type GuidedProductMomentProps = {
|
export type GuidedProductMomentProps = {
|
||||||
readonly beat: SceneBeatDefinition;
|
readonly beat: SceneBeatDefinition;
|
||||||
@@ -44,6 +45,7 @@ export const GuidedProductMoment = ({
|
|||||||
}: GuidedProductMomentProps) => {
|
}: GuidedProductMomentProps) => {
|
||||||
const moment = momentForBeat(beat.id);
|
const moment = momentForBeat(beat.id);
|
||||||
const lens = demoBeatLensForBeat(beat.id);
|
const lens = demoBeatLensForBeat(beat.id);
|
||||||
|
const facts = projectDemoRunFacts(demo);
|
||||||
const runResume = demo.state.events.find((event) => event.stage === "run_resume");
|
const runResume = demo.state.events.find((event) => event.stage === "run_resume");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -56,27 +58,34 @@ export const GuidedProductMoment = ({
|
|||||||
|
|
||||||
<div className="guided-product-moment__primary">
|
<div className="guided-product-moment__primary">
|
||||||
{moment === "approval" && contract ? (
|
{moment === "approval" && contract ? (
|
||||||
<InterruptContractPreview
|
<div className="guided-product-moment__approval-grid">
|
||||||
contract={contract}
|
<RunInputFacts facts={facts} />
|
||||||
mode="approval"
|
<InterruptDecisionForm
|
||||||
hero
|
interrupt={facts.interrupt}
|
||||||
approvalActions={approvalActions}
|
runId={demo.state.events.find((e) => e.stage === "run_start")?.resultingIds.runId ?? "unknown"}
|
||||||
/>
|
onSubmit={(ids, comment) => approvalActions?.submit(ids, comment)}
|
||||||
|
onCancel={() => approvalActions?.cancel()}
|
||||||
|
terminalOutcome={approvalActions?.state === "submitted" ? "submitted" :
|
||||||
|
approvalActions?.state === "cancelled" ? "cancelled" : undefined}
|
||||||
|
/>
|
||||||
|
<RunOutputFacts facts={facts} />
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{moment === "resume" && runResume ? (
|
{moment === "resume" && runResume ? (
|
||||||
<OperationBlock
|
<div className="guided-product-moment__resume-grid">
|
||||||
event={runResume}
|
<OperationBlock
|
||||||
variant="expanded"
|
event={runResume}
|
||||||
openEvidence={openEvidence}
|
variant="expanded"
|
||||||
/>
|
openEvidence={openEvidence}
|
||||||
|
/>
|
||||||
|
<RunOutputFacts facts={facts} />
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{(moment === "output" || moment === "trace") ? (
|
{moment === "output" ? (
|
||||||
<DemoOutcomePanel
|
<RunOutputFacts facts={facts} />
|
||||||
beatId={beat.id}
|
) : null}
|
||||||
lens={lens}
|
{moment === "trace" ? (
|
||||||
operation={operation}
|
<RunTraceFacts facts={facts} />
|
||||||
contract={contract}
|
|
||||||
/>
|
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -201,9 +201,11 @@ describe("PresentationRoute", () => {
|
|||||||
const { PresentationRoute } = await import("./PresentationRoute.js");
|
const { PresentationRoute } = await import("./PresentationRoute.js");
|
||||||
render(<PresentationRoute />);
|
render(<PresentationRoute />);
|
||||||
|
|
||||||
expect(await screen.findByLabelText("typed interrupt contract")).toBeInTheDocument();
|
expect(await screen.findByText("Workflow input")).toBeInTheDocument();
|
||||||
expect(screen.getByRole("group", { name: /issue review resume/i })).toBeInTheDocument();
|
expect(screen.getByText("project-brief.md")).toBeInTheDocument();
|
||||||
expect(screen.getByText(/Recorded resume payload for this decision/i)).toBeInTheDocument();
|
expect(screen.getByText("issue-board.json")).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("group", { name: /operator resume decision/i })).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Output not created yet")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
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 () => {
|
||||||
@@ -250,7 +252,8 @@ describe("PresentationRoute", () => {
|
|||||||
await user.click(cancelButton);
|
await user.click(cancelButton);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.getByText(/Outcome: cancelled/i)).toBeInTheDocument();
|
expect(screen.queryByRole("button", { name: "Submit" })).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/interrupt-evidence/approval");
|
||||||
expect(screen.queryByLabelText("workflow.runs.resume operation")).not.toBeInTheDocument();
|
expect(screen.queryByLabelText("workflow.runs.resume operation")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ export type DemoApprovalActions = {
|
|||||||
readonly state: DemoApprovalUiState;
|
readonly state: DemoApprovalUiState;
|
||||||
readonly canSubmit: boolean;
|
readonly canSubmit: boolean;
|
||||||
readonly canCancel: boolean;
|
readonly canCancel: boolean;
|
||||||
readonly submit: () => Promise<void>;
|
readonly submit: (
|
||||||
|
selectedIssueIds?: ReadonlyArray<string>,
|
||||||
|
comment?: string,
|
||||||
|
) => Promise<void>;
|
||||||
readonly cancel: () => Promise<void>;
|
readonly cancel: () => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1668,3 +1668,25 @@
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Factual Scene 10 layout grids */
|
||||||
|
.guided-product-moment__approval-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1.4fr 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guided-product-moment__resume-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.4fr 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.guided-product-moment__approval-grid,
|
||||||
|
.guided-product-moment__resume-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user