fix(cli): guard numeric .code to HTTP range; bound cause-chain depth

Address @claude review of #1583:

- statusOf() now only treats a numeric `.code` as an HTTP status when it
  is in the 100-599 range, so an MCP SDK McpError JSON-RPC code (e.g.
  -32601 MethodNotFound) is no longer leaked into the envelope `status`
  or misclassified as AUTH_REQUIRED (it falls back to USAGE).
- causeOf() now carries a depth cap (MAX_CAUSE_DEPTH) so a cyclic/
  self-referential `error.cause` chain terminates instead of recursing
  infinitely.
- Add tests for both: a -32601 McpError-shaped error (status undefined,
  exit USAGE) and a self-referential cause chain (finite string).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
This commit is contained in:
cliffhall 2026-07-01 19:10:25 -04:00
commit fa5514a380

View file

@ -159,6 +159,18 @@ describe("classifyError", () => {
).toBe("error");
});
it("terminates on a self-referential cause chain without recursing infinitely", () => {
// A cyclic `error.cause` must not hang the classifier; the depth cap bounds
// the walk and returns a finite string.
const err = new Error("looping");
(err as { cause?: unknown }).cause = err;
const { envelope } = classifyError(err);
expect(envelope.message).toBe("looping");
// Bounded by the depth cap: a finite string, not an infinite loop.
expect(typeof envelope.cause).toBe("string");
expect(envelope.cause).toContain("looping");
});
it("captures a non-Error cause as a string", () => {
const err = new Error("outer");
(err as { cause?: unknown }).cause = { reason: "blocked" };
@ -181,6 +193,16 @@ describe("classifyError", () => {
expect(envelope.status).toBe(403);
});
it("ignores a JSON-RPC McpError numeric .code (outside the HTTP range) as a status", () => {
// MCP SDK's McpError carries a numeric `.code` that is a JSON-RPC error
// code (e.g. -32601 MethodNotFound), NOT an HTTP status. It must not leak
// into `status`, nor get misclassified as AUTH_REQUIRED.
const err = Object.assign(new Error("Method not found"), { code: -32601 });
const { exitCode, envelope } = classifyError(err);
expect(exitCode).toBe(EXIT_CODES.USAGE);
expect(envelope.status).toBeUndefined();
});
it("ignores a non-numeric node error code when reading the status", () => {
const err = Object.assign(new Error("getaddrinfo ENOTFOUND host"), {
code: "ENOTFOUND",