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 userEvent from "@testing-library/user-event";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import { loadCanonicalDemoRecording } from "../demo/timeline/replay.js";
|
import { loadCanonicalDemoRecording } from "../demo/timeline/replay.js";
|
||||||
@@ -190,6 +190,26 @@ describe("DemoWorkflowScene", () => {
|
|||||||
expect(screen.getByLabelText("demo outcome proof")).toHaveTextContent("schema-backed");
|
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", () => {
|
it("adds outcome proof to approval, resume, output, and trace beats", () => {
|
||||||
const approval = renderBeat("approval", "interrupt-evidence");
|
const approval = renderBeat("approval", "interrupt-evidence");
|
||||||
expect(screen.getByLabelText("demo outcome proof")).toHaveTextContent("schema-backed");
|
expect(screen.getByLabelText("demo outcome proof")).toHaveTextContent("schema-backed");
|
||||||
|
|||||||
@@ -54,9 +54,10 @@ export const DemoWorkflowScene = ({
|
|||||||
openEvidence,
|
openEvidence,
|
||||||
}: DemoWorkflowSceneProps) => {
|
}: DemoWorkflowSceneProps) => {
|
||||||
const runStart = findEvent(demo, "run_start");
|
const runStart = findEvent(demo, "run_start");
|
||||||
|
const runResume = findEvent(demo, "run_resume");
|
||||||
const currentStage = operationStageByBeat[beat.id];
|
const currentStage = operationStageByBeat[beat.id];
|
||||||
const currentEvent = currentStage ? findEvent(demo, currentStage) : null;
|
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 execution = graphExecutionForBeat(beat.id);
|
||||||
const layout = layoutForBeat(beat.id);
|
const layout = layoutForBeat(beat.id);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { m } from "motion/react";
|
import { m } from "motion/react";
|
||||||
import type { InterruptContractPresentation } from "./demo-workflow-model.js";
|
import type { InterruptContractPresentation } from "./demo-workflow-model.js";
|
||||||
|
import { SchemaApprovalSurface } from "./approval/SchemaApprovalSurface.js";
|
||||||
import { formatJson } from "./format.js";
|
import { formatJson } from "./format.js";
|
||||||
|
|
||||||
type InterruptContractPreviewProps = {
|
type InterruptContractPreviewProps = {
|
||||||
@@ -36,9 +37,19 @@ export const InterruptContractPreview = ({
|
|||||||
<dd>{contract.outcomes.join(" / ")}</dd>
|
<dd>{contract.outcomes.join(" / ")}</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
|
{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">
|
<div className="interrupt-contract-preview__schema">
|
||||||
<span>Resume schema</span>
|
<span>Resume schema</span>
|
||||||
<pre><code>{formatJson(contract.resumeSchema)}</code></pre>
|
<pre><code>{formatJson(contract.resumeSchema)}</code></pre>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</m.aside>
|
</m.aside>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,9 +4,15 @@ import type { DemoEvent } from "../demo/timeline/models.js";
|
|||||||
const InterruptProjectionSchema = v.looseObject({
|
const InterruptProjectionSchema = v.looseObject({
|
||||||
kind: v.string(),
|
kind: v.string(),
|
||||||
outcomes: v.optional(v.array(v.string())),
|
outcomes: v.optional(v.array(v.string())),
|
||||||
|
request_schema: v.optional(v.unknown()),
|
||||||
resume_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({
|
const RunInterpretationSchema = v.looseObject({
|
||||||
status: v.optional(v.string()),
|
status: v.optional(v.string()),
|
||||||
interrupt: v.optional(v.nullable(InterruptProjectionSchema)),
|
interrupt: v.optional(v.nullable(InterruptProjectionSchema)),
|
||||||
@@ -31,7 +37,10 @@ export type OperationPresentation = {
|
|||||||
export type InterruptContractPresentation = {
|
export type InterruptContractPresentation = {
|
||||||
readonly kind: string;
|
readonly kind: string;
|
||||||
readonly outcomes: ReadonlyArray<string>;
|
readonly outcomes: ReadonlyArray<string>;
|
||||||
|
readonly requestSchema: unknown;
|
||||||
readonly resumeSchema: unknown;
|
readonly resumeSchema: unknown;
|
||||||
|
readonly resumePayloadPreview: unknown;
|
||||||
|
readonly resumeOutcome: string | null;
|
||||||
readonly runId: string | null;
|
readonly runId: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -148,13 +157,19 @@ export const projectOperationPresentation = (
|
|||||||
|
|
||||||
export const projectInterruptContract = (
|
export const projectInterruptContract = (
|
||||||
event: DemoEvent,
|
event: DemoEvent,
|
||||||
|
resumeEvent?: DemoEvent | null,
|
||||||
): InterruptContractPresentation | null => {
|
): InterruptContractPresentation | null => {
|
||||||
const interrupt = decodeInterpretation(event)?.interrupt;
|
const interrupt = decodeInterpretation(event)?.interrupt;
|
||||||
if (!interrupt || !interrupt.outcomes || interrupt.resume_schema === undefined) return null;
|
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 {
|
return {
|
||||||
kind: interrupt.kind,
|
kind: interrupt.kind,
|
||||||
outcomes: interrupt.outcomes,
|
outcomes: interrupt.outcomes,
|
||||||
|
requestSchema: interrupt.request_schema ?? null,
|
||||||
resumeSchema: interrupt.resume_schema,
|
resumeSchema: interrupt.resume_schema,
|
||||||
|
resumePayloadPreview: resumeParams?.resume_payload ?? null,
|
||||||
|
resumeOutcome: resumeParams?.resume_outcome ?? null,
|
||||||
runId: event.resultingIds.runId,
|
runId: event.resultingIds.runId,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user