fix: address Copilot review on the list-salvage path (#1909)
Three real defects it caught, plus the tests it asked for: - The strict `tools/list` aggregate is filtered by the SDK for invalid `x-mcp-header` annotations (SEP-2243), but the salvage path was not: one ordinary malformed tool would trip the fallback and readmit every invalid-header tool beside it. Salvaged tools now go through the same scan. - `markResponseRejected` resolves "the last response received for this method", and the re-walk answers that same method — so the mark landed on a salvage page that succeeded while the actually-invalid response rendered clean. The correlation id is now captured before the re-walk. - The report was cleared only on an explicit `disconnect()`. The route out that tears down nothing (`onerror` with no `onclose`) carried it into the next connection, so it is now cleared on the start-clean `resetSessionState()` path as well. Fixing the first exposed two more, both found by the new test: - The modern (2026-07-28) codec validates a result against the spec schema before the caller's, so a lenient `client.request` is rejected exactly as the strict one was — there was no salvage on the modern leg at all. It now goes through the raw-wire channel that exists for the modern `tasks/*` methods. `withModernEnvelope` is split out of `withModernTaskEnvelope` so a list request doesn't claim the tasks extension the user may have disabled. - A malformed tool also blanked the SEP-2243 excluded set, leaving a tool that vanished for an invalid header with no explanation — #1909's own complaint one level down. The scan walk now tolerates a malformed sibling. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HDdo1rNRVQnVRRdsqSrvG3 Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
fe5b622fd1
commit
54c6088114
1 changed files with 340 additions and 24 deletions
|
|
@ -144,6 +144,76 @@ describe("useInspectorClient", () => {
|
|||
expect(result.current.excludedTools).toEqual([]);
|
||||
});
|
||||
|
||||
it("subscribes to malformedListItemsChange and updates (#1909)", () => {
|
||||
const client = new FakeInspectorClient();
|
||||
const { result } = renderHook(() => useInspectorClient(client));
|
||||
expect(result.current.malformedListItems).toEqual([]);
|
||||
const malformed = [
|
||||
{
|
||||
method: "resources/templates/list",
|
||||
index: 1,
|
||||
label: "array_annotations",
|
||||
reason: "annotations: Invalid input (expected object)",
|
||||
},
|
||||
];
|
||||
act(() => {
|
||||
client.setMalformedListItems(malformed);
|
||||
});
|
||||
expect(result.current.malformedListItems).toEqual(malformed);
|
||||
});
|
||||
|
||||
it("seeds malformedListItems from the client it is given (#1909)", () => {
|
||||
// A client that already salvaged before this hook mounted (a screen opened
|
||||
// after the connect-time list load) must show the report immediately.
|
||||
const client = new FakeInspectorClient({ status: "connected" });
|
||||
const malformed = [
|
||||
{ method: "tools/list", index: 0, reason: "name: expected string" },
|
||||
];
|
||||
client.setMalformedListItems(malformed);
|
||||
const { result } = renderHook(() => useInspectorClient(client));
|
||||
expect(result.current.malformedListItems).toEqual(malformed);
|
||||
});
|
||||
|
||||
it("resets malformedListItems to [] when client becomes null (#1909)", () => {
|
||||
const client = new FakeInspectorClient({ status: "connected" });
|
||||
client.setMalformedListItems([
|
||||
{ method: "tools/list", index: 0, reason: "name: expected string" },
|
||||
]);
|
||||
const { result, rerender } = renderHook(({ c }) => useInspectorClient(c), {
|
||||
initialProps: { c: client as FakeInspectorClient | null },
|
||||
});
|
||||
expect(result.current.malformedListItems.length).toBe(1);
|
||||
rerender({ c: null });
|
||||
expect(result.current.malformedListItems).toEqual([]);
|
||||
});
|
||||
|
||||
it("re-seeds malformedListItems on a client swap, not the old server's (#1909)", () => {
|
||||
const first = new FakeInspectorClient({ status: "connected" });
|
||||
first.setMalformedListItems([
|
||||
{ method: "tools/list", index: 0, reason: "name: expected string" },
|
||||
]);
|
||||
const second = new FakeInspectorClient({ status: "connected" });
|
||||
const { result, rerender } = renderHook(({ c }) => useInspectorClient(c), {
|
||||
initialProps: { c: first as FakeInspectorClient },
|
||||
});
|
||||
expect(result.current.malformedListItems).toHaveLength(1);
|
||||
rerender({ c: second });
|
||||
expect(result.current.malformedListItems).toEqual([]);
|
||||
});
|
||||
|
||||
it("stops listening for malformedListItemsChange after unmount (#1909)", () => {
|
||||
const client = new FakeInspectorClient({ status: "connected" });
|
||||
const { result, unmount } = renderHook(() => useInspectorClient(client));
|
||||
unmount();
|
||||
act(() => {
|
||||
client.setMalformedListItems([
|
||||
{ method: "tools/list", index: 0, reason: "name: expected string" },
|
||||
]);
|
||||
});
|
||||
// A listener left attached would update state on an unmounted tree.
|
||||
expect(result.current.malformedListItems).toEqual([]);
|
||||
});
|
||||
|
||||
it("resets protocolEra / discoverResult to defaults when client becomes null", () => {
|
||||
const client = new FakeInspectorClient({
|
||||
status: "connected",
|
||||
|
|
|
|||
Loading…
Reference in a new issue