fix: sync missing test mocks and agentToolFilter from upstream
Copied 6 missing mock files (auth, axios, childProcess, log, state, toolContext) and agentToolFilter.ts from claude-code-best/claude-code. Result: +92 tests pass, 9 'module not found' errors resolved. LocalMemoryRecallTool has 40 new failures (mock/impl mismatch, TBD).
This commit is contained in:
parent
6ddad59d2f
commit
5b0c0fa46f
23
src/utils/agentToolFilter.ts
Normal file
23
src/utils/agentToolFilter.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
/**
|
||||||
|
* filterParentToolsForFork — gate layer 2 for subagent tool inheritance.
|
||||||
|
*
|
||||||
|
* The fork path of AgentTool (and its sibling resumeAgent) sets
|
||||||
|
* `useExactTools: true` and passes `toolUseContext.options.tools` to
|
||||||
|
* `runAgent` as `availableTools`. With `useExactTools=true`, runAgent
|
||||||
|
* skips `resolveAgentTools`, which means the gate layer 1
|
||||||
|
* (`ALL_AGENT_DISALLOWED_TOOLS`) — which only takes effect inside
|
||||||
|
* `filterToolsForAgent` — is bypassed entirely on fork paths.
|
||||||
|
*
|
||||||
|
* This filter applies the same disallow-list to the parent tool array
|
||||||
|
* before it reaches the fork. Both new-fork (AgentTool.tsx) and
|
||||||
|
* resumed-fork (resumeAgent.ts) paths must call this.
|
||||||
|
*
|
||||||
|
* See docs/jira/LOCAL-WIRING-DESIGN.md §4.5 / §5.5 for design rationale.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ALL_AGENT_DISALLOWED_TOOLS } from '../constants/tools.js'
|
||||||
|
import type { Tool } from '../Tool.js'
|
||||||
|
|
||||||
|
export function filterParentToolsForFork(parentTools: readonly Tool[]): Tool[] {
|
||||||
|
return parentTools.filter(t => !ALL_AGENT_DISALLOWED_TOOLS.has(t.name))
|
||||||
|
}
|
||||||
31
tests/mocks/auth.ts
Normal file
31
tests/mocks/auth.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
/**
|
||||||
|
* Shared mock for `src/utils/auth.js`. Use it via:
|
||||||
|
*
|
||||||
|
* import { authMock } from '../../tests/mocks/auth'
|
||||||
|
* mock.module('src/utils/auth.js', authMock)
|
||||||
|
*
|
||||||
|
* Tests that need different return values can override the helper used by
|
||||||
|
* the suite (e.g. by extending this object and re-registering with mock.module).
|
||||||
|
* Always extend here rather than inlining a different shape per test, so the
|
||||||
|
* surface stays consistent when `auth.ts` exports change.
|
||||||
|
*/
|
||||||
|
export const authMock = () => ({
|
||||||
|
// Mirrors the production contract: src/utils/auth.ts returns
|
||||||
|
// Promise<boolean> ("did the access token change") and a token object that
|
||||||
|
// carries scopes, subscriptionType, expiresAt, etc. Tests that branch on
|
||||||
|
// these values must see the full shape so they can not silently drift away
|
||||||
|
// from production.
|
||||||
|
checkAndRefreshOAuthTokenIfNeeded: async () => false,
|
||||||
|
getClaudeAIOAuthTokens: () => ({
|
||||||
|
accessToken: 'token',
|
||||||
|
refreshToken: null,
|
||||||
|
expiresAt: null,
|
||||||
|
scopes: ['user:inference'],
|
||||||
|
subscriptionType: null,
|
||||||
|
rateLimitTier: null,
|
||||||
|
}),
|
||||||
|
isClaudeAISubscriber: () => true,
|
||||||
|
isProSubscriber: () => false,
|
||||||
|
isMaxSubscriber: () => false,
|
||||||
|
isTeamSubscriber: () => false,
|
||||||
|
})
|
||||||
121
tests/mocks/axios.ts
Normal file
121
tests/mocks/axios.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
/**
|
||||||
|
* Per-file axios mock helper.
|
||||||
|
*
|
||||||
|
* Each call to `setupAxiosMock()` registers its own `mock.module('axios', ...)`
|
||||||
|
* that only knows about the handle returned to that call. No shared state between
|
||||||
|
* test files — eliminates cross-file mock pollution.
|
||||||
|
*
|
||||||
|
* The real axios module is cached at first import (before any mock.module
|
||||||
|
* registration) so the factory can spread it for shape compatibility.
|
||||||
|
*
|
||||||
|
* 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'
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
|
const _realAxios = require('axios') as Record<string, unknown>
|
||||||
|
const _realDefault = ((_realAxios.default as
|
||||||
|
| Record<string, unknown>
|
||||||
|
| undefined) ?? _realAxios) as Record<string, unknown>
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
useStubs: boolean
|
||||||
|
stubs: AxiosMethodStubs
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a mock for `axios` scoped to this test file.
|
||||||
|
* Each call creates an independent mock.module registration — no shared
|
||||||
|
* handles array, no cross-file state.
|
||||||
|
*/
|
||||||
|
export function setupAxiosMock(): AxiosMockHandle {
|
||||||
|
const handle: AxiosMockHandle = { useStubs: false, stubs: {} }
|
||||||
|
|
||||||
|
mock.module('axios', () => {
|
||||||
|
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 {
|
||||||
|
..._realAxios,
|
||||||
|
...routedDefault,
|
||||||
|
default: routedDefault,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return handle
|
||||||
|
}
|
||||||
45
tests/mocks/childProcess.ts
Normal file
45
tests/mocks/childProcess.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
/**
|
||||||
|
* Shared mock for `node:child_process`.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import { mock } from 'bun:test'
|
||||||
|
* import { childProcessMock, execFileMock, execFileSyncMock } from 'tests/mocks/childProcess'
|
||||||
|
* mock.module('node:child_process', () => childProcessMock)
|
||||||
|
*
|
||||||
|
* Call `execFileMock.mockImplementation(...)` or `execFileSyncMock.mockImplementation(...)`
|
||||||
|
* before each test that needs specific behavior.
|
||||||
|
*/
|
||||||
|
import { mock } from 'bun:test'
|
||||||
|
|
||||||
|
// execFile: node-style callback (cmd, args, opts?, callback)
|
||||||
|
export const execFileMock = mock(
|
||||||
|
(
|
||||||
|
_cmd: string,
|
||||||
|
_args: string[],
|
||||||
|
_optsOrCb?: unknown,
|
||||||
|
_cb?: (err: Error | null, stdout: string, stderr: string) => void,
|
||||||
|
) => {
|
||||||
|
const cb =
|
||||||
|
typeof _optsOrCb === 'function'
|
||||||
|
? (_optsOrCb as (
|
||||||
|
err: Error | null,
|
||||||
|
stdout: string,
|
||||||
|
stderr: string,
|
||||||
|
) => void)
|
||||||
|
: _cb
|
||||||
|
if (cb) cb(null, '', '')
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// execFileSync: synchronous (returns Buffer)
|
||||||
|
export const execFileSyncMock = mock(
|
||||||
|
(_cmd: string, _args: string[], _opts?: unknown): Buffer => {
|
||||||
|
return Buffer.from('')
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export const childProcessMock = {
|
||||||
|
execFile: execFileMock,
|
||||||
|
execFileSync: execFileSyncMock,
|
||||||
|
}
|
||||||
24
tests/mocks/log.ts
Normal file
24
tests/mocks/log.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
/**
|
||||||
|
* Shared mock for src/utils/log.ts
|
||||||
|
*
|
||||||
|
* Cuts the bootstrap/state.ts dependency chain (module-level realpathSync + randomUUID).
|
||||||
|
* Must be called via mock.module("src/utils/log.ts", logMock) BEFORE any import that
|
||||||
|
* transitively depends on log.ts.
|
||||||
|
*
|
||||||
|
* Exported as a factory so each call produces a fresh object (mock.module requirement).
|
||||||
|
*/
|
||||||
|
export function logMock() {
|
||||||
|
return {
|
||||||
|
logError: () => {},
|
||||||
|
getLogDisplayTitle: () => '',
|
||||||
|
dateToFilename: (d: Date) => d.toISOString().replace(/[:.]/g, '-'),
|
||||||
|
attachErrorLogSink: () => {},
|
||||||
|
getInMemoryErrors: () => [] as Array<{ error: string; timestamp: string }>,
|
||||||
|
loadErrorLogs: async () => [],
|
||||||
|
getErrorLogByIndex: async () => null,
|
||||||
|
logMCPError: () => {},
|
||||||
|
logMCPDebug: () => {},
|
||||||
|
captureAPIRequest: () => {},
|
||||||
|
_resetErrorLogForTesting: () => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
91
tests/mocks/state.ts
Normal file
91
tests/mocks/state.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
/**
|
||||||
|
* Shared partial mock for src/bootstrap/state.ts
|
||||||
|
*
|
||||||
|
* Covers the most commonly imported exports plus their transitive callers.
|
||||||
|
* Add exports here when new tests need them — never mock exports that don't exist.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import { stateMock } from '../../../tests/mocks/state'
|
||||||
|
* mock.module('src/bootstrap/state.js', stateMock)
|
||||||
|
*/
|
||||||
|
export function stateMock() {
|
||||||
|
const noop = () => {}
|
||||||
|
return {
|
||||||
|
// Session identity
|
||||||
|
getSessionId: () => 'mock-session-id',
|
||||||
|
regenerateSessionId: noop,
|
||||||
|
getParentSessionId: () => undefined,
|
||||||
|
switchSession: noop,
|
||||||
|
onSessionSwitch: () => () => {},
|
||||||
|
|
||||||
|
// CWD / project
|
||||||
|
getOriginalCwd: () => '/mock/cwd',
|
||||||
|
getSessionProjectDir: () => null,
|
||||||
|
getProjectRoot: () => '/mock/project',
|
||||||
|
getCwdState: () => '/mock/cwd',
|
||||||
|
setCwdState: noop,
|
||||||
|
setOriginalCwd: noop,
|
||||||
|
setProjectRoot: noop,
|
||||||
|
|
||||||
|
// Direct-connect
|
||||||
|
getDirectConnectServerUrl: () => undefined,
|
||||||
|
setDirectConnectServerUrl: noop,
|
||||||
|
|
||||||
|
// Duration / cost accumulators
|
||||||
|
addToTotalDurationState: noop,
|
||||||
|
resetTotalDurationStateAndCost_FOR_TESTS_ONLY: noop,
|
||||||
|
addToTotalCostState: noop,
|
||||||
|
getTotalCostUSD: () => 0,
|
||||||
|
getTotalAPIDuration: () => 0,
|
||||||
|
getTotalDuration: () => 0,
|
||||||
|
getTotalAPIDurationWithoutRetries: () => 0,
|
||||||
|
getTotalToolDuration: () => 0,
|
||||||
|
addToToolDuration: noop,
|
||||||
|
|
||||||
|
// Turn stats
|
||||||
|
getTurnHookDurationMs: () => 0,
|
||||||
|
addToTurnHookDuration: noop,
|
||||||
|
resetTurnHookDuration: noop,
|
||||||
|
getTurnHookCount: () => 0,
|
||||||
|
getTurnToolDurationMs: () => 0,
|
||||||
|
resetTurnToolDuration: noop,
|
||||||
|
getTurnToolCount: () => 0,
|
||||||
|
getTurnClassifierDurationMs: () => 0,
|
||||||
|
addToTurnClassifierDuration: noop,
|
||||||
|
resetTurnClassifierDuration: noop,
|
||||||
|
getTurnClassifierCount: () => 0,
|
||||||
|
|
||||||
|
// Stats store
|
||||||
|
getStatsStore: () => ({}),
|
||||||
|
setStatsStore: noop,
|
||||||
|
|
||||||
|
// Interaction time
|
||||||
|
updateLastInteractionTime: noop,
|
||||||
|
flushInteractionTime: noop,
|
||||||
|
|
||||||
|
// Lines changed
|
||||||
|
addToTotalLinesChanged: noop,
|
||||||
|
getTotalLinesAdded: () => 0,
|
||||||
|
getTotalLinesRemoved: () => 0,
|
||||||
|
|
||||||
|
// Token counts
|
||||||
|
getTotalInputTokens: () => 0,
|
||||||
|
getTotalOutputTokens: () => 0,
|
||||||
|
getTotalCacheReadInputTokens: () => 0,
|
||||||
|
getTotalCacheCreationInputTokens: () => 0,
|
||||||
|
getTotalWebSearchRequests: () => 0,
|
||||||
|
getTurnOutputTokens: () => 0,
|
||||||
|
getCurrentTurnTokenBudget: () => null,
|
||||||
|
|
||||||
|
// API request state
|
||||||
|
setLastAPIRequest: noop,
|
||||||
|
getLastAPIRequest: () => null,
|
||||||
|
setLastAPIRequestMessages: noop,
|
||||||
|
getLastAPIRequestMessages: () => [],
|
||||||
|
|
||||||
|
// Various getters (add as needed)
|
||||||
|
getIsNonInteractiveSession: () => false,
|
||||||
|
getSdkAgentProgressSummariesEnabled: () => false,
|
||||||
|
addSlowOperation: noop,
|
||||||
|
}
|
||||||
|
}
|
||||||
52
tests/mocks/toolContext.ts
Normal file
52
tests/mocks/toolContext.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
/**
|
||||||
|
* Shared minimal ToolUseContext stub for tool unit tests.
|
||||||
|
*
|
||||||
|
* Provides only the fields tools actually access in tests:
|
||||||
|
* - getAppState() returns a context with empty rule arrays for every source
|
||||||
|
* - toolUseId / parentMessageId / assistantMessageId / turnId can be
|
||||||
|
* overridden per test for budget tracking tests
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import { mockToolContext } from 'tests/mocks/toolContext'
|
||||||
|
* const ctx = mockToolContext({ toolUseId: 't1' })
|
||||||
|
*
|
||||||
|
* Per memory feedback "Mock dependency not subject" — this exists so each
|
||||||
|
* tool test file does not redefine the same partial stub.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const emptyRules = {
|
||||||
|
user: [],
|
||||||
|
project: [],
|
||||||
|
local: [],
|
||||||
|
session: [],
|
||||||
|
cliArg: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MockToolContextOptions {
|
||||||
|
toolUseId?: string
|
||||||
|
parentMessageId?: string
|
||||||
|
assistantMessageId?: string
|
||||||
|
turnId?: string
|
||||||
|
/** Override toolPermissionContext fields (e.g. mode, alwaysAllowRules). */
|
||||||
|
permissionOverrides?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mockToolContext(opts: MockToolContextOptions = {}): never {
|
||||||
|
return {
|
||||||
|
toolUseId: opts.toolUseId,
|
||||||
|
parentMessageId: opts.parentMessageId,
|
||||||
|
assistantMessageId: opts.assistantMessageId,
|
||||||
|
turnId: opts.turnId,
|
||||||
|
getAppState: () => ({
|
||||||
|
toolPermissionContext: {
|
||||||
|
mode: 'default',
|
||||||
|
additionalWorkingDirectories: new Set(),
|
||||||
|
alwaysAllowRules: { ...emptyRules },
|
||||||
|
alwaysDenyRules: { ...emptyRules },
|
||||||
|
alwaysAskRules: { ...emptyRules },
|
||||||
|
isBypassPermissionsModeAvailable: false,
|
||||||
|
...(opts.permissionOverrides ?? {}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
} as never
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user