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
5 changes: 5 additions & 0 deletions src/browser/components/ProjectSidebar/ProjectSidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -959,6 +959,7 @@ describe("ProjectSidebar flat chat list", () => {
({
selectedWorkspace: null,
setSelectedWorkspace: () => undefined,
archivingWorkspaceIds: new Set<string>(),
preflightArchiveWorkspace: () =>
Promise.resolve({ success: true, data: { kind: "ready" } }),
archiveWorkspace: () => Promise.resolve({ success: true, data: { kind: "archived" } }),
Expand Down Expand Up @@ -1010,6 +1011,7 @@ describe("ProjectSidebar flat chat list", () => {
({
selectedWorkspace: null,
setSelectedWorkspace: () => undefined,
archivingWorkspaceIds: new Set<string>(),
preflightArchiveWorkspace: () =>
Promise.resolve({ success: true, data: { kind: "ready" } }),
archiveWorkspace: () => Promise.resolve({ success: true, data: { kind: "archived" } }),
Expand Down Expand Up @@ -1100,6 +1102,7 @@ describe("ProjectSidebar flat chat list", () => {
({
selectedWorkspace: null,
setSelectedWorkspace: () => undefined,
archivingWorkspaceIds: new Set<string>(),
preflightArchiveWorkspace: () =>
Promise.resolve({ success: true, data: { kind: "ready" } }),
archiveWorkspace: () => Promise.resolve({ success: true, data: { kind: "archived" } }),
Expand Down Expand Up @@ -1280,6 +1283,7 @@ describe("ProjectSidebar flat chat list", () => {
({
selectedWorkspace: null,
setSelectedWorkspace: () => undefined,
archivingWorkspaceIds: new Set<string>(),
preflightArchiveWorkspace: () =>
Promise.resolve({ success: true, data: { kind: "ready" } }),
archiveWorkspace: () => Promise.resolve({ success: true, data: { kind: "archived" } }),
Expand Down Expand Up @@ -1335,6 +1339,7 @@ describe("ProjectSidebar flat chat list", () => {
({
selectedWorkspace: null,
setSelectedWorkspace: () => undefined,
archivingWorkspaceIds: new Set<string>(),
preflightArchiveWorkspace: () =>
Promise.resolve({ success: true, data: { kind: "ready" } }),
archiveWorkspace: () => Promise.resolve({ success: true, data: { kind: "archived" } }),
Expand Down
23 changes: 12 additions & 11 deletions src/browser/features/Messages/MessageRenderer.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -212,10 +212,10 @@ The report inherited transcript-sized markdown styles instead of compact task ch
title: "Agent update",
reportMarkdown: `## Agent update

The same compact report typography applies to incremental agent findings.
Incremental findings read as outgoing communication, separate from compact task details.

- Body and inline \`code\` remain aligned with tool chrome.
- Headings retain a modest hierarchy.`,
- Body and inline \`code\` stay legible alongside transcript messages.
- Headings retain a clear hierarchy.`,
},
{ success: true }
),
Expand All @@ -238,7 +238,7 @@ The same compact report typography applies to incremental agent findings.

const agentReportCard = await waitFor(() => {
const card = canvasElement.querySelector<HTMLElement>(
'[data-component="AgentReportToolCall"]'
'[data-component="AgentCommunicationCard"]'
);
if (!card) throw new Error("Agent report card not rendered");
return card;
Expand Down Expand Up @@ -287,20 +287,21 @@ The same compact report typography applies to incremental agent findings.
});

await waitFor(() => {
const report = agentReportCard.querySelector<HTMLElement>(".compact-report-markdown");
const report = agentReportCard.querySelector<HTMLElement>(".markdown-content");
const heading = report?.querySelector<HTMLElement>("h2");
const code = report?.querySelector<HTMLElement>("code");
if (!report || !heading || !code) {
throw new Error("Expanded agent report markdown not rendered");
}
if (Number.parseFloat(getComputedStyle(report).fontSize) > 11) {
throw new Error("Agent report body text is larger than compact tool chrome");
const bodyFontSize = Number.parseFloat(getComputedStyle(report).fontSize);
if (bodyFontSize < 14) {
throw new Error("Agent report body text is smaller than transcript prose");
}
if (Number.parseFloat(getComputedStyle(heading).fontSize) > 13) {
throw new Error("Agent report heading is too large for compact tool chrome");
if (Number.parseFloat(getComputedStyle(heading).fontSize) <= bodyFontSize) {
throw new Error("Agent report heading has lost its hierarchy");
}
if (Number.parseFloat(getComputedStyle(code).fontSize) > 11) {
throw new Error("Agent report inline code is larger than compact tool chrome");
if (Number.parseFloat(getComputedStyle(code).fontSize) < 12) {
throw new Error("Agent report inline code is too small to read");
}
if (agentReportCard.scrollWidth > agentReportCard.clientWidth) {
throw new Error(
Expand Down
182 changes: 181 additions & 1 deletion src/browser/features/Tools/AgentReportToolCall.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { GlobalWindow } from "happy-dom";
import { cleanup, render } from "@testing-library/react";
import { cleanup, fireEvent, render } from "@testing-library/react";

import { TooltipProvider } from "@/browser/components/Tooltip/Tooltip";
import { AgentReportToolCall } from "./AgentReportToolCall";
Expand Down Expand Up @@ -77,4 +77,184 @@ describe("AgentReportToolCall", () => {

expect(view.getByText(/Report file: report\.md/)).toBeTruthy();
});

test("collapses the body while retaining the title, then reopens it", () => {
const view = render(
<TooltipProvider>
<AgentReportToolCall
args={{ title: "Recovery audit", reportMarkdown: "Summary\n\nDetailed findings" }}
result={{ success: true }}
status="completed"
/>
</TooltipProvider>
);
const toggle = view.getByRole("button", { name: "Recovery audit" });
expect(toggle.getAttribute("aria-expanded")).toBe("true");
fireEvent.click(toggle);
expect(toggle.getAttribute("aria-expanded")).toBe("false");
expect(view.queryByText("Detailed findings")).toBeNull();
fireEvent.click(toggle);
expect(view.getByText(/Detailed findings/)).toBeTruthy();
});

test("keeps validation failures visible when collapsed instead of claiming delivery", () => {
const view = render(
<TooltipProvider>
<AgentReportToolCall
args={{ title: "Audit", reportMarkdown: "Draft" }}
result={{
success: false,
message: "Report rejected",
errors: [{ path: "reportMarkdown", message: "Report exceeds limit" }],
}}
status="completed"
/>
</TooltipProvider>
);
fireEvent.click(view.getByRole("button", { name: "Audit" }));
expect(view.getByRole("alert").textContent).toContain("Report exceeds limit");
expect(view.getByRole("status").className).toContain("text-danger");
});

test("prefers the submitted report over the draft arguments", () => {
const view = render(
<TooltipProvider>
<AgentReportToolCall
args={{ reportMarkdown: "Draft report" }}
result={{ success: true, report: { reportMarkdown: "Accepted report" } }}
status="completed"
/>
</TooltipProvider>
);
expect(view.queryByText("Draft report")).toBeNull();
expect(view.getByText("Accepted report")).toBeTruthy();
});

test.each(["", " "])("keeps the report toggle named for a blank title: %j", (title) => {
const view = render(
<TooltipProvider>
<AgentReportToolCall args={{ title, reportMarkdown: "Findings" }} status="completed" />
</TooltipProvider>
);
expect(view.getByRole("button", { name: /\S/ })).toBeTruthy();
});

test.each(
["legacy output", 42, true, [], { success: false, message: "Missing errors" }].map(
(result) => ({ result })
)
)("keeps malformed persisted report results renderable: %j", ({ result }) => {
const view = render(
<TooltipProvider>
<AgentReportToolCall
args={{ reportMarkdown: "Preserved findings" }}
result={result}
status="completed"
/>
</TooltipProvider>
);
expect(view.getByText("Preserved findings")).toBeTruthy();
expect(view.getByRole("status").className).not.toContain("text-success");
});

test("accepts report results decorated by post hooks without mutating hook output", () => {
const result = Object.freeze({
success: true,
report: { reportMarkdown: "Submitted findings" },
hook_output: "Formatter completed",
hook_duration_ms: 20,
hook_path: ".xum/tool_post",
ui_only: {},
});
const view = render(
<TooltipProvider>
<AgentReportToolCall
args={{ reportMarkdownPath: "report.md" }}
result={result}
status="completed"
/>
</TooltipProvider>
);
expect(view.getByText("Submitted findings")).toBeTruthy();
expect(view.getByRole("status").className).toContain("text-success");
});

test("shows bare pre-hook blocking errors", () => {
const view = render(
<TooltipProvider>
<AgentReportToolCall
args={{ reportMarkdown: "Draft" }}
result={{ error: "Blocked by project hook" }}
status="completed"
/>
</TooltipProvider>
);
expect(view.getByRole("alert").textContent).toBe("Blocked by project hook");
expect(view.getByRole("status").className).toContain("text-danger");
});

test.each([null, undefined, { type: "json", value: null }].map((result) => ({ result })))(
"does not claim delivery for a missing completed result: %j",
({ result }) => {
const view = render(
<TooltipProvider>
<AgentReportToolCall
args={{ reportMarkdown: "Findings" }}
result={result}
status="completed"
/>
</TooltipProvider>
);
expect(view.getByRole("status").className).not.toContain("text-success");
expect(view.getByRole("status").textContent).toBe("Result unavailable");
}
);

test.each([
{ status: "pending", label: "Pending" },
{ status: "executing", label: "Sending…" },
{ status: "failed", label: "Not sent" },
{ status: "interrupted", label: "Interrupted" },
] as const)("preserves lifecycle status without a result: $status", ({ status, label }) => {
const view = render(
<TooltipProvider>
<AgentReportToolCall args={{ reportMarkdown: "Findings" }} result={null} status={status} />
</TooltipProvider>
);
expect(view.getByRole("status").textContent).toBe(label);
});

test("accepts SDK-wrapped results with inner and outer hook metadata", () => {
const view = render(
<TooltipProvider>
<AgentReportToolCall
args={{ reportMarkdown: "Findings" }}
status="completed"
result={Object.freeze({
type: "json",
value: Object.freeze({ ...{ success: true }, hook_output: "Inner hook" }),
hook_output: "Outer hook",
hook_path: ".xum/tool_post",
})}
/>
</TooltipProvider>
);
expect(view.getByRole("status").className).toContain("text-success");
});

test("shows SDK-wrapped blocking errors", () => {
const view = render(
<TooltipProvider>
<AgentReportToolCall
args={{ reportMarkdown: "Findings" }}
status="completed"
result={{
type: "json",
value: { error: "Wrapped blocking error" },
}}
/>
</TooltipProvider>
);
expect(view.getByRole("alert").textContent).toBe("Wrapped blocking error");
});
});
Loading
Loading