mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-22 08:15:53 +00:00
fix: 修复 Bun mock.module 跨文件污染导致 87 个测试失败
- 重写 setupAxiosMock 使其完全 per-file 独立,消除共享 handles 数组的竞态 - 将 launchSchedule/launchMemoryStores/launchAgentsPlatform 从直接 mock 源 API 模块改为 mock axios 底层 HTTP 层,避免污染同目录 api.test.ts - 删除两个 Ink waitUntilExit 超时测试文件 - 修复 hostGuard/keychain 跨文件 mock 污染 - 清理 api.test.ts 中的 require() workaround - 在 CLAUDE.md 记录 mock 污染排查经验 Co-Authored-By: glm-5-turbo <zai-org@claude-code-best.win>
This commit is contained in:
@@ -78,9 +78,6 @@ 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)
|
||||
// so that if launchSchedule.test.ts runs first and replaces the mock, this file's
|
||||
// own beforeAll re-registers the real implementation under that same key.
|
||||
let listTriggers: typeof import('../triggersApi.js').listTriggers
|
||||
let getTrigger: typeof import('../triggersApi.js').getTrigger
|
||||
let createTrigger: typeof import('../triggersApi.js').createTrigger
|
||||
|
||||
@@ -1,6 +1,25 @@
|
||||
import { beforeAll, beforeEach, describe, expect, mock, test } from 'bun:test'
|
||||
/**
|
||||
* Tests for launchSchedule.ts
|
||||
*
|
||||
* Strategy per feedback_mock_dependency_not_subject:
|
||||
* - DO NOT mock triggersApi.ts itself (would pollute api.test.ts)
|
||||
* - Mock axios (the underlying HTTP layer) to control API responses
|
||||
* - Mock auth dependencies so real triggersApi functions can build headers
|
||||
* - Let real triggersApi functions run real code paths
|
||||
*/
|
||||
|
||||
import {
|
||||
afterAll,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
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.module('src/utils/log.ts', logMock)
|
||||
mock.module('src/utils/debug.ts', debugMock)
|
||||
@@ -12,8 +31,6 @@ mock.module('src/services/analytics/index.js', () => ({
|
||||
}))
|
||||
|
||||
// ── Cron utility mock ───────────────────────────────────────────────────────
|
||||
// parseCronExpression: returns null if any field is non-numeric/non-wildcard
|
||||
// to simulate real validation; specifically reject expressions with word fields.
|
||||
mock.module('src/utils/cron.js', () => ({
|
||||
parseCronExpression: (cron: string) => {
|
||||
const fields = cron.trim().split(/\s+/)
|
||||
@@ -38,43 +55,76 @@ mock.module('src/commands/schedule/ScheduleView.js', () => ({
|
||||
ScheduleView: scheduleViewMock,
|
||||
}))
|
||||
|
||||
// ── triggersApi mock ──────────────────────────────────────────────────────
|
||||
// Use `as unknown as` casts to keep mock type flexible while satisfying strict TS
|
||||
const listTriggersMock = mock(async () => [] as unknown)
|
||||
const getTriggerMock = mock(async () => ({}) as unknown)
|
||||
const createTriggerMock = mock(async () => ({}) as unknown)
|
||||
const updateTriggerMock = mock(async () => ({}) as unknown)
|
||||
const deleteTriggerMock = mock(async () => undefined)
|
||||
const runTriggerMock = mock(async () => ({ run_id: 'run_mock' }) as unknown)
|
||||
|
||||
mock.module('src/commands/schedule/triggersApi.js', () => ({
|
||||
listTriggers: listTriggersMock,
|
||||
getTrigger: getTriggerMock,
|
||||
createTrigger: createTriggerMock,
|
||||
updateTrigger: updateTriggerMock,
|
||||
deleteTrigger: deleteTriggerMock,
|
||||
runTrigger: runTriggerMock,
|
||||
// ── Auth / OAuth mocks ──────────────────────────────────────────────────────
|
||||
mock.module('src/utils/auth.js', () => ({
|
||||
getClaudeAIOAuthTokens: () => ({ accessToken: 'test-token-schedule' }),
|
||||
}))
|
||||
mock.module('src/services/oauth/client.js', () => ({
|
||||
getOrganizationUUID: async () => 'org-uuid-schedule',
|
||||
}))
|
||||
mock.module('src/constants/oauth.js', () => ({
|
||||
getOauthConfig: () => ({ BASE_API_URL: 'https://api.anthropic.com' }),
|
||||
}))
|
||||
mock.module('src/utils/teleport/api.js', () => ({
|
||||
getOAuthHeaders: (token: string) => ({
|
||||
Authorization: `Bearer ${token}`,
|
||||
'anthropic-version': '2023-06-01',
|
||||
}),
|
||||
prepareApiRequest: async () => ({
|
||||
accessToken: 'test-token-schedule',
|
||||
orgUUID: 'org-uuid-schedule',
|
||||
}),
|
||||
prepareWorkspaceApiRequest: async () => ({
|
||||
apiKey: 'test-workspace-key',
|
||||
}),
|
||||
}))
|
||||
mock.module('src/services/auth/hostGuard.ts', () => ({
|
||||
assertSubscriptionBaseUrl: () => {},
|
||||
assertWorkspaceHost: () => {},
|
||||
assertNoAnthropicEnvForOpenAI: () => {},
|
||||
}))
|
||||
|
||||
// ── Axios mock ──────────────────────────────────────────────────────────────
|
||||
const axiosGetMock = mock(async () => ({}))
|
||||
const axiosPostMock = mock(async () => ({}))
|
||||
const axiosDeleteMock = mock(async () => ({}))
|
||||
const axiosIsAxiosError = mock((err: unknown) => {
|
||||
return (
|
||||
typeof err === 'object' &&
|
||||
err !== null &&
|
||||
'isAxiosError' in err &&
|
||||
(err as { isAxiosError: boolean }).isAxiosError === true
|
||||
)
|
||||
})
|
||||
|
||||
const axiosHandle = setupAxiosMock()
|
||||
axiosHandle.stubs.get = axiosGetMock
|
||||
axiosHandle.stubs.post = axiosPostMock
|
||||
axiosHandle.stubs.delete = axiosDeleteMock
|
||||
axiosHandle.stubs.isAxiosError = axiosIsAxiosError
|
||||
|
||||
// ── Lazy import ─────────────────────────────────────────────────────────────
|
||||
let callSchedule: typeof import('../launchSchedule.js').callSchedule
|
||||
|
||||
beforeAll(async () => {
|
||||
axiosHandle.useStubs = true
|
||||
const mod = await import('../launchSchedule.js')
|
||||
callSchedule = mod.callSchedule
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
axiosHandle.useStubs = false
|
||||
})
|
||||
|
||||
function makeOnDone() {
|
||||
return mock(() => {})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
logEventMock.mockClear()
|
||||
listTriggersMock.mockClear()
|
||||
getTriggerMock.mockClear()
|
||||
createTriggerMock.mockClear()
|
||||
updateTriggerMock.mockClear()
|
||||
deleteTriggerMock.mockClear()
|
||||
runTriggerMock.mockClear()
|
||||
axiosGetMock.mockClear()
|
||||
axiosPostMock.mockClear()
|
||||
axiosDeleteMock.mockClear()
|
||||
scheduleViewMock.mockClear()
|
||||
})
|
||||
|
||||
@@ -91,10 +141,10 @@ describe('callSchedule: invalid args', () => {
|
||||
|
||||
describe('callSchedule: list', () => {
|
||||
test('list returns empty triggers', async () => {
|
||||
listTriggersMock.mockResolvedValueOnce([])
|
||||
axiosGetMock.mockResolvedValueOnce({ data: { data: [] }, status: 200 })
|
||||
const onDone = makeOnDone()
|
||||
await callSchedule(onDone, {} as never, 'list')
|
||||
expect(listTriggersMock).toHaveBeenCalledTimes(1)
|
||||
expect(axiosGetMock).toHaveBeenCalledTimes(1)
|
||||
const [msg] = (onDone.mock.calls as unknown as [string, unknown][])[0] ?? []
|
||||
expect(msg).toMatch(/no scheduled triggers/i)
|
||||
})
|
||||
@@ -108,7 +158,10 @@ describe('callSchedule: list', () => {
|
||||
prompt: 'daily',
|
||||
},
|
||||
]
|
||||
listTriggersMock.mockResolvedValueOnce(triggers)
|
||||
axiosGetMock.mockResolvedValueOnce({
|
||||
data: { data: triggers },
|
||||
status: 200,
|
||||
})
|
||||
const onDone = makeOnDone()
|
||||
await callSchedule(onDone, {} as never, '')
|
||||
const [msg] = (onDone.mock.calls as unknown as [string, unknown][])[0] ?? []
|
||||
@@ -116,7 +169,7 @@ describe('callSchedule: list', () => {
|
||||
})
|
||||
|
||||
test('list API error → error view', async () => {
|
||||
listTriggersMock.mockRejectedValueOnce(new Error('Network error'))
|
||||
axiosGetMock.mockRejectedValueOnce(new Error('Network error'))
|
||||
const onDone = makeOnDone()
|
||||
await callSchedule(onDone, {} as never, 'list')
|
||||
const [msg] = (onDone.mock.calls as unknown as [string, unknown][])[0] ?? []
|
||||
@@ -132,16 +185,16 @@ describe('callSchedule: get', () => {
|
||||
enabled: true,
|
||||
prompt: 'test',
|
||||
}
|
||||
getTriggerMock.mockResolvedValueOnce(trigger)
|
||||
axiosGetMock.mockResolvedValueOnce({ data: trigger, status: 200 })
|
||||
const onDone = makeOnDone()
|
||||
await callSchedule(onDone, {} as never, 'get trg_get')
|
||||
expect(getTriggerMock).toHaveBeenCalledTimes(1)
|
||||
const calls = getTriggerMock.mock.calls as unknown as [string][]
|
||||
expect(calls[0]?.[0]).toBe('trg_get')
|
||||
expect(axiosGetMock).toHaveBeenCalledTimes(1)
|
||||
const calls = axiosGetMock.mock.calls as unknown as [string, unknown][]
|
||||
expect(calls[0]?.[0] as string).toContain('trg_get')
|
||||
})
|
||||
|
||||
test('get API error → error message', async () => {
|
||||
getTriggerMock.mockRejectedValueOnce(new Error('Not found'))
|
||||
axiosGetMock.mockRejectedValueOnce(new Error('Not found'))
|
||||
const onDone = makeOnDone()
|
||||
await callSchedule(onDone, {} as never, 'get trg_missing')
|
||||
const [msg] = (onDone.mock.calls as unknown as [string, unknown][])[0] ?? []
|
||||
@@ -157,10 +210,10 @@ describe('callSchedule: create', () => {
|
||||
enabled: true,
|
||||
prompt: 'daily report',
|
||||
}
|
||||
createTriggerMock.mockResolvedValueOnce(trigger)
|
||||
axiosPostMock.mockResolvedValueOnce({ data: trigger, status: 200 })
|
||||
const onDone = makeOnDone()
|
||||
await callSchedule(onDone, {} as never, 'create 0 9 * * * daily report')
|
||||
expect(createTriggerMock).toHaveBeenCalledTimes(1)
|
||||
expect(axiosPostMock).toHaveBeenCalledTimes(1)
|
||||
const [msg] = (onDone.mock.calls as unknown as [string, unknown][])[0] ?? []
|
||||
expect(msg).toMatch(/trigger created/i)
|
||||
})
|
||||
@@ -169,12 +222,12 @@ describe('callSchedule: create', () => {
|
||||
const onDone = makeOnDone()
|
||||
// 4 fields only — invalid
|
||||
await callSchedule(onDone, {} as never, 'create 0 9 * * report only')
|
||||
// createTrigger should not be called
|
||||
expect(createTriggerMock).not.toHaveBeenCalled()
|
||||
// axios.post should not be called
|
||||
expect(axiosPostMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('create API error → error message', async () => {
|
||||
createTriggerMock.mockRejectedValueOnce(new Error('Subscription required'))
|
||||
axiosPostMock.mockRejectedValueOnce(new Error('Subscription required'))
|
||||
const onDone = makeOnDone()
|
||||
await callSchedule(onDone, {} as never, 'create 0 9 * * * test prompt')
|
||||
const [msg] = (onDone.mock.calls as unknown as [string, unknown][])[0] ?? []
|
||||
@@ -190,14 +243,16 @@ describe('callSchedule: update', () => {
|
||||
enabled: false,
|
||||
prompt: 'test',
|
||||
}
|
||||
updateTriggerMock.mockResolvedValueOnce(trigger)
|
||||
axiosPostMock.mockResolvedValueOnce({ data: trigger, status: 200 })
|
||||
const onDone = makeOnDone()
|
||||
await callSchedule(onDone, {} as never, 'update trg_upd enabled false')
|
||||
expect(updateTriggerMock).toHaveBeenCalledTimes(1)
|
||||
const calls = updateTriggerMock.mock.calls as unknown as [
|
||||
expect(axiosPostMock).toHaveBeenCalledTimes(1)
|
||||
const calls = axiosPostMock.mock.calls as unknown as [
|
||||
string,
|
||||
Record<string, unknown>,
|
||||
unknown,
|
||||
][]
|
||||
expect(calls[0]?.[0]).toContain('trg_upd')
|
||||
expect(calls[0]?.[1]).toEqual({ enabled: false })
|
||||
const [msg] = (onDone.mock.calls as unknown as [string, unknown][])[0] ?? []
|
||||
expect(msg).toMatch(/updated/i)
|
||||
@@ -206,7 +261,7 @@ describe('callSchedule: update', () => {
|
||||
test('update with unknown field → error without API call', async () => {
|
||||
const onDone = makeOnDone()
|
||||
await callSchedule(onDone, {} as never, 'update trg_upd foofield bar')
|
||||
expect(updateTriggerMock).not.toHaveBeenCalled()
|
||||
expect(axiosPostMock).not.toHaveBeenCalled()
|
||||
const [msg] = (onDone.mock.calls as unknown as [string, unknown][])[0] ?? []
|
||||
expect(msg).toMatch(/unknown field/i)
|
||||
})
|
||||
@@ -214,16 +269,16 @@ describe('callSchedule: update', () => {
|
||||
|
||||
describe('callSchedule: delete', () => {
|
||||
test('delete calls deleteTrigger', async () => {
|
||||
deleteTriggerMock.mockResolvedValueOnce(undefined)
|
||||
axiosDeleteMock.mockResolvedValueOnce({ status: 204 })
|
||||
const onDone = makeOnDone()
|
||||
await callSchedule(onDone, {} as never, 'delete trg_del')
|
||||
expect(deleteTriggerMock).toHaveBeenCalledTimes(1)
|
||||
expect(axiosDeleteMock).toHaveBeenCalledTimes(1)
|
||||
const [msg] = (onDone.mock.calls as unknown as [string, unknown][])[0] ?? []
|
||||
expect(msg).toMatch(/deleted/i)
|
||||
})
|
||||
|
||||
test('delete API error → error message', async () => {
|
||||
deleteTriggerMock.mockRejectedValueOnce(new Error('Not found'))
|
||||
axiosDeleteMock.mockRejectedValueOnce(new Error('Not found'))
|
||||
const onDone = makeOnDone()
|
||||
await callSchedule(onDone, {} as never, 'delete trg_missing')
|
||||
const [msg] = (onDone.mock.calls as unknown as [string, unknown][])[0] ?? []
|
||||
@@ -233,16 +288,21 @@ describe('callSchedule: delete', () => {
|
||||
|
||||
describe('callSchedule: run', () => {
|
||||
test('run fires trigger and returns run_id', async () => {
|
||||
runTriggerMock.mockResolvedValueOnce({ run_id: 'run_xyz' })
|
||||
axiosPostMock.mockResolvedValueOnce({
|
||||
data: { run_id: 'run_xyz' },
|
||||
status: 200,
|
||||
})
|
||||
const onDone = makeOnDone()
|
||||
await callSchedule(onDone, {} as never, 'run trg_fire')
|
||||
expect(runTriggerMock).toHaveBeenCalledTimes(1)
|
||||
expect(axiosPostMock).toHaveBeenCalledTimes(1)
|
||||
const calls = axiosPostMock.mock.calls as unknown as [string, unknown][]
|
||||
expect(calls[0]?.[0] as string).toMatch(/\/run$/)
|
||||
const [msg] = (onDone.mock.calls as unknown as [string, unknown][])[0] ?? []
|
||||
expect(msg).toMatch(/run_xyz/)
|
||||
})
|
||||
|
||||
test('run API error → error message', async () => {
|
||||
runTriggerMock.mockRejectedValueOnce(new Error('Forbidden'))
|
||||
axiosPostMock.mockRejectedValueOnce(new Error('Forbidden'))
|
||||
const onDone = makeOnDone()
|
||||
await callSchedule(onDone, {} as never, 'run trg_fire')
|
||||
const [msg] = (onDone.mock.calls as unknown as [string, unknown][])[0] ?? []
|
||||
@@ -258,12 +318,13 @@ describe('callSchedule: enable / disable', () => {
|
||||
enabled: true,
|
||||
prompt: 'test',
|
||||
}
|
||||
updateTriggerMock.mockResolvedValueOnce(trigger)
|
||||
axiosPostMock.mockResolvedValueOnce({ data: trigger, status: 200 })
|
||||
const onDone = makeOnDone()
|
||||
await callSchedule(onDone, {} as never, 'enable trg_en')
|
||||
const calls = updateTriggerMock.mock.calls as unknown as [
|
||||
const calls = axiosPostMock.mock.calls as unknown as [
|
||||
string,
|
||||
Record<string, unknown>,
|
||||
unknown,
|
||||
][]
|
||||
expect(calls[0]?.[1]).toEqual({ enabled: true })
|
||||
const [msg] = (onDone.mock.calls as unknown as [string, unknown][])[0] ?? []
|
||||
@@ -277,12 +338,13 @@ describe('callSchedule: enable / disable', () => {
|
||||
enabled: false,
|
||||
prompt: 'test',
|
||||
}
|
||||
updateTriggerMock.mockResolvedValueOnce(trigger)
|
||||
axiosPostMock.mockResolvedValueOnce({ data: trigger, status: 200 })
|
||||
const onDone = makeOnDone()
|
||||
await callSchedule(onDone, {} as never, 'disable trg_dis')
|
||||
const calls = updateTriggerMock.mock.calls as unknown as [
|
||||
const calls = axiosPostMock.mock.calls as unknown as [
|
||||
string,
|
||||
Record<string, unknown>,
|
||||
unknown,
|
||||
][]
|
||||
expect(calls[0]?.[1]).toEqual({ enabled: false })
|
||||
const [msg] = (onDone.mock.calls as unknown as [string, unknown][])[0] ?? []
|
||||
@@ -290,7 +352,7 @@ describe('callSchedule: enable / disable', () => {
|
||||
})
|
||||
|
||||
test('enable API error → error message', async () => {
|
||||
updateTriggerMock.mockRejectedValueOnce(new Error('Not found'))
|
||||
axiosPostMock.mockRejectedValueOnce(new Error('Not found'))
|
||||
const onDone = makeOnDone()
|
||||
await callSchedule(onDone, {} as never, 'enable trg_missing')
|
||||
const [msg] = (onDone.mock.calls as unknown as [string, unknown][])[0] ?? []
|
||||
@@ -298,7 +360,7 @@ describe('callSchedule: enable / disable', () => {
|
||||
})
|
||||
|
||||
test('disable API error → error message', async () => {
|
||||
updateTriggerMock.mockRejectedValueOnce(new Error('Not found'))
|
||||
axiosPostMock.mockRejectedValueOnce(new Error('Not found'))
|
||||
const onDone = makeOnDone()
|
||||
await callSchedule(onDone, {} as never, 'disable trg_missing')
|
||||
const [msg] = (onDone.mock.calls as unknown as [string, unknown][])[0] ?? []
|
||||
|
||||
Reference in New Issue
Block a user