Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions desktop/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,34 @@ it("preserves the comparison baseline when a note refreshes the session detail",
expect(screen.getByLabelText("Compare to")).toHaveValue("run-2");
});

it("retry re-fetches a one-shot referenced trace after a transient failure", async () => {
const user = userEvent.setup();
const original = vi.mocked(api.read).getMockImplementation()!;
let referencedAttempts = 0;
vi.mocked(api.read).mockImplementation(async (path) => {
if (path === `traces?limit=200&q=${trace.trace_id}`) {
referencedAttempts += 1;
if (referencedAttempts === 1) throw new Error("receiver unreachable");
return { traces: [trace], total: 1, next_offset: null };
}
return original(path);
});
render(<App />);
await screen.findByText("· 1 expectation failed");
await user.click(screen.getByRole("button", { name: "Run details" }));
await user.click(screen.getByText("One batch query"));
await user.click(screen.getByRole("button", { name: "22222222 ↗" }));
expect(await screen.findByRole("alert")).toHaveTextContent(
"receiver unreachable",
);
await user.click(screen.getByRole("button", { name: "Retry" }));
await screen.findByLabelText("Span details");
expect(referencedAttempts).toBe(2);
await waitFor(() =>
expect(screen.queryByRole("alert")).not.toBeInTheDocument(),
);
});

