Skip to content

test(mcp): restore SupermemoryClient coverage and fix two blank error messages - #1564

Open
addyCooks wants to merge 1 commit into
supermemoryai:mainfrom
addyCooks:test/mcp-client-error-handling
Open

test(mcp): restore SupermemoryClient coverage and fix two blank error messages#1564
addyCooks wants to merge 1 commit into
supermemoryai:mainfrom
addyCooks:test/mcp-client-error-handling

Conversation

@addyCooks

Copy link
Copy Markdown

Closes #1550

src/server/client/index.test.ts was deleted in a99cf4f (#1397). Ten days
later #1406 added the API-error unwrapping and the status-aware handleError
fallbacks and claimed tests for them, but the file was never restored so
SupermemoryClient, which owns every outbound Supermemory API call, had no
coverage at all.

Tests restored

53 cases in src/server/client/index.test.ts, with the supermemory SDK and
fetch both stubbed:

  • SDK construction API key, base URL, 30s timeout, x-sm-source header
  • Space scoping unscoped omits the filter, configured space is sent,
    explicit override wins, empty string is treated as unscoped
  • Result normalisation memory / chunk / context / content precedence,
    chunk results stay distinguishable, 200k-char truncation
  • forgetMemory exact match, 404 → similarity-search fallback, no match,
    chunks-only, non-404 errors do not fall back
  • Raw-fetch endpoints request bodies, auth headers, caller-supplied abort
    signal, zod contract violations
  • Full status table, timeouts, network failures, non-Error throws, and the
    "<operation> failed: …" prefixes

Includes the two cases #1406 described: a 403 with a JSON error body surfaces
the API's message, and an empty-body 403 gets the scope-aware fallback.

Two fixes the tests forced (+2 lines in index.ts)

  • A 403 with body {"error": ""} leaked raw JSON to the user.
    extractApiErrorMessage returns the raw string when the recognised key holds
    an empty value, so the user saw {"error":""}. A parsed envelope carrying no
    message now returns undefined and the caller reaches its fallback.
  • An unmapped status with an empty body produced a blank error message.
    409/413/etc. fall past the switch, and getDocuments/listMemoryEntries
    construct new Error("") when the body is empty, so the user got nothing.
    Unmapped statuses with no message now report the status.

Verification

  • vitest run src 5 files, 74 tests passed (was 4 files, 21 tests)
  • tsc --noEmit -p tsconfig.json clean, Biome clean
  • Mutation-checked: 7 injected regressions each fail 1–4 of the new tests

@Agnik47

Agnik47 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Nice to see coverage here — #1550 has been open a while.

One thing worth catching before this lands: uses a caller supplied abort signal and pagination asserts expect(init.signal).toBe(controller.signal), which pins the current behaviour at apps/mcp/src/server/client/index.ts:339:

const signal = options?.signal ?? AbortSignal.timeout(FETCH_TIMEOUT_MS)

That ?? makes cancellation and the 30s bound mutually exclusive — passing a signal removes the timeout, so a hung request is unbounded again.

It's the same defect as #1549 in packages/tools, fixed in #1595 by composing the two instead of choosing between them:

signal: options?.signal
    ? AbortSignal.any([options.signal, AbortSignal.timeout(FETCH_TIMEOUT_MS)])
    : AbortSignal.timeout(FETCH_TIMEOUT_MS),

No production caller in apps/mcp passes options today (fetch-graph-data.ts and memory-graph.ts both omit it), so it's latent either way — but a test asserting toBe(controller.signal) would make it meaningfully harder to fix afterwards, since the fix would then have to argue against a passing test.

Not asking you to expand scope here. Happy to send the index.ts:339 fix as a follow-up once this merges — flagging it mainly so the assertion doesn't lock the behaviour in.

… messages

supermemoryai#1406 shipped the API-error unwrapping and the status-aware handleError
fallbacks with a test file that had already been deleted in supermemoryai#1397, so
SupermemoryClient - every outbound API call, extractApiErrorMessage, and
all of handleError - had no coverage at all.

Restores src/server/client/index.test.ts with 53 cases covering the SDK
wiring, space scoping, result normalisation, forgetMemory's exact-match
and similarity fallbacks, the raw-fetch endpoints, and the full status
table.

Two of those cases failed against the untested code:

- A 403 whose body is {"error": ""} leaked the raw JSON envelope to the
  user, because extractApiErrorMessage falls through to the raw string
  when the recognised key holds an empty value. An envelope we parsed
  but that carries no message now yields undefined so the caller reaches
  its scope-aware fallback.
- A status outside the mapped switch (409, 413, ...) with an empty body
  reached the user as an Error with an empty message. Unmapped statuses
  with no message now report the status instead.
@addyCooks
addyCooks force-pushed the test/mcp-client-error-handling branch from 017a80b to 5eeebec Compare August 25, 2026 20:10
@addyCooks

Copy link
Copy Markdown
Author

Hey @Agnik47!
Good catch, fixed.
Swapped the identity check for a behavioural one:
it aborts the controller and asserts the signal fetch got observes it,
so it passes now and after AbortSignal.any.

Sanity-checked both directions the old toBe fails under your fix, the new one doesn't.
74/74 green either way.
index.ts:339 is all yours.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP SupermemoryClient has no test file — #1406 shipped error-handling logic with tests that don't exist

2 participants