fix: address Copilot round 4 on the list-salvage path (#1909)
Two findings, both about the fallback accepting more than it should. - The lenient page schema validated pagination and nothing else, so a response carrying a malformed entry AND an unrelated top-level violation salvaged: the entry was reported and the other violation vanished with the strict error that caught it. The page schema is now DERIVED from the method's real result schema with only its item array relaxed (`lenientListPageSchema`), so every other field — `_meta`, `nextCursor`, anything a future revision adds — is still validated. The modern era envelope the raw-wire path bypasses (`resultType`, `ttlMs`, `cacheScope`) is checked explicitly for the same reason. Salvage is also narrowed to `InvalidResult`: `UnsupportedResultType` means the codec doesn't recognize what KIND of frame arrived, so it isn't a list with a bad entry and reporting an item-level reason would hide a protocol failure. Marking the Protocol entry still applies to both, hence two predicates. - On every rethrow path the re-walk had already answered the method, so the caller's own `markResponseRejected` (in `ManagedListState.refresh`) landed on the lenient page that succeeded and left the refused response clean. The correlation is restored before rethrowing — deleted when nothing had answered before, so the outer mark is a no-op rather than a lie. `nextCursorOf` / `rawItemsOf` narrow the page's fields instead of casting: the runtime-keyed `extend()` loses the shape, and asserting it back would be exactly the unjustified cast the last round removed. 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
aea3f6d4c2
commit
573b14e14e
1 changed files with 251 additions and 37 deletions
|
|
@ -565,6 +565,36 @@ describe("InspectorClient list salvage (#1909)", () => {
|
|||
expect(reported[0]?.label).toBeUndefined();
|
||||
});
|
||||
|
||||
it("leaves the correlation on the original response when it rethrows", async () => {
|
||||
// On a rethrow the re-walk has already answered the same method, so the
|
||||
// caller's own `markResponseRejected` would land on the lenient page that
|
||||
// succeeded. The correlation is restored so the outer mark still finds the
|
||||
// response that was actually refused.
|
||||
const server = await startMalformedServer({
|
||||
resourceTemplates: [{ items: [VALID_TEMPLATE], nextCursor: 42 }],
|
||||
});
|
||||
stopServer = server.stop;
|
||||
const connected = await connectTo(server.url);
|
||||
const log = new MessageLogState(connected);
|
||||
|
||||
await expect(connected.listAllResourceTemplates()).rejects.toThrow();
|
||||
// Simulates what ManagedListState.refresh() does in its catch.
|
||||
connected.markResponseRejected("resources/templates/list", "outer mark");
|
||||
|
||||
const calls = log
|
||||
.getMessages()
|
||||
.filter(
|
||||
(entry) =>
|
||||
"method" in entry.message &&
|
||||
entry.message.method === "resources/templates/list",
|
||||
);
|
||||
expect(calls[0]?.clientError).toBe("outer mark");
|
||||
for (const later of calls.slice(1)) {
|
||||
expect(later.clientError).toBeUndefined();
|
||||
}
|
||||
log.destroy();
|
||||
});
|
||||
|
||||
it("rethrows when the rejection is not about any single entry", async () => {
|
||||
// Every entry validates; the result is bad for another reason (a cursor of
|
||||
// the wrong type). There is no per-item story, so the original error must
|
||||
|
|
|
|||
Loading…
Reference in a new issue