feat: add presentation evidence receipt

This commit is contained in:
lda
2026-07-06 04:00:26 +07:00 Verified
parent baf26156e5
commit 5f3fccec8c
6 changed files with 311 additions and 0 deletions
@@ -0,0 +1,37 @@
import { cleanup, render, screen, within } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { EvidenceRecord } from "../app/state.js";
import { PresentationFooter } from "./PresentationFooter.js";
afterEach(() => cleanup());
describe("PresentationFooter", () => {
it("combines scene progress and evidence provenance", () => {
const evidence: EvidenceRecord = {
id: "trace",
operation: "workflow.runs.trace",
label: "Inspect trace",
equivalentCli: "uv run wf run trace run_demo",
request: { run_id: "run_demo" },
response: { result: { status: "completed" } },
durationMs: 34,
};
render(
<PresentationFooter
location={{
kind: "main",
sceneId: "architecture",
beatId: "runtime",
focusPath: [],
}}
evidence={[evidence]}
showEvidenceReceipt
inspectEvidence={vi.fn()}
/>,
);
const footer = screen.getByRole("contentinfo", { name: /presentation footer/i });
expect(within(footer).getByText("6 / 12")).toBeInTheDocument();
expect(within(footer).getByText("3 / 4")).toBeInTheDocument();
expect(within(footer).getByText(/workflow\.runs\.trace/)).toBeInTheDocument();
});
});
@@ -0,0 +1,27 @@
import type { EvidenceRecord } from "../app/state.js";
import { SceneProgress } from "./SceneProgress.js";
import { EvidenceReceipt } from "./evidence/EvidenceReceipt.js";
import type { MainLocation } from "./storyboard.js";
type PresentationFooterProps = {
readonly location: MainLocation;
readonly evidence: readonly EvidenceRecord[];
readonly showEvidenceReceipt: boolean;
readonly inspectEvidence: () => void;
};
export const PresentationFooter = ({
location,
evidence,
showEvidenceReceipt,
inspectEvidence,
}: PresentationFooterProps) => (
<footer className="presentation-footer" aria-label="presentation footer">
<SceneProgress location={location} />
<EvidenceReceipt
records={evidence}
visible={showEvidenceReceipt}
onInspect={inspectEvidence}
/>
</footer>
);
@@ -0,0 +1,40 @@
import { cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { EvidenceRecord } from "../../app/state.js";
import { EvidenceReceipt } from "./EvidenceReceipt.js";
afterEach(() => cleanup());
const record: EvidenceRecord = {
id: "run-start",
operation: "workflow.runs.start",
label: "Start run",
equivalentCli: "uv run wf run start demo.default",
request: { deployment_id: "demo.default" },
response: { result: { status: "interrupted", run_id: "run_demo" } },
durationMs: 88,
};
describe("EvidenceReceipt", () => {
it("shows latest operation, status, count, and opens inspection", async () => {
const user = userEvent.setup();
const inspect = vi.fn();
render(<EvidenceReceipt records={[record]} visible onInspect={inspect} />);
expect(screen.getByText(/workflow\.runs\.start/)).toBeInTheDocument();
expect(screen.getByText("interrupted")).toBeInTheDocument();
expect(screen.getByText(/1 record/i)).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: /inspect evidence/i }));
expect(inspect).toHaveBeenCalledOnce();
});
it("renders no receipt when the beat keeps evidence hidden", () => {
render(<EvidenceReceipt records={[record]} visible={false} onInspect={vi.fn()} />);
expect(screen.queryByRole("button", { name: /inspect evidence/i })).not.toBeInTheDocument();
});
it("disables inspection when evidence is unavailable", () => {
render(<EvidenceReceipt records={[]} visible onInspect={vi.fn()} />);
expect(screen.getByRole("button", { name: /inspect evidence/i })).toBeDisabled();
});
});
@@ -0,0 +1,32 @@
import type { EvidenceRecord } from "../../app/state.js";
import { projectEvidenceReceipt } from "./evidence-model.js";
type EvidenceReceiptProps = {
readonly records: readonly EvidenceRecord[];
readonly visible: boolean;
readonly onInspect: () => void;
};
export const EvidenceReceipt = ({
records,
visible,
onInspect,
}: EvidenceReceiptProps) => {
if (!visible) return null;
const receipt = projectEvidenceReceipt(records);
const countLabel = `${receipt.recordCount} ${receipt.recordCount === 1 ? "record" : "records"}`;
return (
<button
type="button"
className="evidence-receipt"
aria-label="Inspect evidence"
disabled={!receipt.available}
onClick={onInspect}
>
<span>Evidence: {receipt.operation}</span>
{receipt.status && <span data-status={receipt.status}>{receipt.status}</span>}
<span>{countLabel}</span>
{receipt.available && <span aria-hidden="true">Inspect</span>}
</button>
);
};
@@ -0,0 +1,68 @@
import { describe, expect, it } from "vitest";
import type { EvidenceRecord } from "../../app/state.js";
import {
formatEvidenceValue,
projectEvidenceDetail,
projectEvidenceReceipt,
} from "./evidence-model.js";
const record = (response: unknown): EvidenceRecord => ({
id: "run-start",
operation: "workflow.runs.start",
label: "Start run",
equivalentCli: "uv run wf run start demo.default",
request: { deployment_id: "demo.default" },
response,
durationMs: 88,
});
describe("evidence projection", () => {
it("uses the latest record and extracts a nested result status", () => {
const model = projectEvidenceReceipt([
record({ result: { status: "interrupted" } }),
{ ...record({ result: { status: "completed" } }), id: "trace", operation: "workflow.runs.trace" },
]);
expect(model).toMatchObject({
available: true,
operation: "workflow.runs.trace",
status: "completed",
recordCount: 2,
});
});
it("returns an unavailable receipt for no records", () => {
expect(projectEvidenceReceipt([])).toEqual({
available: false,
operation: "Evidence unavailable",
status: null,
recordCount: 0,
});
});
it("projects run and deployment identifiers without requiring status", () => {
expect(projectEvidenceDetail({
...record({ result: { run_id: "run_demo" } }),
request: { deployment_id: "demo.default" },
})).toMatchObject({
status: null,
durationMs: 88,
deploymentId: "demo.default",
runId: "run_demo",
equivalentCli: "uv run wf run start demo.default",
});
});
it("returns bounded text and a note when raw evidence is not JSON serializable", () => {
const cyclic: Record<string, unknown> = {};
cyclic.self = cyclic;
const formatted = formatEvidenceValue(cyclic);
expect(formatted.text).toBe("[object Object]");
expect(formatted.note).toMatch(/could not format as json/i);
});
it("truncates oversized evidence before it reaches the inspector", () => {
const formatted = formatEvidenceValue("x".repeat(120_000));
expect(formatted.text.length).toBeLessThanOrEqual(100_003);
expect(formatted.note).toMatch(/truncated/i);
});
});
@@ -0,0 +1,107 @@
import type { EvidenceRecord } from "../../app/state.js";
export type EvidenceReceiptModel = {
readonly available: boolean;
readonly operation: string;
readonly status: string | null;
readonly recordCount: number;
};
export type EvidenceDetailModel = EvidenceReceiptModel & {
readonly id: string;
readonly label: string;
readonly equivalentCli: string;
readonly durationMs: number;
readonly deploymentId: string | null;
readonly runId: string | null;
readonly request: FormattedEvidenceValue;
readonly response: FormattedEvidenceValue;
};
export type FormattedEvidenceValue = {
readonly text: string;
readonly note: string | null;
};
const MAX_EVIDENCE_TEXT_LENGTH = 100_000;
// Guarded object helper: reads nested properties without assuming the full
// response shape or casting the complete payload.
const objectValue = (value: unknown): Readonly<Record<string, unknown>> | null =>
typeof value === "object" && value !== null && !Array.isArray(value)
? value as Readonly<Record<string, unknown>>
: null;
const responseStatus = (response: unknown): string | null => {
const result = objectValue(objectValue(response)?.result);
return typeof result?.status === "string" ? result.status : null;
};
const stringField = (
value: Readonly<Record<string, unknown>> | null,
field: string,
): string | null => {
const candidate = value?.[field];
return typeof candidate === "string" ? candidate : null;
};
const boundEvidenceText = (
text: string,
note: string | null,
): FormattedEvidenceValue => {
if (text.length <= MAX_EVIDENCE_TEXT_LENGTH) return { text, note };
const truncation = `Evidence truncated to ${MAX_EVIDENCE_TEXT_LENGTH} characters.`;
return {
text: `${text.slice(0, MAX_EVIDENCE_TEXT_LENGTH)}...`,
note: note ? `${note} ${truncation}` : truncation,
};
};
export const formatEvidenceValue = (value: unknown): FormattedEvidenceValue => {
try {
const encoded = JSON.stringify(value, null, 2);
return boundEvidenceText(encoded ?? String(value), null);
} catch {
let text: string;
try {
text = String(value);
} catch {
text = "[Unprintable evidence]";
}
return boundEvidenceText(
text,
"Could not format as JSON; showing a bounded text representation.",
);
}
};
export const projectEvidenceReceipt = (
records: readonly EvidenceRecord[],
): EvidenceReceiptModel => {
const latest = records.at(-1);
if (!latest) {
return { available: false, operation: "Evidence unavailable", status: null, recordCount: 0 };
}
return {
available: true,
operation: latest.operation,
status: responseStatus(latest.response),
recordCount: records.length,
};
};
export const projectEvidenceDetail = (record: EvidenceRecord): EvidenceDetailModel => {
const request = objectValue(record.request);
const result = objectValue(objectValue(record.response)?.result);
return {
...projectEvidenceReceipt([record]),
id: record.id,
label: record.label,
equivalentCli: record.equivalentCli,
durationMs: record.durationMs,
deploymentId: stringField(result, "deployment_id") ?? stringField(request, "deployment_id"),
runId: stringField(result, "run_id") ?? stringField(request, "run_id"),
request: formatEvidenceValue(record.request),
response: formatEvidenceValue(record.response),
};
};