Merge remote-tracking branch 'origin/v2/main' into v2/sdk-v2-migration-1624

# Conflicts:
#	clients/web/src/components/groups/ResourceLink/ResourceLink.tsx
#	clients/web/src/components/groups/ToolResultPanel/ToolResultPanel.stories.tsx
#	clients/web/src/components/screens/ToolsScreen/ToolsScreen.tsx
This commit is contained in:
cliffhall 2026-07-15 14:15:50 -04:00
commit 39362dd5ff

View file

@ -7,6 +7,7 @@ import {
waitFor,
} from "../../../test/renderWithMantine";
import { ToolResultPanel } from "./ToolResultPanel";
import { resultHasResourceLinks } from "./toolResultUtils";
const okResult: CallToolResult = {
content: [{ type: "text", text: "ok" }],
@ -41,7 +42,7 @@ describe("ToolResultPanel", () => {
expect(screen.getByText("No results yet")).toBeInTheDocument();
});
it("renders a resource_link block as an expandable ResourceLink", async () => {
it("groups resource_link blocks in a scrollable Resource Links box", async () => {
const user = userEvent.setup();
const onReadResource = vi.fn().mockResolvedValue({
contents: [{ uri: "demo://r/1", text: "linked body" }],
@ -60,6 +61,10 @@ describe("ToolResultPanel", () => {
/>,
);
expect(screen.getByText("ok")).toBeInTheDocument();
// The link sits inside a grouped, labeled box.
expect(
screen.getByRole("heading", { name: "Resource Links" }),
).toBeInTheDocument();
await user.click(
screen.getByRole("button", { name: "Expand resource demo://r/1" }),
);
@ -69,6 +74,41 @@ describe("ToolResultPanel", () => {
);
});
it("collapses consecutive resource_link blocks into a single box", () => {
const result: CallToolResult = {
content: [
{ type: "text", text: "intro" },
{ type: "resource_link", uri: "demo://r/1", name: "One" },
{ type: "resource_link", uri: "demo://r/2", name: "Two" },
{ type: "resource_link", uri: "demo://r/3", name: "Three" },
],
};
renderWithMantine(<ToolResultPanel result={result} onClear={() => {}} />);
// One shared "Resource Links" heading for the whole run of links.
expect(
screen.getAllByRole("heading", { name: "Resource Links" }),
).toHaveLength(1);
expect(screen.getByText("One")).toBeInTheDocument();
expect(screen.getByText("Two")).toBeInTheDocument();
expect(screen.getByText("Three")).toBeInTheDocument();
});
it("renders a separate Resource Links box per non-adjacent run", () => {
const result: CallToolResult = {
content: [
{ type: "resource_link", uri: "demo://r/1", name: "One" },
{ type: "text", text: "divider" },
{ type: "resource_link", uri: "demo://r/2", name: "Two" },
],
};
renderWithMantine(<ToolResultPanel result={result} onClear={() => {}} />);
// The text block between the two links splits them into two boxes.
expect(
screen.getAllByRole("heading", { name: "Resource Links" }),
).toHaveLength(2);
expect(screen.getByText("divider")).toBeInTheDocument();
});
it("invokes onClear when the close button is clicked", async () => {
const user = userEvent.setup();
const onClear = vi.fn();
@ -76,4 +116,36 @@ describe("ToolResultPanel", () => {
await user.click(screen.getByRole("button", { name: "Close results" }));
expect(onClear).toHaveBeenCalledTimes(1);
});
describe("resultHasResourceLinks", () => {
it("is true only for a non-error result containing a resource_link", () => {
expect(
resultHasResourceLinks({
content: [
{ type: "text", text: "ok" },
{ type: "resource_link", uri: "demo://r/1", name: "Linked" },
],
}),
).toBe(true);
});
it("is false for a text-only result", () => {
expect(resultHasResourceLinks(okResult)).toBe(false);
});
it("is false for an empty result", () => {
expect(resultHasResourceLinks(emptyResult)).toBe(false);
});
it("is false when the result is an error, even with a resource_link", () => {
expect(
resultHasResourceLinks({
isError: true,
content: [
{ type: "resource_link", uri: "demo://r/1", name: "Linked" },
],
}),
).toBe(false);
});
});
});