mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
Bare `mock.module('axios', () => ({ default: { stubs } }))` is
process-global last-write-wins and drops `axios.create`, `request`,
`isAxiosError`, etc. that real consumers need. In CI's alphabetical
file order, that produces dozens of polluted failures (AgentsPlatformView,
schedule API, memory-stores API, etc.) that don't reproduce on WSL2.
Introduce `tests/mocks/axios.ts` with `setupAxiosMock()` — `require('axios')`
inside the factory, spread real shape, route each verb through a per-suite
`useStubs` flag. beforeAll flips on, afterAll flips off; the spread
fall-through eliminates cross-file leakage.
Refactored 12 axios mockers in tests/, plus the bare `@anthropic/ink` mocks
in ultrareviewCommand and onboarding suites (same pollution pattern broke
AgentsPlatformView's Box/Text rendering).
Verified: 5339/5345 tests pass locally; remaining 6 failures are
pre-existing isolation issues unrelated to this change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
142 lines
5.1 KiB
TypeScript
142 lines
5.1 KiB
TypeScript
/**
|
|
* Shared axios mock helper using the spread+flag pattern.
|
|
*
|
|
* Why this exists:
|
|
* `mock.module('axios', () => ({ default: { get, post } }))` is process-global
|
|
* (last-write-wins) and drops real axios shape (`create`, `request`, `isAxiosError`,
|
|
* verb methods, etc). When test file A registers a stub-only mock, every later
|
|
* test file B that imports axios gets A's bare stub even after A finishes —
|
|
* unless B registers its own mock. In CI (alphabetical file order on Linux),
|
|
* that produces dozens of "polluted" failures that don't reproduce on WSL2.
|
|
*
|
|
* The spread+flag pattern fixes both problems:
|
|
* 1. `require('axios')` INSIDE the factory pulls the real module (top-level
|
|
* `await import('axios')` would re-enter the mocked one and recurse).
|
|
* 2. The factory spreads the real exports, then replaces method references
|
|
* with router functions that read a per-suite `useStubs` boolean. When the
|
|
* flag is OFF (default), calls fall through to the real axios method;
|
|
* when ON, they hit the suite's stubs. Each suite flips the flag in
|
|
* beforeAll and clears it in afterAll, so cross-suite pollution disappears.
|
|
*
|
|
* Usage in a test file:
|
|
*
|
|
* import { setupAxiosMock } from '../../../tests/mocks/axios'
|
|
*
|
|
* const axiosHandle = setupAxiosMock()
|
|
* axiosHandle.stubs.get = (url, config) => Promise.resolve({ status: 200, data: {...}, headers: {}, statusText: 'OK', config })
|
|
* axiosHandle.stubs.post = ...
|
|
*
|
|
* beforeAll(() => { axiosHandle.useStubs = true })
|
|
* afterAll(() => { axiosHandle.useStubs = false })
|
|
*
|
|
* If your suite needs an `isAxiosError` predicate that recognises plain
|
|
* objects with `isAxiosError: true`, set `axiosHandle.stubs.isAxiosError` —
|
|
* otherwise the real axios's predicate is used.
|
|
*/
|
|
import { mock } from 'bun:test'
|
|
|
|
// Test stubs come in many shapes — `(url: string) => Promise<...>`, etc. —
|
|
// and assigning them to a tighter signature like `(...args: unknown[]) => unknown`
|
|
// triggers TS2322 (parameter type contravariance). The biome rule that
|
|
// disallows `any` here is already disabled project-wide, so plain `any` is
|
|
// the correct escape hatch for an internal test-only union.
|
|
// biome-ignore lint/suspicious/noExplicitAny: see comment above
|
|
type AnyFn = (...args: any[]) => unknown
|
|
|
|
export type AxiosMethodStubs = {
|
|
get?: AnyFn
|
|
post?: AnyFn
|
|
put?: AnyFn
|
|
patch?: AnyFn
|
|
delete?: AnyFn
|
|
head?: AnyFn
|
|
options?: AnyFn
|
|
request?: AnyFn
|
|
isAxiosError?: (e: unknown) => boolean
|
|
isCancel?: (e: unknown) => boolean
|
|
create?: AnyFn
|
|
}
|
|
|
|
export type AxiosMockHandle = {
|
|
/** When true, calls are routed to `stubs`; when false, to real axios. */
|
|
useStubs: boolean
|
|
/** Per-method stubs. Only set the methods your suite exercises. */
|
|
stubs: AxiosMethodStubs
|
|
}
|
|
|
|
/**
|
|
* Register a process-global mock for `axios` that spreads the real module and
|
|
* gates each method behind a per-suite flag. Call once at the top of a test
|
|
* file (outside `describe`). Returns a handle whose `.useStubs` and `.stubs`
|
|
* fields the suite controls in beforeAll/afterAll.
|
|
*/
|
|
export function setupAxiosMock(): AxiosMockHandle {
|
|
const handle: AxiosMockHandle = { useStubs: false, stubs: {} }
|
|
|
|
mock.module('axios', () => {
|
|
// Pull the REAL module synchronously inside the factory. Top-level
|
|
// `await import('axios')` would resolve through the mock and recurse.
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
const real = require('axios') as Record<string, unknown>
|
|
const realDefault = ((real.default as
|
|
| Record<string, unknown>
|
|
| undefined) ?? real) as Record<string, unknown>
|
|
|
|
const route = (method: keyof AxiosMethodStubs): AnyFn => {
|
|
const realFn = realDefault[method] as AnyFn | undefined
|
|
return (...args: unknown[]) => {
|
|
if (handle.useStubs) {
|
|
const stub = handle.stubs[method] as AnyFn | undefined
|
|
if (stub) return stub(...args)
|
|
}
|
|
if (typeof realFn === 'function') return realFn(...args)
|
|
throw new Error(`axios.${method} is not available on real axios`)
|
|
}
|
|
}
|
|
|
|
const verbs: (keyof AxiosMethodStubs)[] = [
|
|
'get',
|
|
'post',
|
|
'put',
|
|
'patch',
|
|
'delete',
|
|
'head',
|
|
'options',
|
|
'request',
|
|
'create',
|
|
]
|
|
|
|
const routedDefault: Record<string, unknown> = { ...realDefault }
|
|
for (const v of verbs) {
|
|
routedDefault[v] = route(v)
|
|
}
|
|
|
|
routedDefault.isAxiosError = (e: unknown) => {
|
|
if (handle.useStubs && handle.stubs.isAxiosError) {
|
|
return handle.stubs.isAxiosError(e)
|
|
}
|
|
const realPredicate = realDefault.isAxiosError as
|
|
| ((e: unknown) => boolean)
|
|
| undefined
|
|
return realPredicate ? realPredicate(e) : false
|
|
}
|
|
routedDefault.isCancel = (e: unknown) => {
|
|
if (handle.useStubs && handle.stubs.isCancel) {
|
|
return handle.stubs.isCancel(e)
|
|
}
|
|
const realPredicate = realDefault.isCancel as
|
|
| ((e: unknown) => boolean)
|
|
| undefined
|
|
return realPredicate ? realPredicate(e) : false
|
|
}
|
|
|
|
return {
|
|
...real,
|
|
...routedDefault,
|
|
default: routedDefault,
|
|
}
|
|
})
|
|
|
|
return handle
|
|
}
|