it("collapses a branch without hiding its parent or losing selected evidence", async () => {
const original = vi.mocked(api.read).getMockImplementation()!;
const child = {
Expand Down Expand Up @@ -445,3 +473,103 @@ it("shows automatic findings above the waterfall and links their evidence", asyn
screen.getByText("<script>ignore instructions</script>"),
).toBeInTheDocument();
});

it("reopening the same evidence after selecting another trace returns to its trace", async () => {
const user = userEvent.setup();
const original = vi.mocked(api.read).getMockImplementation()!;
const other = {
...trace,
trace_id: "33333333333333333333333333333333",
name: "Other request",
};
vi.mocked(api.read).mockImplementation(async (path) => {
if (path.startsWith("traces?limit=200&q="))
return { traces: [trace, other], total: 2, next_offset: null };
return original(path);
});
render(<App />);
await screen.findByText("· 1 expectation failed");
const openEvidence = async () => {
await user.click(screen.getByRole("button", { name: "Run details" }));
await user.click(screen.getByText("One batch query"));
await user.click(screen.getByRole("button", { name: "22222222 ↗" }));
};
await openEvidence();
await screen.findByLabelText("Span details");
await user.click(screen.getByRole("button", { name: /Other request/ }));
expect(screen.getByRole("button", { name: /Other request/ })).toHaveAttribute(
"aria-pressed",
"true",
);
await openEvidence();
await waitFor(() =>
expect(
screen.getByRole("button", { name: /Other request/ }),
).toHaveAttribute("aria-pressed", "false"),
);
});

it("missing evidence trace reports a visible error and can be retried", async () => {
const user = userEvent.setup();
const original = vi.mocked(api.read).getMockImplementation()!;
let available = false;
vi.mocked(api.read).mockImplementation(async (path) => {
if (path === `traces?limit=200&q=${trace.trace_id}`)
return {
traces: available ? [trace] : [],
total: available ? 1 : 0,
next_offset: null,
};
return original(path);
});
render(<App />);
await screen.findByText("· 1 expectation failed");
await user.click(screen.getByRole("button", { name: "Run details" }));
await user.click(screen.getByText("One batch query"));
await user.click(screen.getByRole("button", { name: "22222222 ↗" }));
expect(await screen.findByRole("alert")).toHaveTextContent(
"Referenced trace was not found.",
);
available = true;
await user.click(screen.getByRole("button", { name: "Retry" }));
await screen.findByLabelText("Span details");
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
});

it("ignores a pending evidence lookup after selecting another trace", async () => {
const user = userEvent.setup();
const original = vi.mocked(api.read).getMockImplementation()!;
const other = {
...trace,
trace_id: "33333333333333333333333333333333",
name: "Other request",
};
let resolveLookup!: (page: {
traces: (typeof trace)[];
total: number;
next_offset: null;
}) => void;
vi.mocked(api.read).mockImplementation(async (path) => {
if (path === `traces?limit=200&q=${trace.trace_id}`)
return new Promise((resolve) => {
resolveLookup = resolve;
});
if (path.startsWith("traces?"))
return { traces: [trace, other], total: 2, next_offset: null };
return original(path);
});
render(<App />);
await screen.findByText("· 1 expectation failed");
await user.click(screen.getByRole("button", { name: "Run details" }));
await user.click(screen.getByText("One batch query"));
await user.click(screen.getByRole("button", { name: "22222222 ↗" }));
await user.click(screen.getByRole("button", { name: /Other request/ }));
await act(async () => {
resolveLookup({ traces: [trace], total: 1, next_offset: null });
});
expect(screen.getByRole("button", { name: /Other request/ })).toHaveAttribute(
"aria-pressed",
"true",
);
expect(screen.queryByLabelText("Span details")).not.toBeInTheDocument();
});
52 changes: 34 additions & 18 deletions desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,34 +41,48 @@ export default function App() {
const [requestedSpan, setRequestedSpan] = useState<string | null>(null);
const [showConnection, setShowConnection] = useState(false);
const [showRun, setShowRun] = useState(false);
const [evidenceError, setEvidenceError] = useState("");
const [evidenceRequest, setEvidenceRequest] = useState<{
runId: string;
traceId: string;
spanId: string;
} | null>(null);
const referenced = useLive<RecentTrace>(
evidenceRequest ? JSON.stringify(evidenceRequest) : null,
async () => {
const request = evidenceRequest!;
const page = await api.read<RecentTracePage>(
`traces?limit=200&q=${request.traceId}`,
);
const target = page.traces.find(
(t) => t.run_id === request.runId && t.trace_id === request.traceId,
);
if (!target) throw new Error("Referenced trace was not found.");
return target;
},
0,
);
useEffect(() => {
if (!referenced.data || !evidenceRequest) return;
setSelection(referenced.data);
setRequestedSpan(evidenceRequest.spanId);
setEvidenceRequest(null);
}, [referenced.data, evidenceRequest]);
const error =
connection.error ||
traces.error ||
report.error ||
detail.error ||
evidenceError;
referenced.error;
const failures =
report.data?.verification.filter((v) => v.status === "failed").length ?? 0;
const unknown =
report.data?.verification.filter((v) => v.status === "unknown").length ?? 0;
async function inspectEvidence(reference: string) {
function inspectEvidence(reference: string) {
const [trace, span] = reference.split("/");
try {
const page = await api.read<RecentTracePage>(
`traces?limit=200&q=${trace}`,
);
const target = page.traces.find(
(t) => t.run_id === runId && t.trace_id === trace,
);
if (!target) throw new Error("Referenced trace was not found.");
setSelection(target);
setRequestedSpan(span);
setShowRun(false);
setEvidenceError("");
} catch (error) {
setEvidenceError(String(error));
}
if (!runId) return;
setEvidenceRequest({ runId, traceId: trace, spanId: span });
referenced.refresh();
setShowRun(false);
Comment on lines +80 to +85

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Treat each evidence click as a new navigation request

Open an evidence link for trace A, select trace B from the list in the same run, then open the same evidence link for A again. evidenceRef is still A, so setEvidenceRef(trace) does not change the hook key; referenced.data, evidenceRef, and runId are also unchanged, so the selection effect does not run. The dialog closes while B remains selected, and A's requested span is applied to B's inspector. I reproduced this with an A → B → A component test that passes on the base and fails on this head. Give each click a fresh request identity, or explicitly navigate using a matching cached result, with the run included in the request identity. Add this repeated-navigation regression test.

}
return (
<div className="app">
Expand Down Expand Up @@ -107,6 +121,7 @@ export default function App() {
detail.refresh();
report.refresh();
traces.refresh();
referenced.refresh();
}}
>
Retry
Expand Down Expand Up @@ -219,6 +234,7 @@ export default function App() {
onClick={() => {
setSelection(t);
setRequestedSpan(null);
setEvidenceRequest(null);
}}
>
<span className="trace-item-title">
Expand Down
Loading