mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
test: add spread+flag axios mock helper to stop CI mock pollution
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>
This commit is contained in:
@@ -1,17 +1,31 @@
|
||||
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
|
||||
import {
|
||||
afterAll,
|
||||
afterEach,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
mock,
|
||||
test,
|
||||
} from 'bun:test'
|
||||
import { authMock } from '../../../../../../tests/mocks/auth'
|
||||
import { setupAxiosMock } from '../../../../../../tests/mocks/axios'
|
||||
|
||||
let requestStatus = 200
|
||||
const auditRecords: Record<string, unknown>[] = []
|
||||
|
||||
mock.module('axios', () => ({
|
||||
default: {
|
||||
request: async () => ({
|
||||
status: requestStatus,
|
||||
data: { ok: requestStatus >= 200 && requestStatus < 300 },
|
||||
}),
|
||||
},
|
||||
}))
|
||||
const axiosHandle = setupAxiosMock()
|
||||
axiosHandle.stubs.request = async () => ({
|
||||
status: requestStatus,
|
||||
data: { ok: requestStatus >= 200 && requestStatus < 300 },
|
||||
})
|
||||
|
||||
beforeAll(() => {
|
||||
axiosHandle.useStubs = true
|
||||
})
|
||||
afterAll(() => {
|
||||
axiosHandle.useStubs = false
|
||||
})
|
||||
|
||||
mock.module('src/utils/auth.js', authMock)
|
||||
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
import {
|
||||
afterAll,
|
||||
afterEach,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
mock,
|
||||
test,
|
||||
} from 'bun:test'
|
||||
import { setupAxiosMock } from '../../../../../../tests/mocks/axios'
|
||||
|
||||
// After this suite finishes, switch our getSecret override off so localVault's
|
||||
// own store.test.ts (running in the same process) sees the real impl.
|
||||
// own store.test.ts (running in the same process) sees the real impl. Also
|
||||
// flip the axios stub flag off so the spread mock falls through to real axios
|
||||
// for any test file that runs after this one.
|
||||
afterAll(() => {
|
||||
useMockForGetSecret = false
|
||||
getSecretShouldThrow = false
|
||||
axiosHandle.useStubs = false
|
||||
})
|
||||
|
||||
beforeAll(() => {
|
||||
axiosHandle.useStubs = true
|
||||
})
|
||||
|
||||
// We mock the LOWER layers (axios + localVault store + http util) rather
|
||||
@@ -34,9 +43,8 @@ const mockAxiosRequest = mock(
|
||||
}),
|
||||
)
|
||||
|
||||
mock.module('axios', () => ({
|
||||
default: { request: mockAxiosRequest },
|
||||
}))
|
||||
const axiosHandle = setupAxiosMock()
|
||||
axiosHandle.stubs.request = mockAxiosRequest
|
||||
|
||||
let mockedSecret: string | null = 'XSECRETXX'
|
||||
let getSecretShouldThrow = false
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { beforeEach, describe, expect, mock, test } from 'bun:test'
|
||||
import {
|
||||
afterAll,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
mock,
|
||||
test,
|
||||
} from 'bun:test'
|
||||
import { logMock } from '../../../../../../tests/mocks/log'
|
||||
import { setupAxiosMock } from '../../../../../../tests/mocks/axios'
|
||||
|
||||
type MockAxiosResponse = {
|
||||
data: ArrayBuffer
|
||||
@@ -18,17 +27,12 @@ type MockAxiosError = Error & {
|
||||
|
||||
let getMock: (url: string) => Promise<MockAxiosResponse>
|
||||
|
||||
mock.module('axios', () => {
|
||||
const axiosMock = {
|
||||
get: (url: string) => getMock(url),
|
||||
isAxiosError: (error: unknown): error is MockAxiosError =>
|
||||
typeof error === 'object' &&
|
||||
error !== null &&
|
||||
(error as { isAxiosError?: unknown }).isAxiosError === true,
|
||||
}
|
||||
|
||||
return { default: axiosMock }
|
||||
})
|
||||
const axiosHandle = setupAxiosMock()
|
||||
axiosHandle.stubs.get = (url: string) => getMock(url)
|
||||
axiosHandle.stubs.isAxiosError = (error: unknown): boolean =>
|
||||
typeof error === 'object' &&
|
||||
error !== null &&
|
||||
(error as { isAxiosError?: unknown }).isAxiosError === true
|
||||
|
||||
mock.module('src/services/analytics/index.js', () => ({
|
||||
logEvent: () => {},
|
||||
@@ -67,6 +71,14 @@ beforeEach(() => {
|
||||
})
|
||||
})
|
||||
|
||||
beforeAll(() => {
|
||||
axiosHandle.useStubs = true
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
axiosHandle.useStubs = false
|
||||
})
|
||||
|
||||
describe('WebFetch response headers', () => {
|
||||
test('reads redirect Location from AxiosHeaders-style get()', async () => {
|
||||
getMock = async () => {
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { describe, expect, mock, test } from 'bun:test'
|
||||
import { afterAll, describe, expect, mock, test } from 'bun:test'
|
||||
import { setupAxiosMock } from '../../../../../../tests/mocks/axios'
|
||||
|
||||
// Each test below calls `mock.module('axios', ...)` per-test. Re-register a
|
||||
// spread-real axios mock at end-of-file so the per-test stubs do not leak
|
||||
// into subsequent test files (mock.module is process-global, last-write-wins).
|
||||
afterAll(() => {
|
||||
setupAxiosMock()
|
||||
})
|
||||
|
||||
const _abortMock = () => ({
|
||||
AbortError: class AbortError extends Error {
|
||||
|
||||
@@ -1,4 +1,22 @@
|
||||
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
|
||||
import {
|
||||
afterAll,
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
mock,
|
||||
test,
|
||||
} from 'bun:test'
|
||||
import { setupAxiosMock } from '../../../../../../tests/mocks/axios'
|
||||
|
||||
// Each test below calls `mock.module('axios', ...)` per-test. Without an
|
||||
// afterAll cleanup, the LAST per-test stub leaks into every test file that
|
||||
// runs after this one (mock.module is process-global, last-write-wins). The
|
||||
// spread-real mock registered here at the end re-routes axios to the real
|
||||
// module, undoing the stub leakage so later suites see real axios.
|
||||
afterAll(() => {
|
||||
setupAxiosMock()
|
||||
})
|
||||
|
||||
// Defensive mock: agent.test.ts mocks config.js which can corrupt Bun's
|
||||
// src/* path alias resolution. Provide AbortError directly so the dynamic
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { afterEach, describe, expect, mock, test } from 'bun:test'
|
||||
import { afterAll, afterEach, describe, expect, mock, test } from 'bun:test'
|
||||
import { setupAxiosMock } from '../../../../../../tests/mocks/axios'
|
||||
|
||||
// Each test below calls `mock.module('axios', ...)` per-test. Re-register a
|
||||
// spread-real axios mock at end-of-file so the per-test stubs do not leak
|
||||
// into subsequent test files (mock.module is process-global, last-write-wins).
|
||||
afterAll(() => {
|
||||
setupAxiosMock()
|
||||
})
|
||||
|
||||
const _abortMock = () => ({
|
||||
AbortError: class AbortError extends Error {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
afterAll,
|
||||
afterEach,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
} from 'bun:test'
|
||||
import { debugMock } from '../../../../tests/mocks/debug.js'
|
||||
import { logMock } from '../../../../tests/mocks/log.js'
|
||||
import { setupAxiosMock } from '../../../../tests/mocks/axios.js'
|
||||
|
||||
// Mock side-effect modules first
|
||||
mock.module('src/utils/log.ts', logMock)
|
||||
@@ -48,15 +50,11 @@ const axiosIsAxiosError = mock((err: unknown) => {
|
||||
)
|
||||
})
|
||||
|
||||
mock.module('axios', () => ({
|
||||
default: {
|
||||
get: axiosGetMock,
|
||||
post: axiosPostMock,
|
||||
delete: axiosDeleteMock,
|
||||
isAxiosError: axiosIsAxiosError,
|
||||
},
|
||||
isAxiosError: axiosIsAxiosError,
|
||||
}))
|
||||
const axiosHandle = setupAxiosMock()
|
||||
axiosHandle.stubs.get = axiosGetMock
|
||||
axiosHandle.stubs.post = axiosPostMock
|
||||
axiosHandle.stubs.delete = axiosDeleteMock
|
||||
axiosHandle.stubs.isAxiosError = axiosIsAxiosError
|
||||
|
||||
// Lazy import after mocks are in place
|
||||
let listAgents: typeof import('../agentsApi.js').listAgents
|
||||
@@ -65,6 +63,7 @@ let deleteAgent: typeof import('../agentsApi.js').deleteAgent
|
||||
let runAgent: typeof import('../agentsApi.js').runAgent
|
||||
|
||||
beforeAll(async () => {
|
||||
axiosHandle.useStubs = true
|
||||
const mod = await import('../agentsApi.js')
|
||||
listAgents = mod.listAgents
|
||||
createAgent = mod.createAgent
|
||||
@@ -72,6 +71,10 @@ beforeAll(async () => {
|
||||
runAgent = mod.runAgent
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
axiosHandle.useStubs = false
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
axiosGetMock.mockClear()
|
||||
axiosPostMock.mockClear()
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
afterAll,
|
||||
afterEach,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
} from 'bun:test'
|
||||
import { debugMock } from '../../../../tests/mocks/debug.js'
|
||||
import { logMock } from '../../../../tests/mocks/log.js'
|
||||
import { setupAxiosMock } from '../../../../tests/mocks/axios.js'
|
||||
|
||||
mock.module('src/utils/log.ts', logMock)
|
||||
mock.module('src/utils/debug.ts', debugMock)
|
||||
@@ -60,16 +62,12 @@ const axiosIsAxiosError = mock((err: unknown) => {
|
||||
)
|
||||
})
|
||||
|
||||
mock.module('axios', () => ({
|
||||
default: {
|
||||
get: axiosGetMock,
|
||||
post: axiosPostMock,
|
||||
patch: axiosPatchMock,
|
||||
delete: axiosDeleteMock,
|
||||
isAxiosError: axiosIsAxiosError,
|
||||
},
|
||||
isAxiosError: axiosIsAxiosError,
|
||||
}))
|
||||
const axiosHandle = setupAxiosMock()
|
||||
axiosHandle.stubs.get = axiosGetMock
|
||||
axiosHandle.stubs.post = axiosPostMock
|
||||
axiosHandle.stubs.patch = axiosPatchMock
|
||||
axiosHandle.stubs.delete = axiosDeleteMock
|
||||
axiosHandle.stubs.isAxiosError = axiosIsAxiosError
|
||||
|
||||
// ── Lazy import after mocks ─────────────────────────────────────────────────
|
||||
let listStores: typeof import('../memoryStoresApi.js').listStores
|
||||
@@ -85,6 +83,7 @@ let listVersions: typeof import('../memoryStoresApi.js').listVersions
|
||||
let redactVersion: typeof import('../memoryStoresApi.js').redactVersion
|
||||
|
||||
beforeAll(async () => {
|
||||
axiosHandle.useStubs = true
|
||||
const mod = await import('../memoryStoresApi.js')
|
||||
listStores = mod.listStores
|
||||
getStore = mod.getStore
|
||||
@@ -99,6 +98,10 @@ beforeAll(async () => {
|
||||
redactVersion = mod.redactVersion
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
axiosHandle.useStubs = false
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
axiosGetMock.mockClear()
|
||||
axiosPostMock.mockClear()
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
|
||||
import { afterAll, afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
|
||||
import * as React from 'react';
|
||||
import { logMock } from '../../../../tests/mocks/log';
|
||||
import { debugMock } from '../../../../tests/mocks/debug';
|
||||
|
||||
// Pre-import real ink so we can fall through after this suite. Bun's
|
||||
// mock.module is process-global / last-write-wins; without delegation the
|
||||
// stub Box/Pane/Text/useTheme leak into other test files (e.g.
|
||||
// AgentsPlatformView.test.tsx) that need real ink components.
|
||||
const _realOnboardingInkMod = (await import('@anthropic/ink')) as Record<string, unknown>;
|
||||
let _useStubInkForOnboarding = true;
|
||||
afterAll(() => {
|
||||
_useStubInkForOnboarding = false;
|
||||
});
|
||||
|
||||
mock.module('bun:bundle', () => ({
|
||||
feature: (_name: string) => false,
|
||||
}));
|
||||
@@ -37,13 +47,20 @@ mock.module('src/utils/config.js', () => ({
|
||||
}));
|
||||
|
||||
// Stub heavy theme + ink imports — the launcher only references them for
|
||||
// the `theme` subcommand JSX render path.
|
||||
mock.module('@anthropic/ink', () => ({
|
||||
Box: ({ children }: { children?: React.ReactNode }) => React.createElement('box', null, children),
|
||||
Pane: ({ children }: { children?: React.ReactNode }) => React.createElement('pane', null, children),
|
||||
Text: ({ children }: { children?: React.ReactNode }) => React.createElement('text', null, children),
|
||||
useTheme: () => ['dark', (_t: string) => undefined],
|
||||
}));
|
||||
// the `theme` subcommand JSX render path. Spread real ink so when the flag
|
||||
// flips off in afterAll, later test files see real components.
|
||||
mock.module('@anthropic/ink', () => {
|
||||
if (_useStubInkForOnboarding) {
|
||||
return {
|
||||
..._realOnboardingInkMod,
|
||||
Box: ({ children }: { children?: React.ReactNode }) => React.createElement('box', null, children),
|
||||
Pane: ({ children }: { children?: React.ReactNode }) => React.createElement('pane', null, children),
|
||||
Text: ({ children }: { children?: React.ReactNode }) => React.createElement('text', null, children),
|
||||
useTheme: () => ['dark', (_t: string) => undefined],
|
||||
};
|
||||
}
|
||||
return _realOnboardingInkMod;
|
||||
});
|
||||
|
||||
mock.module('src/components/ThemePicker.js', () => ({
|
||||
ThemePicker: () => React.createElement('theme-picker'),
|
||||
|
||||
@@ -15,17 +15,26 @@
|
||||
import { afterAll, describe, expect, mock, test } from 'bun:test';
|
||||
import { debugMock } from '../../../../tests/mocks/debug.js';
|
||||
import { logMock } from '../../../../tests/mocks/log.js';
|
||||
import { setupAxiosMock } from '../../../../tests/mocks/axios.js';
|
||||
|
||||
// Pre-import the real react module so we can delegate after this suite.
|
||||
// Bun's mock.module is process-global / last-write-wins; without delegation
|
||||
// the stub createElement leaks into other test files (e.g.
|
||||
// SnapshotUpdateDialog.test.tsx) that need real React.createElement.
|
||||
// Pre-import the real react and ink modules so we can delegate after this
|
||||
// suite. Bun's mock.module is process-global / last-write-wins; without
|
||||
// delegation the stub createElement / stub ink components leak into other
|
||||
// test files (e.g. SnapshotUpdateDialog.test.tsx, AgentsPlatformView.test.tsx)
|
||||
// that need real React.createElement and real Box/Text components.
|
||||
const _realReactMod = (await import('react')) as Record<string, unknown> & {
|
||||
default?: Record<string, unknown>;
|
||||
};
|
||||
const _realInkMod = (await import('@anthropic/ink')) as Record<string, unknown>;
|
||||
let _useStubReactForUltrareview = true;
|
||||
let _useStubInkForUltrareview = true;
|
||||
afterAll(() => {
|
||||
_useStubReactForUltrareview = false;
|
||||
_useStubInkForUltrareview = false;
|
||||
// The handle reference exists by the time afterAll runs (TDZ resolves via
|
||||
// closure). Flip useStubs off so the spread-real fall-through kicks in for
|
||||
// any test file that runs after this one in the same process.
|
||||
_ultrareviewAxiosHandle.useStubs = false;
|
||||
});
|
||||
|
||||
// Mock dependency chain before any subject import
|
||||
@@ -79,14 +88,15 @@ const mockAxiosPost = mock(
|
||||
}),
|
||||
);
|
||||
|
||||
mock.module('axios', () => {
|
||||
const axiosMock = {
|
||||
post: mockAxiosPost,
|
||||
isAxiosError: (e: unknown) =>
|
||||
typeof e === 'object' && e !== null && (e as { isAxiosError?: boolean }).isAxiosError === true,
|
||||
};
|
||||
return { default: axiosMock, ...axiosMock };
|
||||
});
|
||||
// Spread real axios + flag-gate stubs so the per-test mockAxiosPost stops
|
||||
// leaking into later test files (mock.module is process-global). Default ON
|
||||
// for this suite; afterAll above flips _useStubReactForUltrareview, but here
|
||||
// we tie axios cleanup to the helper's own flag — see suite-level afterAll.
|
||||
const _ultrareviewAxiosHandle = setupAxiosMock();
|
||||
_ultrareviewAxiosHandle.useStubs = true;
|
||||
_ultrareviewAxiosHandle.stubs.post = mockAxiosPost;
|
||||
_ultrareviewAxiosHandle.stubs.isAxiosError = (e: unknown) =>
|
||||
typeof e === 'object' && e !== null && (e as { isAxiosError?: boolean }).isAxiosError === true;
|
||||
|
||||
// Mock detectCurrentRepositoryWithHost
|
||||
mock.module('src/utils/detectRepository.js', () => ({
|
||||
@@ -128,11 +138,21 @@ mock.module('react', () => {
|
||||
};
|
||||
});
|
||||
|
||||
mock.module('@anthropic/ink', () => ({
|
||||
Box: 'Box',
|
||||
Dialog: 'Dialog',
|
||||
Text: 'Text',
|
||||
}));
|
||||
// Spread real ink + flag-gate the stub components. Without spread, the bare
|
||||
// { Box: 'Box', Dialog: 'Dialog', Text: 'Text' } leaks into every later test
|
||||
// file (e.g. AgentsPlatformView.test.tsx) that imports @anthropic/ink — those
|
||||
// consumers receive strings instead of real components and rendering breaks.
|
||||
mock.module('@anthropic/ink', () => {
|
||||
if (_useStubInkForUltrareview) {
|
||||
return {
|
||||
..._realInkMod,
|
||||
Box: 'Box',
|
||||
Dialog: 'Dialog',
|
||||
Text: 'Text',
|
||||
};
|
||||
}
|
||||
return _realInkMod;
|
||||
});
|
||||
|
||||
mock.module('src/components/CustomSelect/select.js', () => ({
|
||||
Select: 'Select',
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
afterAll,
|
||||
afterEach,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
} from 'bun:test'
|
||||
import { debugMock } from '../../../../tests/mocks/debug.js'
|
||||
import { logMock } from '../../../../tests/mocks/log.js'
|
||||
import { setupAxiosMock } from '../../../../tests/mocks/axios.js'
|
||||
|
||||
mock.module('src/utils/log.ts', logMock)
|
||||
mock.module('src/utils/debug.ts', debugMock)
|
||||
@@ -57,15 +59,11 @@ const axiosIsAxiosError = mock((err: unknown) => {
|
||||
)
|
||||
})
|
||||
|
||||
mock.module('axios', () => ({
|
||||
default: {
|
||||
get: axiosGetMock,
|
||||
post: axiosPostMock,
|
||||
delete: axiosDeleteMock,
|
||||
isAxiosError: axiosIsAxiosError,
|
||||
},
|
||||
isAxiosError: axiosIsAxiosError,
|
||||
}))
|
||||
const axiosHandle = setupAxiosMock()
|
||||
axiosHandle.stubs.get = axiosGetMock
|
||||
axiosHandle.stubs.post = axiosPostMock
|
||||
axiosHandle.stubs.delete = axiosDeleteMock
|
||||
axiosHandle.stubs.isAxiosError = axiosIsAxiosError
|
||||
|
||||
// ── Lazy import after mocks ─────────────────────────────────────────────────
|
||||
// Use the src/ alias path (same canonical key used in launchSchedule.test.ts mock)
|
||||
@@ -79,6 +77,7 @@ let deleteTrigger: typeof import('../triggersApi.js').deleteTrigger
|
||||
let runTrigger: typeof import('../triggersApi.js').runTrigger
|
||||
|
||||
beforeAll(async () => {
|
||||
axiosHandle.useStubs = true
|
||||
const mod = await import('../triggersApi.js')
|
||||
listTriggers = mod.listTriggers
|
||||
getTrigger = mod.getTrigger
|
||||
@@ -88,6 +87,10 @@ beforeAll(async () => {
|
||||
runTrigger = mod.runTrigger
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
axiosHandle.useStubs = false
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
axiosGetMock.mockClear()
|
||||
axiosPostMock.mockClear()
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
afterAll,
|
||||
afterEach,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
} from 'bun:test'
|
||||
import { debugMock } from '../../../../tests/mocks/debug.js'
|
||||
import { logMock } from '../../../../tests/mocks/log.js'
|
||||
import { setupAxiosMock } from '../../../../tests/mocks/axios.js'
|
||||
|
||||
mock.module('src/utils/log.ts', logMock)
|
||||
mock.module('src/utils/debug.ts', debugMock)
|
||||
@@ -62,15 +64,11 @@ const axiosIsAxiosError = mock((err: unknown) => {
|
||||
)
|
||||
})
|
||||
|
||||
mock.module('axios', () => ({
|
||||
default: {
|
||||
get: axiosGetMock,
|
||||
post: axiosPostMock,
|
||||
delete: axiosDeleteMock,
|
||||
isAxiosError: axiosIsAxiosError,
|
||||
},
|
||||
isAxiosError: axiosIsAxiosError,
|
||||
}))
|
||||
const axiosHandle = setupAxiosMock()
|
||||
axiosHandle.stubs.get = axiosGetMock
|
||||
axiosHandle.stubs.post = axiosPostMock
|
||||
axiosHandle.stubs.delete = axiosDeleteMock
|
||||
axiosHandle.stubs.isAxiosError = axiosIsAxiosError
|
||||
|
||||
// ── Lazy import after mocks ─────────────────────────────────────────────────
|
||||
let listSkills: typeof import('../skillsApi.js').listSkills
|
||||
@@ -81,6 +79,7 @@ let createSkill: typeof import('../skillsApi.js').createSkill
|
||||
let deleteSkill: typeof import('../skillsApi.js').deleteSkill
|
||||
|
||||
beforeAll(async () => {
|
||||
axiosHandle.useStubs = true
|
||||
const mod = await import('../skillsApi.js')
|
||||
listSkills = mod.listSkills
|
||||
getSkill = mod.getSkill
|
||||
@@ -90,6 +89,10 @@ beforeAll(async () => {
|
||||
deleteSkill = mod.deleteSkill
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
axiosHandle.useStubs = false
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
axiosGetMock.mockClear()
|
||||
axiosPostMock.mockClear()
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from 'bun:test'
|
||||
import { debugMock } from '../../../../tests/mocks/debug.js'
|
||||
import { logMock } from '../../../../tests/mocks/log.js'
|
||||
import { setupAxiosMock } from '../../../../tests/mocks/axios.js'
|
||||
|
||||
mock.module('src/utils/log.ts', logMock)
|
||||
mock.module('src/utils/debug.ts', debugMock)
|
||||
@@ -73,15 +74,11 @@ const axiosIsAxiosError = mock((err: unknown) => {
|
||||
)
|
||||
})
|
||||
|
||||
mock.module('axios', () => ({
|
||||
default: {
|
||||
get: axiosGetMock,
|
||||
post: axiosPostMock,
|
||||
delete: axiosDeleteMock,
|
||||
isAxiosError: axiosIsAxiosError,
|
||||
},
|
||||
isAxiosError: axiosIsAxiosError,
|
||||
}))
|
||||
const axiosHandle = setupAxiosMock()
|
||||
axiosHandle.stubs.get = axiosGetMock
|
||||
axiosHandle.stubs.post = axiosPostMock
|
||||
axiosHandle.stubs.delete = axiosDeleteMock
|
||||
axiosHandle.stubs.isAxiosError = axiosIsAxiosError
|
||||
|
||||
// ── fs/promises mock ─────────────────────────────────────────────────────────
|
||||
// Bun's mock.module is global per-process and last-write-wins. Replacing
|
||||
@@ -119,6 +116,7 @@ let getClaudeConfigHomeDir: typeof import('../../../utils/envUtils.js').getClaud
|
||||
let origConfigDir: string | undefined
|
||||
|
||||
beforeAll(async () => {
|
||||
axiosHandle.useStubs = true
|
||||
const mod = await import('../launchSkillStore.js')
|
||||
callSkillStore = mod.callSkillStore
|
||||
const envMod = await import('../../../utils/envUtils.js')
|
||||
@@ -130,6 +128,7 @@ beforeAll(async () => {
|
||||
// Flip the stub flag off after this suite so localVault/store and other
|
||||
// fs-dependent tests in the same process see real readFile/readdir/etc.
|
||||
afterAll(() => {
|
||||
axiosHandle.useStubs = false
|
||||
useSkillStoreFsStubs = false
|
||||
})
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
afterAll,
|
||||
afterEach,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
} from 'bun:test'
|
||||
import { debugMock } from '../../../../tests/mocks/debug.js'
|
||||
import { logMock } from '../../../../tests/mocks/log.js'
|
||||
import { setupAxiosMock } from '../../../../tests/mocks/axios.js'
|
||||
|
||||
mock.module('src/utils/log.ts', logMock)
|
||||
mock.module('src/utils/debug.ts', debugMock)
|
||||
@@ -60,15 +62,11 @@ const axiosIsAxiosError = mock((err: unknown) => {
|
||||
)
|
||||
})
|
||||
|
||||
mock.module('axios', () => ({
|
||||
default: {
|
||||
get: axiosGetMock,
|
||||
post: axiosPostMock,
|
||||
delete: axiosDeleteMock,
|
||||
isAxiosError: axiosIsAxiosError,
|
||||
},
|
||||
isAxiosError: axiosIsAxiosError,
|
||||
}))
|
||||
const axiosHandle = setupAxiosMock()
|
||||
axiosHandle.stubs.get = axiosGetMock
|
||||
axiosHandle.stubs.post = axiosPostMock
|
||||
axiosHandle.stubs.delete = axiosDeleteMock
|
||||
axiosHandle.stubs.isAxiosError = axiosIsAxiosError
|
||||
|
||||
// ── Lazy import after mocks ─────────────────────────────────────────────────
|
||||
let listVaults: typeof import('../vaultsApi.js').listVaults
|
||||
@@ -80,6 +78,7 @@ let addCredential: typeof import('../vaultsApi.js').addCredential
|
||||
let archiveCredential: typeof import('../vaultsApi.js').archiveCredential
|
||||
|
||||
beforeAll(async () => {
|
||||
axiosHandle.useStubs = true
|
||||
const mod = await import('../vaultsApi.js')
|
||||
listVaults = mod.listVaults
|
||||
createVault = mod.createVault
|
||||
@@ -90,6 +89,10 @@ beforeAll(async () => {
|
||||
archiveCredential = mod.archiveCredential
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
axiosHandle.useStubs = false
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
axiosGetMock.mockClear()
|
||||
axiosPostMock.mockClear()
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
afterAll,
|
||||
afterEach,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
} from 'bun:test'
|
||||
import { debugMock } from '../../../../tests/mocks/debug.js'
|
||||
import { logMock } from '../../../../tests/mocks/log.js'
|
||||
import { setupAxiosMock } from '../../../../tests/mocks/axios.js'
|
||||
|
||||
mock.module('src/utils/log.ts', logMock)
|
||||
mock.module('src/utils/debug.ts', debugMock)
|
||||
@@ -51,24 +53,27 @@ const axiosIsAxiosError = mock((err: unknown) => {
|
||||
)
|
||||
})
|
||||
|
||||
mock.module('axios', () => ({
|
||||
default: {
|
||||
get: axiosGetMock,
|
||||
post: axiosPostMock,
|
||||
delete: mock(async () => ({})),
|
||||
isAxiosError: axiosIsAxiosError,
|
||||
},
|
||||
isAxiosError: axiosIsAxiosError,
|
||||
}))
|
||||
const axiosDeleteMock = mock(async () => ({}))
|
||||
|
||||
const axiosHandle = setupAxiosMock()
|
||||
axiosHandle.stubs.get = axiosGetMock
|
||||
axiosHandle.stubs.post = axiosPostMock
|
||||
axiosHandle.stubs.delete = axiosDeleteMock
|
||||
axiosHandle.stubs.isAxiosError = axiosIsAxiosError
|
||||
|
||||
// ── Lazy import after mocks ─────────────────────────────────────────────────
|
||||
let callVault: typeof import('../launchVault.js').callVault
|
||||
|
||||
beforeAll(async () => {
|
||||
axiosHandle.useStubs = true
|
||||
const mod = await import('../launchVault.js')
|
||||
callVault = mod.callVault
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
axiosHandle.useStubs = false
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
axiosGetMock.mockClear()
|
||||
axiosPostMock.mockClear()
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
* Verifies all three action enum states (proceed/confirm/blocked),
|
||||
* network/HTTP error handling, and Zod schema mismatch fallback.
|
||||
*/
|
||||
import { describe, expect, mock, test } from 'bun:test'
|
||||
import { afterAll, beforeAll, describe, expect, mock, test } from 'bun:test'
|
||||
import { debugMock } from '../../../../tests/mocks/debug.js'
|
||||
import { logMock } from '../../../../tests/mocks/log.js'
|
||||
import { setupAxiosMock } from '../../../../tests/mocks/axios.js'
|
||||
|
||||
// Mock dependency chain before any subject import
|
||||
mock.module('src/utils/debug.ts', debugMock)
|
||||
@@ -46,15 +47,19 @@ const mockAxiosPost = mock(async (..._args: any[]): Promise<any> => {
|
||||
throw new Error('not configured')
|
||||
})
|
||||
|
||||
mock.module('axios', () => {
|
||||
const axiosMock = {
|
||||
post: mockAxiosPost,
|
||||
isAxiosError: (e: unknown) =>
|
||||
typeof e === 'object' &&
|
||||
e !== null &&
|
||||
(e as { isAxiosError?: boolean }).isAxiosError === true,
|
||||
}
|
||||
return { default: axiosMock, ...axiosMock }
|
||||
const axiosHandle = setupAxiosMock()
|
||||
axiosHandle.stubs.post = mockAxiosPost
|
||||
axiosHandle.stubs.isAxiosError = (e: unknown) =>
|
||||
typeof e === 'object' &&
|
||||
e !== null &&
|
||||
(e as { isAxiosError?: boolean }).isAxiosError === true
|
||||
|
||||
beforeAll(() => {
|
||||
axiosHandle.useStubs = true
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
axiosHandle.useStubs = false
|
||||
})
|
||||
|
||||
import {
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
import { mock, describe, expect, test, afterEach } from 'bun:test'
|
||||
import {
|
||||
mock,
|
||||
describe,
|
||||
expect,
|
||||
test,
|
||||
afterEach,
|
||||
beforeAll,
|
||||
afterAll,
|
||||
} from 'bun:test'
|
||||
import { debugMock } from '../../../../tests/mocks/debug'
|
||||
import { setupAxiosMock } from '../../../../tests/mocks/axios.js'
|
||||
|
||||
const axiosHandle = setupAxiosMock()
|
||||
axiosHandle.stubs.get = async () => ({ data: { servers: [] } })
|
||||
|
||||
beforeAll(() => {
|
||||
axiosHandle.useStubs = true
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
axiosHandle.useStubs = false
|
||||
})
|
||||
|
||||
mock.module('axios', () => ({
|
||||
default: { get: async () => ({ data: { servers: [] } }) },
|
||||
}))
|
||||
mock.module('src/utils/debug.ts', debugMock)
|
||||
|
||||
const { isOfficialMcpUrl, resetOfficialMcpUrlsForTesting } = await import(
|
||||
|
||||
141
tests/mocks/axios.ts
Normal file
141
tests/mocks/axios.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
Reference in New Issue
Block a user