Task 3: recompose approval, resume, output, and trace beats

This commit is contained in:
lda
2026-07-09 22:17:11 +07:00 Verified
parent 095d40e944
commit f8042cd5f9
3 changed files with 140 additions and 5 deletions
@@ -43,6 +43,15 @@ const demo = {
primeReplayToStage: vi.fn(), primeReplayToStage: vi.fn(),
} as unknown as DemoTimelineController; } as unknown as DemoTimelineController;
const demoWithAppliedCount = (appliedCount: number): DemoTimelineController => ({
...demo,
state: {
...demo.state,
appliedCount,
phase: "completed",
},
});
describe("GuidedProductMoment", () => { describe("GuidedProductMoment", () => {
it("makes approval the primary product decision with factual panels", () => { it("makes approval the primary product decision with factual panels", () => {
render( render(
@@ -107,7 +116,7 @@ describe("GuidedProductMoment", () => {
expect(screen.getByText("Workflow input")).toBeInTheDocument(); expect(screen.getByText("Workflow input")).toBeInTheDocument();
expect(screen.getByRole("group", { name: /operator resume decision/i })).toBeInTheDocument(); expect(screen.getByRole("group", { name: /operator resume decision/i })).toBeInTheDocument();
expect(screen.queryByText("Output not created yet")).not.toBeInTheDocument(); expect(screen.queryByText("Output not created yet")).not.toBeInTheDocument();
expect(screen.getByText(/lda.chat Thesis And Project Readiness Report/i)).toBeInTheDocument(); expect(screen.getAllByText(/lda.chat Thesis And Project Readiness Report/i).length).toBeGreaterThanOrEqual(1);
}); });
it("marks the primary surface for visual hierarchy", () => { it("marks the primary surface for visual hierarchy", () => {
@@ -131,4 +140,82 @@ 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.getByRole("group", { name: /operator resume decision/i })).toBeInTheDocument(); expect(screen.getByRole("group", { name: /operator resume decision/i })).toBeInTheDocument();
}); });
it("approval shows input, interrupt payload, and decision but no output or trace", () => {
render(
<GuidedProductMoment
beat={findBeat("typed-human-boundary", "approval")!}
demo={demo}
contract={contract}
operation={null}
approvalActions={{
state: "ready",
canSubmit: true,
canCancel: true,
submit: vi.fn(async () => {}),
cancel: vi.fn(async () => {}),
}}
openEvidence={vi.fn()}
/>,
);
expect(screen.getByText("Workflow input")).toBeInTheDocument();
expect(screen.getByText("Interrupt payload")).toBeInTheDocument();
expect(screen.getByRole("region", { name: /interrupt report markdown/i })).toBeInTheDocument();
expect(screen.getByRole("group", { name: /operator resume decision/i })).toBeInTheDocument();
expect(screen.queryByText("Output")).not.toBeInTheDocument();
expect(screen.queryByText("Trace frames")).not.toBeInTheDocument();
});
it("resume shows operation, resume payload, and large output report", () => {
const resumedDemo = demoWithAppliedCount(6);
render(
<GuidedProductMoment
beat={findBeat("resume-output-evidence", "resume")!}
demo={resumedDemo}
contract={contract}
operation={resumeOperation}
openEvidence={vi.fn()}
/>,
);
expect(screen.getByLabelText("workflow.runs.resume operation")).toBeInTheDocument();
expect(screen.getByText("Resume decision")).toBeInTheDocument();
expect(screen.getByRole("region", { name: /workflow markdown output/i })).toBeInTheDocument();
});
it("output beat makes the report and created issues primary", () => {
const resumedDemo = demoWithAppliedCount(6);
render(
<GuidedProductMoment
beat={findBeat("resume-output-evidence", "output")!}
demo={resumedDemo}
contract={contract}
operation={null}
openEvidence={vi.fn()}
/>,
);
expect(screen.getByRole("region", { name: /workflow markdown output/i })).toHaveClass("run-facts-scroll-region");
expect(screen.getByText("ISSUE-001")).toBeInTheDocument();
});
it("trace beat shows trace frames instead of the empty fallback after trace is primed", () => {
const tracedDemo = demoWithAppliedCount(5);
render(
<GuidedProductMoment
beat={findBeat("resume-output-evidence", "trace")!}
demo={tracedDemo}
contract={contract}
operation={null}
openEvidence={vi.fn()}
/>,
);
expect(screen.queryByText("No trace frames captured.")).not.toBeInTheDocument();
expect(screen.getByRole("region", { name: /workflow trace frames/i })).toBeInTheDocument();
});
}); });
@@ -8,7 +8,13 @@ import type {
import { demoBeatLensForBeat } from "./demo-workflow-model.js"; import { demoBeatLensForBeat } from "./demo-workflow-model.js";
import { InterruptDecisionForm } from "./InterruptDecisionForm.js"; import { InterruptDecisionForm } from "./InterruptDecisionForm.js";
import { OperationBlock } from "./OperationBlock.js"; import { OperationBlock } from "./OperationBlock.js";
import { RunInputFacts, RunOutputFacts, RunTraceFacts } from "./RunFactsPanel.js"; import {
InterruptPayloadFacts,
RunInputFacts,
RunOutputFacts,
RunResumeFacts,
RunTraceFacts,
} from "./RunFactsPanel.js";
import type { SceneBeatDefinition } from "./storyboard.js"; import type { SceneBeatDefinition } from "./storyboard.js";
export type GuidedProductMomentProps = { export type GuidedProductMomentProps = {
@@ -60,6 +66,7 @@ export const GuidedProductMoment = ({
{moment === "approval" && contract ? ( {moment === "approval" && contract ? (
<div className="guided-product-moment__approval-grid"> <div className="guided-product-moment__approval-grid">
<RunInputFacts facts={facts} /> <RunInputFacts facts={facts} />
<InterruptPayloadFacts facts={facts} />
<InterruptDecisionForm <InterruptDecisionForm
interrupt={facts.interrupt} interrupt={facts.interrupt}
runId={demo.state.events.find((e) => e.stage === "run_start")?.resultingIds.runId ?? "unknown"} runId={demo.state.events.find((e) => e.stage === "run_start")?.resultingIds.runId ?? "unknown"}
@@ -77,14 +84,20 @@ export const GuidedProductMoment = ({
variant="expanded" variant="expanded"
openEvidence={openEvidence} openEvidence={openEvidence}
/> />
<RunOutputFacts facts={facts} /> <RunResumeFacts facts={facts} />
<RunOutputFacts facts={facts} priority="report" />
</div> </div>
) : null} ) : null}
{moment === "output" ? ( {moment === "output" ? (
<RunOutputFacts facts={facts} /> <div className="guided-product-moment__output-grid">
<RunOutputFacts facts={facts} priority="report" />
</div>
) : null} ) : null}
{moment === "trace" ? ( {moment === "trace" ? (
<RunTraceFacts facts={facts} /> <div className="guided-product-moment__trace-grid">
<RunTraceFacts facts={facts} />
<RunOutputFacts facts={facts} priority="summary" />
</div>
) : null} ) : null}
</div> </div>
</section> </section>
@@ -1200,3 +1200,38 @@
max-width: min(62rem, 100%); max-width: min(62rem, 100%);
justify-self: center; justify-self: center;
} }
.guided-product-moment__approval-grid {
display: grid;
grid-template-columns: minmax(13rem, 0.55fr) minmax(0, 1.2fr) minmax(18rem, 0.75fr);
gap: 0.85rem;
min-height: 0;
}
.guided-product-moment__resume-grid {
display: grid;
grid-template-columns: minmax(18rem, 0.85fr) minmax(14rem, 0.45fr) minmax(0, 1fr);
gap: 0.85rem;
min-height: 0;
}
.guided-product-moment__output-grid {
display: grid;
grid-template-columns: minmax(0, 1fr);
min-height: 0;
}
.guided-product-moment__trace-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(17rem, 0.45fr);
gap: 0.85rem;
min-height: 0;
}
@container presentation-canvas (max-width: 1050px) {
.guided-product-moment__approval-grid,
.guided-product-moment__resume-grid,
.guided-product-moment__trace-grid {
grid-template-columns: minmax(0, 1fr);
}
}