feat: show schema approval in demo interrupt
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { cleanup, render, screen, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { loadCanonicalDemoRecording } from "../demo/timeline/replay.js";
|
||||
@@ -190,6 +190,26 @@ describe("DemoWorkflowScene", () => {
|
||||
expect(screen.getByLabelText("demo outcome proof")).toHaveTextContent("schema-backed");
|
||||
});
|
||||
|
||||
it("shows a schema approval surface for the approval beat instead of raw schema as the primary visual", () => {
|
||||
renderBeat("approval", "interrupt-evidence");
|
||||
|
||||
const approval = screen.getByRole("group", { name: /issue_review resume/i });
|
||||
expect(within(approval).getByText("Schema-backed decision")).toBeInTheDocument();
|
||||
expect(within(approval).getByText("selected_issue_ids")).toBeInTheDocument();
|
||||
expect(within(approval).getByText("[\"risk-1\"]")).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", () => {
|
||||
renderBeat("interrupt");
|
||||
expect(screen.getByText("Resume schema")).toBeInTheDocument();
|
||||
cleanup();
|
||||
|
||||
renderBeat("approval", "interrupt-evidence");
|
||||
expect(screen.queryByText("Resume schema")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("adds outcome proof to approval, resume, output, and trace beats", () => {
|
||||
const approval = renderBeat("approval", "interrupt-evidence");
|
||||
expect(screen.getByLabelText("demo outcome proof")).toHaveTextContent("schema-backed");
|
||||
|
||||
@@ -54,9 +54,10 @@ export const DemoWorkflowScene = ({
|
||||
openEvidence,
|
||||
}: DemoWorkflowSceneProps) => {
|
||||
const runStart = findEvent(demo, "run_start");
|
||||
const runResume = findEvent(demo, "run_resume");
|
||||
const currentStage = operationStageByBeat[beat.id];
|
||||
const currentEvent = currentStage ? findEvent(demo, currentStage) : null;
|
||||
const contract = runStart ? projectInterruptContract(runStart) : null;
|
||||
const contract = runStart ? projectInterruptContract(runStart, runResume) : null;
|
||||
const execution = graphExecutionForBeat(beat.id);
|
||||
const layout = layoutForBeat(beat.id);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { m } from "motion/react";
|
||||
import type { InterruptContractPresentation } from "./demo-workflow-model.js";
|
||||
import { SchemaApprovalSurface } from "./approval/SchemaApprovalSurface.js";
|
||||
import { formatJson } from "./format.js";
|
||||
|
||||
type InterruptContractPreviewProps = {
|
||||
@@ -36,9 +37,19 @@ export const InterruptContractPreview = ({
|
||||
<dd>{contract.outcomes.join(" / ")}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div className="interrupt-contract-preview__schema">
|
||||
<span>Resume schema</span>
|
||||
<pre><code>{formatJson(contract.resumeSchema)}</code></pre>
|
||||
</div>
|
||||
{mode === "approval" ? (
|
||||
<SchemaApprovalSurface
|
||||
title={`${contract.kind} resume`}
|
||||
schema={contract.resumeSchema}
|
||||
payload={contract.resumePayloadPreview}
|
||||
outcomes={contract.outcomes}
|
||||
runId={contract.runId}
|
||||
/>
|
||||
) : (
|
||||
<div className="interrupt-contract-preview__schema">
|
||||
<span>Resume schema</span>
|
||||
<pre><code>{formatJson(contract.resumeSchema)}</code></pre>
|
||||
</div>
|
||||
)}
|
||||
</m.aside>
|
||||
);
|
||||
|
||||
@@ -4,9 +4,15 @@ import type { DemoEvent } from "../demo/timeline/models.js";
|
||||
const InterruptProjectionSchema = v.looseObject({
|
||||
kind: v.string(),
|
||||
outcomes: v.optional(v.array(v.string())),
|
||||
request_schema: v.optional(v.unknown()),
|
||||
resume_schema: v.optional(v.unknown()),
|
||||
});
|
||||
|
||||
const ResumeParamsSchema = v.looseObject({
|
||||
resume_payload: v.optional(v.unknown()),
|
||||
resume_outcome: v.optional(v.string()),
|
||||
});
|
||||
|
||||
const RunInterpretationSchema = v.looseObject({
|
||||
status: v.optional(v.string()),
|
||||
interrupt: v.optional(v.nullable(InterruptProjectionSchema)),
|
||||
@@ -31,7 +37,10 @@ export type OperationPresentation = {
|
||||
export type InterruptContractPresentation = {
|
||||
readonly kind: string;
|
||||
readonly outcomes: ReadonlyArray<string>;
|
||||
readonly requestSchema: unknown;
|
||||
readonly resumeSchema: unknown;
|
||||
readonly resumePayloadPreview: unknown;
|
||||
readonly resumeOutcome: string | null;
|
||||
readonly runId: string | null;
|
||||
};
|
||||
|
||||
@@ -148,13 +157,19 @@ export const projectOperationPresentation = (
|
||||
|
||||
export const projectInterruptContract = (
|
||||
event: DemoEvent,
|
||||
resumeEvent?: DemoEvent | null,
|
||||
): InterruptContractPresentation | null => {
|
||||
const interrupt = decodeInterpretation(event)?.interrupt;
|
||||
if (!interrupt || !interrupt.outcomes || interrupt.resume_schema === undefined) return null;
|
||||
const decodedResumeParams = v.safeParse(ResumeParamsSchema, resumeEvent?.params);
|
||||
const resumeParams = decodedResumeParams.success ? decodedResumeParams.output : null;
|
||||
return {
|
||||
kind: interrupt.kind,
|
||||
outcomes: interrupt.outcomes,
|
||||
requestSchema: interrupt.request_schema ?? null,
|
||||
resumeSchema: interrupt.resume_schema,
|
||||
resumePayloadPreview: resumeParams?.resume_payload ?? null,
|
||||
resumeOutcome: resumeParams?.resume_outcome ?? null,
|
||||
runId: event.resultingIds.runId,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user