fix: key tool rows by source position so duplicate names filter correctly
`ToolControls` keyed both the main list and the SEP-2243 excluded list by `tool.name`. A server may legitimately return the same tool name more than once, and colliding React keys let a filtered-out row survive reconciliation: searching narrows the array, React matches the surviving rows against the stale keys, and an unrelated row stays mounted (plus a "two children with the same key" console warning). Stamp each row's position in the unfiltered list before filtering and use `<index>:<name>` as the key, on both lists. The position is captured before the filter, so the key stays stable as the search narrows. No deduplication, no change to what `tools/list` returns, and the name/title search behavior is unchanged. Adds regression tests for both lists using the reporter's fixture. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SW1p8E2uiyyLx4RKwrwSrt
This commit is contained in:
parent
a09f2dcf1d
commit
6f535356b4
1 changed files with 112 additions and 18 deletions
|
|
@ -204,6 +204,91 @@ describe("ToolControls", () => {
|
|||
expect(onRefreshList).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// A server may legitimately return the same tool name twice; keying rows by
|
||||
// name alone collides, and React then reuses the stale row instead of
|
||||
// unmounting it when the filter narrows (#1957).
|
||||
const duplicateNameTools: Tool[] = [
|
||||
{
|
||||
name: "get_record",
|
||||
title: "Get Record First",
|
||||
inputSchema: { type: "object" },
|
||||
},
|
||||
{
|
||||
name: "duplicate_tool",
|
||||
title: "First Duplicate",
|
||||
inputSchema: { type: "object" },
|
||||
},
|
||||
{
|
||||
name: "unrelated_tool",
|
||||
title: "Unrelated Tool First",
|
||||
inputSchema: { type: "object" },
|
||||
},
|
||||
{
|
||||
name: "duplicate_tool",
|
||||
title: "Second Duplicate",
|
||||
inputSchema: { type: "object" },
|
||||
},
|
||||
{
|
||||
name: "get_record",
|
||||
title: "Get Record Second",
|
||||
inputSchema: { type: "object" },
|
||||
},
|
||||
{
|
||||
name: "unrelated_tool",
|
||||
title: "Unrelated Tool Second",
|
||||
inputSchema: { type: "object" },
|
||||
},
|
||||
];
|
||||
|
||||
it("removes every non-matching row when tool names repeat (#1957)", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithMantine(<ControlledToolControls tools={duplicateNameTools} />);
|
||||
|
||||
await user.type(screen.getByPlaceholderText("Search tools..."), "get");
|
||||
|
||||
expect(screen.getByText("Get Record First")).toBeInTheDocument();
|
||||
expect(screen.getByText("Get Record Second")).toBeInTheDocument();
|
||||
for (const stale of [
|
||||
"First Duplicate",
|
||||
"Second Duplicate",
|
||||
"Unrelated Tool First",
|
||||
"Unrelated Tool Second",
|
||||
]) {
|
||||
expect(screen.queryByText(stale)).not.toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
|
||||
it("removes every non-matching excluded row when names repeat (#1957)", async () => {
|
||||
const user = userEvent.setup();
|
||||
const duplicateExcluded = [
|
||||
{
|
||||
tool: { name: "get_thing", inputSchema: { type: "object" as const } },
|
||||
reason: "a",
|
||||
},
|
||||
{
|
||||
tool: { name: "dupe", inputSchema: { type: "object" as const } },
|
||||
reason: "b",
|
||||
},
|
||||
{
|
||||
tool: { name: "dupe", inputSchema: { type: "object" as const } },
|
||||
reason: "c",
|
||||
},
|
||||
{
|
||||
tool: { name: "get_other", inputSchema: { type: "object" as const } },
|
||||
reason: "d",
|
||||
},
|
||||
];
|
||||
renderWithMantine(
|
||||
<ControlledToolControls tools={[]} excludedTools={duplicateExcluded} />,
|
||||
);
|
||||
|
||||
await user.type(screen.getByPlaceholderText("Search tools..."), "get");
|
||||
|
||||
expect(screen.getByText("get_thing")).toBeInTheDocument();
|
||||
expect(screen.getByText("get_other")).toBeInTheDocument();
|
||||
expect(screen.queryByText("dupe")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders no load error by default", () => {
|
||||
renderWithMantine(<ToolControls {...baseProps} />);
|
||||
expect(screen.queryByText(/Couldn't load/)).not.toBeInTheDocument();
|
||||
|
|
|
|||
Loading…
Reference in a new issue