inspector/clients/cli/__tests__/helpers/oauth-test-fakes.ts
cliffhall b33532c024 feat: allow complex JSON values in request _meta
Closes #1910

`_meta` was typed `Record<string, string>` end to end in the Inspector —
on disk as a `{ key, value }[]` pair array, in the Server Settings form as
key/value rows, and in every request verb's per-call metadata parameter.
Nothing in the MCP spec restricts a `_meta` value to a string, and the SDK
models the field as a `z.looseObject`, so the restriction was ours alone.

Widen the whole path to `RequestMetadata` (`Record<string, JsonValue>`):

- Per-server default metadata is a JSON **object** on disk and in memory.
  The pre-#1910 pair array is still read (`normalizeStoredMetadata`) so an
  existing `mcp.json` keeps working; it is never written back, so a save
  migrates the entry.
- The Server Settings → Request Metadata section is a JSON editor
  (`JsonObjectInput`) instead of key/value rows — rows cannot express an
  object value. It keeps the typed text as the display source and emits
  only parsed JSON, the same draft split `SchemaJsonField` uses, so the box
  stays typeable; invalid text is flagged inline and not applied.
- The CLI stops flattening `--metadata` / `--tool-metadata` through
  `metaValueToString`. Those values were already JSON-parsed, so
  `trace={"id":"abc"}` used to reach the wire as the *string* `{"id":"abc"}`
  and now reaches it as an object.
- `servers/show` redaction replaces a sensitive key's whole value rather
  than a string, so a structured value cannot leak through its members.
- `progressTokenOf` narrows the reserved `progressToken` key back to
  `string | number` before it reaches the SDK; any other JSON still travels
  inside `_meta` but wires up no progress callback.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177mPHAdECD18nLLCTwR5rh
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-08-23 12:23:40 -04:00

75 lines
3 KiB
TypeScript

import { vi } from "vitest";
import {
DEFAULT_MAX_FETCH_REQUESTS,
DEFAULT_TASK_TTL_MS,
} from "@inspector/core/mcp/types.js";
import type { InspectorServerSettings } from "@inspector/core/mcp/types.js";
import type { CliOAuthClient } from "../../src/cliOAuth.js";
/**
* Typed mock factories for the CLI OAuth tests. They exist so a test can supply
* only the surface a given code path exercises while `tsc` still sees a
* complete `CliOAuthClient` / `InspectorServerSettings` — avoiding a spray of
* `as unknown as` casts (see #1791 and the AGENTS.md `as`-cast policy).
*/
/**
* A full {@link CliOAuthClient} whose every method is a `vi.fn()` resolving to a
* benign value. Pass `overrides` to swap in the method(s) a test drives (e.g. a
* `connect` that rejects with `AuthRecoveryRequiredError`).
*/
export function makeFakeCliOAuthClient(
overrides: Partial<CliOAuthClient> = {},
): CliOAuthClient {
// The defaults are typed against the real CliOAuthClient signatures (not bare
// vi.fn()s) so a default with a wrong implementation/return would be rejected,
// the same way the tui App spies are typed. Note this does NOT extend to
// `overrides`: a caller passing a bare `vi.fn()` gets `Mock<(...args) => any>`,
// assignable to any member, so an override's implementation is unchecked — the
// factory guarantees the object's shape, not each override's signature. (And
// since `exactOptionalPropertyTypes` is off, `Partial<…>` even admits an
// explicit `undefined` for a required member.)
return {
connect: vi.fn<CliOAuthClient["connect"]>().mockResolvedValue(undefined),
disconnect: vi
.fn<CliOAuthClient["disconnect"]>()
.mockResolvedValue(undefined),
authenticate: vi
.fn<CliOAuthClient["authenticate"]>()
.mockResolvedValue(undefined),
beginInteractiveAuthorization: vi
.fn<CliOAuthClient["beginInteractiveAuthorization"]>()
.mockResolvedValue(undefined),
completeOAuthFlow: vi
.fn<CliOAuthClient["completeOAuthFlow"]>()
.mockResolvedValue(undefined),
checkAuthChallengeSatisfied: vi
.fn<CliOAuthClient["checkAuthChallengeSatisfied"]>()
.mockResolvedValue(false),
...overrides,
};
}
/**
* A full {@link InspectorServerSettings} with representative defaults for its
* required fields (the timeouts at 0 = "SDK default", `taskTtl` /
* `maxFetchRequests` at their product defaults, empty lists elsewhere). Pass
* `overrides` for the field(s) under test (e.g. `{ enterpriseManaged: true }`);
* as a `Partial<…>` with `exactOptionalPropertyTypes` off it also admits an
* explicit `undefined` for a required field, so don't rely on it to reject one.
*/
export function makeFakeServerSettings(
overrides: Partial<InspectorServerSettings> = {},
): InspectorServerSettings {
return {
headers: [],
metadata: {},
env: [],
connectionTimeout: 0,
requestTimeout: 0,
taskTtl: DEFAULT_TASK_TTL_MS,
maxFetchRequests: DEFAULT_MAX_FETCH_REQUESTS,
roots: [],
...overrides,
};
}