feat: tighten scene 11-12 factual evidence

This commit is contained in:
lda
2026-07-11 22:06:24 +07:00 Verified
parent 4b75948450
commit 68ac64724c
12 changed files with 262 additions and 33 deletions
@@ -73,6 +73,7 @@ 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-primary-surface", "interrupt-approval");
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-approval-focus", "decision");
expect(screen.getByText(/Run is paused/i)).toBeInTheDocument();
expect(screen.getByText("Workflow input")).toBeInTheDocument();
expect(screen.getByText("project-brief.md")).toBeInTheDocument();
@@ -94,6 +95,7 @@ describe("GuidedProductMoment", () => {
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "resume");
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-primary-surface", "resume-output");
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-continuation-focus", "output");
expect(screen.getByLabelText("workflow.runs.resume operation")).toBeInTheDocument();
});
@@ -117,6 +119,7 @@ describe("GuidedProductMoment", () => {
expect(screen.getByText("Workflow input")).toBeInTheDocument();
expect(screen.getByRole("group", { name: /operator resume decision/i })).toBeInTheDocument();
expect(screen.getByRole("region", { name: /operator resume decision/i })).toBeInTheDocument();
expect(screen.queryByText("Output not created yet")).not.toBeInTheDocument();
expect(screen.getAllByText(/lda.chat Thesis And Project Readiness Report/i).length).toBeGreaterThanOrEqual(1);
});
@@ -191,6 +194,7 @@ describe("GuidedProductMoment", () => {
expect(screen.getByRole("group", { name: /operator resume decision/i })).toBeInTheDocument();
expect(screen.queryByText("Output")).not.toBeInTheDocument();
expect(screen.queryByText("Trace frames")).not.toBeInTheDocument();
expect(document.querySelector(".interrupt-decision-form__report-preview")).toBeNull();
});
it("resume makes output primary and operation/resume payload supporting", () => {
@@ -260,9 +264,11 @@ describe("GuidedProductMoment", () => {
);
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-support-surface", "output-summary");
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-continuation-focus", "trace");
expect(screen.getByRole("region", { name: /workflow trace proof/i })).toBeInTheDocument();
expect(screen.getByRole("region", { name: /workflow output summary/i })).toHaveAttribute("data-output-priority", "summary");
expect(screen.queryByText("No trace frames captured.")).not.toBeInTheDocument();
expect(document.querySelectorAll(".run-trace-frame")).toHaveLength(3);
});
it("trace beat shows trace frames instead of the empty fallback after trace is primed", () => {
@@ -75,6 +75,8 @@ export const GuidedProductMoment = ({
data-moment={moment}
data-primary-surface={hierarchy.primary}
data-support-surface={hierarchy.support}
data-approval-focus={moment === "approval" ? "decision" : undefined}
data-continuation-focus={moment === "resume" ? "output" : moment === "trace" ? "trace" : undefined}
>
<header className="guided-product-moment__header">
<span>{lens.eyebrow}</span>
@@ -89,14 +91,17 @@ export const GuidedProductMoment = ({
<RunInputFacts facts={facts} density="compact" />
</aside>
<InterruptPayloadFacts facts={facts} priority="primary" />
<InterruptDecisionForm
interrupt={facts.interrupt}
runId={demo.state.events.find((e) => e.stage === "run_start")?.resultingIds.runId ?? "unknown"}
onSubmit={approvalActions?.canSubmit ? (ids, comment) => approvalActions.submit(ids, comment) : undefined}
onCancel={approvalActions?.canCancel ? () => approvalActions.cancel() : undefined}
terminalOutcome={approvalActions?.state === "submitted" ? "submitted" :
approvalActions?.state === "cancelled" ? "cancelled" : undefined}
/>
<div className="guided-product-moment__decision-column" role="region" aria-label="operator resume decision">
<InterruptDecisionForm
interrupt={facts.interrupt}
runId={demo.state.events.find((e) => e.stage === "run_start")?.resultingIds.runId ?? "unknown"}
onSubmit={approvalActions?.canSubmit ? (ids, comment) => approvalActions.submit(ids, comment) : undefined}
onCancel={approvalActions?.canCancel ? () => approvalActions.cancel() : undefined}
terminalOutcome={approvalActions?.state === "submitted" ? "submitted" :
approvalActions?.state === "cancelled" ? "cancelled" : undefined}
showReportPreview={false}
/>
</div>
</div>
) : null}
{moment === "resume" && runResume ? (
@@ -127,6 +127,21 @@ describe("InterruptDecisionForm", () => {
expect(screen.queryByRole("button", { name: /cancel/i })).toBeFalsy();
});
it("can leave report evidence to the surrounding interrupt panel", () => {
render(
<InterruptDecisionForm
interrupt={interrupt}
runId="run_recorded_lda_report"
onSubmit={vi.fn()}
onCancel={vi.fn()}
showReportPreview={false}
/>,
);
expect(screen.queryByText(/The workflow substrate is ready/i)).not.toBeInTheDocument();
expect(screen.getAllByRole("checkbox")).toHaveLength(2);
});
it("shows terminal outcome label when state is cancelled", () => {
render(
<InterruptDecisionForm
@@ -7,6 +7,7 @@ type InterruptDecisionFormProps = {
readonly onSubmit?: ((selectedIssueIds: ReadonlyArray<string>, comment: string) => void) | undefined;
readonly onCancel?: (() => void) | undefined;
readonly terminalOutcome?: "submitted" | "cancelled" | undefined;
readonly showReportPreview?: boolean;
};
export const InterruptDecisionForm = ({
@@ -15,6 +16,7 @@ export const InterruptDecisionForm = ({
onSubmit,
onCancel,
terminalOutcome,
showReportPreview = true,
}: InterruptDecisionFormProps) => {
const [selectedIds, setSelectedIds] = useState<Set<string>>(() =>
new Set(interrupt.proposedIssues.map((issue) => issue.id)),
@@ -71,7 +73,7 @@ export const InterruptDecisionForm = ({
<dd>{interrupt.outcomes.join(", ")}</dd>
</dl>
{interrupt.reportMarkdownPreview && (
{showReportPreview && interrupt.reportMarkdownPreview && (
<pre className="interrupt-decision-form__report-preview">
{interrupt.reportMarkdownPreview}
</pre>
@@ -1,6 +1,7 @@
import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { callOperation } from "../connection/api.js";
vi.mock("../connection/api.js", () => ({
callOperation: vi.fn().mockResolvedValue({
@@ -20,6 +21,21 @@ class MockResizeObserver {
disconnect() {}
}
const mockedCallOperation = vi.mocked(callOperation);
const healthyResponse = {
ok: true as const,
operation: "workflow.health" as const,
label: "Health",
interpreted: { status: "ok" },
exchange: { request: {}, response: {} },
equivalentCli: "uv run wf status",
durationMs: 2,
};
beforeAll(() => {
mockedCallOperation.mockResolvedValue(healthyResponse);
});
beforeAll(() => {
globalThis.ResizeObserver = MockResizeObserver as unknown as typeof ResizeObserver;
globalThis.DOMRect = {
@@ -50,6 +66,8 @@ const graphNodeByLabel = (label: RegExp): HTMLElement => {
afterEach(() => {
cleanup();
mockedCallOperation.mockReset();
mockedCallOperation.mockResolvedValue(healthyResponse);
window.sessionStorage.clear();
window.location.hash = "";
});
@@ -218,6 +236,21 @@ describe("PresentationRoute", () => {
expect(screen.queryByRole("button", { name: "Submit" })).not.toBeInTheDocument();
});
it("switches to replay evidence when a configured target fails health", async () => {
mockedCallOperation.mockResolvedValue({
ok: false as const,
error: { code: "upstream_unreachable", message: "connection refused" },
exchange: { request: {}, response: {} },
});
window.sessionStorage.setItem("lda.workflowConsole.target", "http://127.0.0.1:8766/rpc");
window.location.hash = "#scene/resume-output-evidence/resume";
const { PresentationRoute } = await import("./PresentationRoute.js");
render(<PresentationRoute />);
expect(await screen.findByText(/Replay fallback/i)).toBeInTheDocument();
expect(await screen.findByRole("region", { name: /workflow output report/i })).toBeInTheDocument();
});
it("updates the chat intro after the live health probe succeeds", async () => {
window.sessionStorage.setItem("lda.workflowConsole.target", "http://127.0.0.1:8765/rpc");
const { PresentationRoute } = await import("./PresentationRoute.js");
@@ -97,9 +97,12 @@ export const PresentationRoute = () => {
useEffect(() => {
if (demo.state.phase !== "ready") return;
// The resolved target owns the initial mode: a healthy loopback target must
// remain live, while an invalid target starts from the offline recording.
if (demo.state.mode !== presentationTarget.mode) demo.setMode(presentationTarget.mode);
}, [demo.setMode, demo.state.mode, demo.state.phase, presentationTarget.mode]);
// remain live, while an invalid or unreachable target starts from the
// offline recording. The health hook is the point where an HTTP target
// becomes known to be unreachable; URL shape alone is not enough.
const desiredMode = targetStatus.kind === "failed" ? "replay" : presentationTarget.mode;
if (demo.state.mode !== desiredMode) demo.setMode(desiredMode);
}, [demo.setMode, demo.state.mode, demo.state.phase, presentationTarget.mode, targetStatus.kind]);
useEffect(() => {
if (demo.state.phase === "ready" && demo.state.mode === "replay") {
@@ -234,6 +234,11 @@ describe("RunTraceFacts", () => {
expect(screen.getByText("review_issues")).toBeDefined();
expect(screen.getByText("2 captured")).toBeInTheDocument();
expect(screen.getAllByText("captured as empty object").length).toBeGreaterThanOrEqual(2);
expect(screen.getAllByText("Resolved input").length).toBe(2);
expect(screen.getAllByText("captured as empty object")[0]!.closest(".run-trace-frame__fact")).toHaveAttribute(
"data-value-kind",
"empty-object",
);
});
it("renders trace frames inside a scrollable list", () => {
@@ -244,4 +249,18 @@ describe("RunTraceFacts", () => {
expect(screen.getByRole("region", { name: /workflow trace frames/i })).toHaveClass("run-facts-scroll-region");
expect(screen.getByText("node-7")).toBeInTheDocument();
});
it("keeps repeated node IDs as separate trace frames", () => {
const traceFacts = makeTraceFacts(2).trace;
const repeatedTraceFacts: DemoRunFacts = {
...baseFacts,
trace: {
frames: traceFacts.frames.map((frame) => ({ ...frame, nodeId: "review_issues" })),
},
};
render(<RunTraceFacts facts={repeatedTraceFacts} />);
expect(document.querySelectorAll(".run-trace-frame")).toHaveLength(2);
});
});
@@ -1,5 +1,5 @@
import { Fragment } from "react";
import type { DemoRunFacts } from "./demo-run-facts.js";
import { factValueKind, type DemoRunFacts } from "./demo-run-facts.js";
type RunInputFactsProps = {
readonly facts: DemoRunFacts;
@@ -138,6 +138,13 @@ type RunTraceFactsProps = {
readonly facts: DemoRunFacts;
};
const TraceFact = ({ label, value }: { readonly label: string; readonly value: string }) => (
<div className="run-trace-frame__fact" data-value-kind={factValueKind(value)}>
<dt>{label}</dt>
<dd><code>{value}</code></dd>
</div>
);
export const RunTraceFacts = ({ facts }: RunTraceFactsProps) => (
<div className="run-facts-card run-trace-facts" role="region" aria-label="workflow trace proof">
<h3>
@@ -148,18 +155,17 @@ export const RunTraceFacts = ({ facts }: RunTraceFactsProps) => (
) : (
<div className="run-facts-scroll-region run-facts-scroll-region--trace" role="region" aria-label="workflow trace frames">
<ul className="run-facts-list">
{facts.trace.frames.map((frame) => (
<li key={frame.nodeId} className="run-trace-frame">
<strong>{frame.nodeId}</strong>
<span className="run-trace-step-type">{frame.stepType}</span>
<span className="run-trace-outcome">{frame.outcome}</span>
<dl className="run-facts-dl">
<dt>Resolved input</dt>
<dd>{frame.resolvedInputLabel}</dd>
<dt>Output</dt>
<dd>{frame.outputLabel}</dd>
<dt>State changes</dt>
<dd>{frame.stateChangesLabel}</dd>
{facts.trace.frames.map((frame, index) => (
<li key={`${frame.nodeId}-${index}`} className="run-trace-frame" data-trace-node={frame.nodeId}>
<header className="run-trace-frame__header">
<strong>{frame.nodeId}</strong>
<span className="run-trace-step-type">{frame.stepType}</span>
<span className="run-trace-outcome">{frame.outcome}</span>
</header>
<dl className="run-trace-frame__facts">
<TraceFact label="Resolved input" value={frame.resolvedInputLabel} />
<TraceFact label="Output" value={frame.outputLabel} />
<TraceFact label="State changes" value={frame.stateChangesLabel} />
</dl>
</li>
))}
@@ -2,7 +2,7 @@ import { describe, expect, it, vi } from "vitest";
import type { DemoTimelineController } from "../demo/useDemoTimeline.js";
import { initialDemoTimelineState } from "../demo/timeline/reducer.js";
import { loadCanonicalDemoRecording } from "../demo/timeline/replay.js";
import { formatFactValue, projectDemoRunFacts } from "./demo-run-facts.js";
import { factValueKind, formatFactValue, projectDemoRunFacts } from "./demo-run-facts.js";
const controller = (overrides: Partial<DemoTimelineController> = {}): DemoTimelineController => {
const recording = loadCanonicalDemoRecording();
@@ -189,4 +189,10 @@ describe("demo-run-facts", () => {
expect(formatFactValue({}, "not captured in this recording")).toBe("captured as empty object");
expect(formatFactValue(undefined, "not captured in this recording")).toBe("not captured in this recording");
});
it("classifies captured values for compact trace rendering", () => {
expect(factValueKind("captured as empty object")).toBe("empty-object");
expect(factValueKind("not captured in this recording")).toBe("missing");
expect(factValueKind('{"documents":3}')).toBe("value");
});
});
@@ -52,6 +52,8 @@ export type RunFactsTrace = {
readonly frames: ReadonlyArray<RunFactsTraceFrame>;
};
export type FactValueKind = "value" | "empty-object" | "missing";
export type DemoRunFacts = {
readonly input: RunFactsInput;
readonly interrupt: RunFactsInterrupt;
@@ -109,6 +111,12 @@ export const formatFactValue = (value: unknown, absentLabel: string): string =>
return JSON.stringify(value);
};
export const factValueKind = (value: string): FactValueKind => {
if (value === EMPTY_OBJECT_LABEL) return "empty-object";
if (value === "not captured in this recording") return "missing";
return "value";
};
const findEvent = (
events: ReadonlyArray<DemoEvent>,
stage: DemoEventStage,
@@ -2514,6 +2514,7 @@
display: flex;
gap: 0.8rem;
align-items: baseline;
min-width: 0;
margin-bottom: 0.8rem;
}
@@ -2526,6 +2527,10 @@
}
.interrupt-decision-form__run-id {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font: 0.82rem/1.4 var(--font-mono);
color: color-mix(in srgb, var(--text-primary), white 20%);
}
@@ -2733,7 +2738,7 @@
}
.guided-product-moment[data-moment="approval"] .interrupt-decision-form__meta {
grid-template-columns: auto minmax(0, 1fr) auto minmax(0, 1fr);
grid-template-columns: auto minmax(0, 1fr);
gap: 0.15rem 0.45rem;
margin-bottom: 0.45rem;
font-size: 0.74rem;
@@ -1286,16 +1286,64 @@
.guided-product-moment__approval-grid {
display: grid;
grid-template-columns: minmax(12.5rem, 0.42fr) minmax(0, 1.25fr) minmax(20rem, 0.62fr);
grid-template-columns: 14rem minmax(0, 1.45fr) minmax(18rem, 0.78fr);
gap: 0.85rem;
min-height: 0;
}
.guided-product-moment__approval-grid > * {
min-width: 0;
}
.guided-product-moment__input-rail {
min-width: 0;
min-height: 0;
}
.guided-product-moment__decision-column {
display: grid;
min-width: 0;
min-height: 0;
}
.guided-product-moment[data-approval-focus="decision"] .guided-product-moment__decision-column {
border-left: 1px solid color-mix(in srgb, var(--accent-cyan) 36%, transparent);
padding-left: 0.85rem;
}
.guided-product-moment[data-approval-focus="decision"] .interrupt-decision-form {
display: flex;
flex-direction: column;
align-items: stretch;
flex-wrap: nowrap;
overflow: hidden;
}
.guided-product-moment[data-approval-focus="decision"] .interrupt-decision-form > * {
box-sizing: border-box;
width: 100%;
min-width: 0;
}
.guided-product-moment[data-approval-focus="decision"] .interrupt-decision-form__issues {
flex: 1 1 auto;
min-height: 0;
overflow: auto;
}
.guided-product-moment[data-approval-focus="decision"] .interrupt-decision-form__header,
.guided-product-moment[data-approval-focus="decision"] .interrupt-decision-form__meta,
.guided-product-moment[data-approval-focus="decision"] .interrupt-decision-form__comment-label,
.guided-product-moment[data-approval-focus="decision"] .interrupt-decision-form__actions {
flex: 0 0 auto;
}
.guided-product-moment[data-approval-focus="decision"] .interrupt-decision-form__issue-title {
min-width: 0;
overflow-wrap: anywhere;
word-break: break-word;
}
.run-facts-card[data-density="compact"] {
padding: 0.8rem;
font-size: 0.82rem;
@@ -1384,6 +1432,11 @@
box-sizing: border-box;
}
.guided-product-moment[data-continuation-focus="output"] .run-facts-card[data-output-priority="report"] {
border-color: color-mix(in srgb, var(--accent-cyan) 62%, var(--stage-line));
box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent-cyan) 12%, transparent);
}
.guided-product-moment__resume-support .operation-command {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
@@ -1427,13 +1480,75 @@
.run-trace-frame {
display: grid;
grid-template-columns: minmax(8rem, 0.26fr) auto auto minmax(0, 1fr);
gap: 0.5rem 0.75rem;
align-items: start;
gap: 0.65rem;
padding: 0.7rem 0.8rem;
border: 1px solid var(--stage-line);
border-radius: 0.7rem;
background: color-mix(in srgb, var(--stage-surface) 84%, transparent);
}
.run-trace-frame .run-facts-dl {
grid-column: 4;
.guided-product-moment[data-continuation-focus="trace"] .run-trace-facts {
border-color: color-mix(in srgb, var(--accent-cyan) 62%, var(--stage-line));
box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent-cyan) 12%, transparent);
}
.run-trace-frame__header {
display: flex;
align-items: center;
gap: 0.5rem;
min-width: 0;
}
.run-trace-frame__header strong {
min-width: 0;
overflow-wrap: anywhere;
color: var(--text-primary);
font: 700 0.82rem/1.2 var(--font-mono, monospace);
}
.run-trace-frame__facts {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.45rem;
min-width: 0;
margin: 0;
}
.run-trace-frame__fact {
min-width: 0;
padding: 0.42rem 0.5rem;
border-radius: 0.45rem;
background: color-mix(in srgb, var(--stage-inset) 72%, transparent);
}
.run-trace-frame__fact dt {
margin: 0 0 0.25rem;
color: var(--text-muted);
font-size: 0.62rem;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.run-trace-frame__fact dd {
min-width: 0;
margin: 0;
}
.run-trace-frame__fact code {
display: block;
min-width: 0;
overflow-wrap: anywhere;
color: var(--text-primary);
font: 600 0.67rem/1.35 var(--font-mono, monospace);
}
.run-trace-frame__fact[data-value-kind="empty-object"] code {
color: var(--text-muted);
}
.run-trace-frame__fact[data-value-kind="missing"] code {
color: var(--accent-amber);
font-style: italic;
}
.guided-product-moment[data-moment="trace"] .run-facts-card[data-output-priority="summary"] {
@@ -1447,6 +1562,12 @@
overflow-wrap: anywhere;
}
@container presentation-canvas (max-width: 820px) {
.run-trace-frame__facts {
grid-template-columns: 1fr;
}
}
@container presentation-canvas (max-width: 1050px) {
.guided-product-moment__approval-grid,
.guided-product-moment__resume-grid,