mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-22 16:25:51 +00:00
refactor: 统一 log.ts/debug.ts 的测试 mock 为共享定义
- 新增 tests/mocks/log.ts 和 tests/mocks/debug.ts,覆盖源文件全部实际导出 - 移除旧 mock 中不存在的导出(logToFile、logEvent、getLogFilePath) - 13 个测试文件改为使用共享 mock,避免定义分散和不一致 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
12
CLAUDE.md
12
CLAUDE.md
@@ -263,6 +263,18 @@ Feature flags control which functionality is enabled at runtime. 代码中统一
|
|||||||
|
|
||||||
被迫 mock 的根源:`log.ts` / `debug.ts` → `bootstrap/state.ts`(模块级 `realpathSync` / `randomUUID` 副作用)。必须 mock 的模块:`log.ts`、`debug.ts`、`bun:bundle`、`settings/settings.js`、`config.ts`、`auth.ts`、第三方网络库。
|
被迫 mock 的根源:`log.ts` / `debug.ts` → `bootstrap/state.ts`(模块级 `realpathSync` / `randomUUID` 副作用)。必须 mock 的模块:`log.ts`、`debug.ts`、`bun:bundle`、`settings/settings.js`、`config.ts`、`auth.ts`、第三方网络库。
|
||||||
|
|
||||||
|
**`log.ts` 和 `debug.ts` 使用共享 mock**(`tests/mocks/log.ts` / `tests/mocks/debug.ts`),不要在测试文件中内联 mock 定义。使用方式:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { logMock } from "../../../tests/mocks/log";
|
||||||
|
mock.module("src/utils/log.ts", logMock);
|
||||||
|
|
||||||
|
import { debugMock } from "../../../../tests/mocks/debug";
|
||||||
|
mock.module("src/utils/debug.ts", debugMock);
|
||||||
|
```
|
||||||
|
|
||||||
|
源文件导出变更时只需更新 `tests/mocks/` 下的对应文件,不需要逐个修改测试。
|
||||||
|
|
||||||
不要 mock:纯函数模块(`errors.ts`、`stringUtils.js`)、mock 值与真实实现相同的模块、mock 路径与实际 import 不匹配的模块。
|
不要 mock:纯函数模块(`errors.ts`、`stringUtils.js`)、mock 值与真实实现相同的模块、mock 路径与实际 import 不匹配的模块。
|
||||||
|
|
||||||
路径规则:统一用 `.ts` 扩展名 + `src/*` 别名路径,禁止双重 mock 同一模块。
|
路径规则:统一用 `.ts` 扩展名 + `src/*` 别名路径,禁止双重 mock 同一模块。
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { mock, describe, expect, test } from "bun:test";
|
import { mock, describe, expect, test } from "bun:test";
|
||||||
|
import { debugMock } from "../../../../../../tests/mocks/debug";
|
||||||
|
|
||||||
// ─── Mocks for agentToolUtils.ts dependencies ───
|
// ─── Mocks for agentToolUtils.ts dependencies ───
|
||||||
// Only mock modules that are truly unavailable or cause side effects.
|
// Only mock modules that are truly unavailable or cause side effects.
|
||||||
@@ -87,20 +88,7 @@ mock.module("src/tasks/LocalAgentTask/LocalAgentTask.js", () => ({
|
|||||||
updateProgressFromMessage: noop,
|
updateProgressFromMessage: noop,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
mock.module("src/utils/debug.ts", () => ({
|
mock.module("src/utils/debug.ts", debugMock);
|
||||||
getMinDebugLogLevel: () => "warn",
|
|
||||||
isDebugMode: () => false,
|
|
||||||
enableDebugLogging: () => false,
|
|
||||||
getDebugFilter: () => null,
|
|
||||||
isDebugToStdErr: () => false,
|
|
||||||
getDebugFilePath: () => null,
|
|
||||||
setHasFormattedOutput: noop,
|
|
||||||
getHasFormattedOutput: () => false,
|
|
||||||
flushDebugLogs: async () => {},
|
|
||||||
logForDebugging: noop,
|
|
||||||
getDebugLogPath: () => "",
|
|
||||||
logAntError: noop,
|
|
||||||
}));
|
|
||||||
|
|
||||||
mock.module("src/utils/errors.js", () => ({
|
mock.module("src/utils/errors.js", () => ({
|
||||||
ClaudeError: class extends Error {},
|
ClaudeError: class extends Error {},
|
||||||
|
|||||||
@@ -1,21 +1,7 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
|
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
|
||||||
|
import { logMock } from '../../../../../../tests/mocks/log'
|
||||||
|
|
||||||
mock.module('src/utils/log.ts', () => ({
|
mock.module('src/utils/log.ts', logMock)
|
||||||
logError: () => {},
|
|
||||||
logToFile: () => {},
|
|
||||||
getLogDisplayTitle: () => '',
|
|
||||||
logEvent: () => {},
|
|
||||||
logMCPError: () => {},
|
|
||||||
logMCPDebug: () => {},
|
|
||||||
dateToFilename: (d: Date) => d.toISOString().replace(/[:.]/g, '-'),
|
|
||||||
getLogFilePath: () => '/tmp/mock-log',
|
|
||||||
attachErrorLogSink: () => {},
|
|
||||||
getInMemoryErrors: () => [],
|
|
||||||
loadErrorLogs: async () => [],
|
|
||||||
getErrorLogByIndex: async () => null,
|
|
||||||
captureAPIRequest: () => {},
|
|
||||||
_resetErrorLogForTesting: () => {},
|
|
||||||
}))
|
|
||||||
|
|
||||||
mock.module('src/services/tokenEstimation.ts', () => ({
|
mock.module('src/services/tokenEstimation.ts', () => ({
|
||||||
roughTokenCountEstimation: (text: string) => Math.ceil(text.length / 4),
|
roughTokenCountEstimation: (text: string) => Math.ceil(text.length / 4),
|
||||||
|
|||||||
@@ -1,22 +1,8 @@
|
|||||||
import { mock, describe, expect, test } from "bun:test";
|
import { mock, describe, expect, test } from "bun:test";
|
||||||
|
import { logMock } from "../../../../../../tests/mocks/log";
|
||||||
|
|
||||||
// Mock log.ts to cut the heavy dependency chain
|
// Mock log.ts to cut the heavy dependency chain
|
||||||
mock.module("src/utils/log.ts", () => ({
|
mock.module("src/utils/log.ts", logMock);
|
||||||
logError: () => {},
|
|
||||||
logToFile: () => {},
|
|
||||||
getLogDisplayTitle: () => "",
|
|
||||||
logEvent: () => {},
|
|
||||||
logMCPError: () => {},
|
|
||||||
logMCPDebug: () => {},
|
|
||||||
dateToFilename: (d: Date) => d.toISOString().replace(/[:.]/g, "-"),
|
|
||||||
getLogFilePath: () => "/tmp/mock-log",
|
|
||||||
attachErrorLogSink: () => {},
|
|
||||||
getInMemoryErrors: () => [],
|
|
||||||
loadErrorLogs: async () => [],
|
|
||||||
getErrorLogByIndex: async () => null,
|
|
||||||
captureAPIRequest: () => {},
|
|
||||||
_resetErrorLogForTesting: () => {},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
normalizeQuotes,
|
normalizeQuotes,
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { mock, describe, expect, test } from "bun:test";
|
import { mock, describe, expect, test } from "bun:test";
|
||||||
|
import { debugMock } from "../../../../../../tests/mocks/debug";
|
||||||
|
|
||||||
mock.module("src/utils/debug.ts", () => ({
|
mock.module("src/utils/debug.ts", debugMock);
|
||||||
logForDebugging: () => {},
|
|
||||||
isDebugMode: () => false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
formatGoToDefinitionResult,
|
formatGoToDefinitionResult,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { mock, describe, test, expect, beforeEach } from 'bun:test'
|
import { mock, describe, test, expect, beforeEach } from 'bun:test'
|
||||||
|
import { debugMock } from '../../../../tests/mocks/debug'
|
||||||
|
|
||||||
// Mock @langfuse/otel before any imports
|
// Mock @langfuse/otel before any imports
|
||||||
const mockForceFlush = mock(() => Promise.resolve())
|
const mockForceFlush = mock(() => Promise.resolve())
|
||||||
@@ -71,9 +72,7 @@ mock.module('@langfuse/tracing', () => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
// Mock debug logger
|
// Mock debug logger
|
||||||
mock.module('src/utils/debug.ts', () => ({
|
mock.module('src/utils/debug.ts', debugMock)
|
||||||
logForDebugging: mock(() => {}),
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Mock user data — resolveLangfuseUserId uses getCoreUserData().email and .deviceId
|
// Mock user data — resolveLangfuseUserId uses getCoreUserData().email and .deviceId
|
||||||
mock.module('src/utils/user.js', () => ({
|
mock.module('src/utils/user.js', () => ({
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { mock, describe, expect, test, afterEach } from "bun:test";
|
import { mock, describe, expect, test, afterEach } from "bun:test";
|
||||||
|
import { debugMock } from "../../../../tests/mocks/debug";
|
||||||
|
|
||||||
mock.module("axios", () => ({
|
mock.module("axios", () => ({
|
||||||
default: { get: async () => ({ data: { servers: [] } }) },
|
default: { get: async () => ({ data: { servers: [] } }) },
|
||||||
}));
|
}));
|
||||||
mock.module("src/utils/debug.ts", () => ({
|
mock.module("src/utils/debug.ts", debugMock);
|
||||||
logForDebugging: () => {},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { isOfficialMcpUrl, resetOfficialMcpUrlsForTesting } = await import(
|
const { isOfficialMcpUrl, resetOfficialMcpUrlsForTesting } = await import(
|
||||||
"../officialRegistry"
|
"../officialRegistry"
|
||||||
|
|||||||
@@ -1,20 +1,8 @@
|
|||||||
import { mock, describe, expect, test } from "bun:test";
|
import { mock, describe, expect, test } from "bun:test";
|
||||||
|
import { debugMock } from "../../../tests/mocks/debug";
|
||||||
|
|
||||||
// Mock debug.ts to cut bootstrap/state dependency chain
|
// Mock debug.ts to cut bootstrap/state dependency chain
|
||||||
mock.module("src/utils/debug.ts", () => ({
|
mock.module("src/utils/debug.ts", debugMock);
|
||||||
logForDebugging: () => {},
|
|
||||||
isDebugMode: () => false,
|
|
||||||
isDebugToStdErr: () => false,
|
|
||||||
getDebugFilePath: () => null,
|
|
||||||
getDebugFilter: () => null,
|
|
||||||
getMinDebugLogLevel: () => "debug",
|
|
||||||
getDebugLogPath: () => "/tmp/mock-debug.log",
|
|
||||||
flushDebugLogs: async () => {},
|
|
||||||
enableDebugLogging: () => false,
|
|
||||||
setHasFormattedOutput: () => {},
|
|
||||||
getHasFormattedOutput: () => false,
|
|
||||||
logAntError: () => {},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { validateBoundedIntEnvVar } = await import("../envValidation");
|
const { validateBoundedIntEnvVar } = await import("../envValidation");
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
import { mock, describe, expect, test } from "bun:test";
|
import { mock, describe, expect, test } from "bun:test";
|
||||||
|
import { logMock } from "../../../tests/mocks/log";
|
||||||
|
|
||||||
// Mock log.ts to cut the heavy dependency chain (log.ts → bootstrap/state.ts → analytics)
|
// Mock log.ts to cut the heavy dependency chain (log.ts → bootstrap/state.ts → analytics)
|
||||||
mock.module("src/utils/log.ts", () => ({
|
mock.module("src/utils/log.ts", logMock);
|
||||||
logError: () => {},
|
|
||||||
logToFile: () => {},
|
|
||||||
getLogDisplayTitle: () => "",
|
|
||||||
logEvent: () => {},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { safeParseJSON, safeParseJSONC, parseJSONL, addItemToJSONCArray } =
|
const { safeParseJSON, safeParseJSONC, parseJSONL, addItemToJSONCArray } =
|
||||||
await import("../json");
|
await import("../json");
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
import { mock, describe, expect, test, beforeEach } from "bun:test";
|
import { mock, describe, expect, test, beforeEach } from "bun:test";
|
||||||
|
import { logMock } from "../../../tests/mocks/log";
|
||||||
|
|
||||||
// Mock log.ts to cut the bootstrap/state dependency chain
|
// Mock log.ts to cut the bootstrap/state dependency chain
|
||||||
mock.module("src/utils/log.ts", () => ({
|
mock.module("src/utils/log.ts", logMock);
|
||||||
logError: () => {},
|
|
||||||
logToFile: () => {},
|
|
||||||
getLogDisplayTitle: () => "",
|
|
||||||
logEvent: () => {},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { memoizeWithTTL, memoizeWithTTLAsync, memoizeWithLRU } = await import(
|
const { memoizeWithTTL, memoizeWithTTLAsync, memoizeWithLRU } = await import(
|
||||||
"../memoize"
|
"../memoize"
|
||||||
|
|||||||
@@ -1,22 +1,8 @@
|
|||||||
import { mock, describe, expect, test } from "bun:test";
|
import { mock, describe, expect, test } from "bun:test";
|
||||||
|
import { logMock } from "../../../tests/mocks/log";
|
||||||
|
|
||||||
// Mock heavy dependency chain: tokenEstimation.ts → log.ts → bootstrap/state.ts
|
// Mock heavy dependency chain: tokenEstimation.ts → log.ts → bootstrap/state.ts
|
||||||
mock.module("src/utils/log.ts", () => ({
|
mock.module("src/utils/log.ts", logMock);
|
||||||
logError: () => {},
|
|
||||||
logToFile: () => {},
|
|
||||||
getLogDisplayTitle: () => "",
|
|
||||||
logEvent: () => {},
|
|
||||||
logMCPError: () => {},
|
|
||||||
logMCPDebug: () => {},
|
|
||||||
dateToFilename: (d: Date) => d.toISOString().replace(/[:.]/g, "-"),
|
|
||||||
getLogFilePath: () => "/tmp/mock-log",
|
|
||||||
attachErrorLogSink: () => {},
|
|
||||||
getInMemoryErrors: () => [],
|
|
||||||
loadErrorLogs: async () => [],
|
|
||||||
getErrorLogByIndex: async () => null,
|
|
||||||
captureAPIRequest: () => {},
|
|
||||||
_resetErrorLogForTesting: () => {},
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock tokenEstimation to avoid pulling in API provider deps
|
// Mock tokenEstimation to avoid pulling in API provider deps
|
||||||
mock.module("src/services/tokenEstimation.ts", () => ({
|
mock.module("src/services/tokenEstimation.ts", () => ({
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import { mock, describe, expect, test, beforeEach, afterEach } from "bun:test";
|
import { mock, describe, expect, test, beforeEach, afterEach } from "bun:test";
|
||||||
|
import { logMock } from "../../../../tests/mocks/log";
|
||||||
|
|
||||||
mock.module("src/utils/log.ts", () => ({
|
mock.module("src/utils/log.ts", logMock);
|
||||||
logError: () => {},
|
|
||||||
logToFile: () => {},
|
|
||||||
getLogDisplayTitle: () => "",
|
|
||||||
logEvent: () => {},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isExternalPermissionMode,
|
isExternalPermissionMode,
|
||||||
|
|||||||
@@ -1,24 +1,10 @@
|
|||||||
import { mock, describe, expect, test } from 'bun:test'
|
import { mock, describe, expect, test } from 'bun:test'
|
||||||
|
import { logMock } from '../../../../tests/mocks/log'
|
||||||
import { createFileStateCacheWithSizeLimit } from '../../../utils/fileStateCache.js'
|
import { createFileStateCacheWithSizeLimit } from '../../../utils/fileStateCache.js'
|
||||||
import { createSubagentContext } from '../../../utils/forkedAgent.js'
|
import { createSubagentContext } from '../../../utils/forkedAgent.js'
|
||||||
import { getEmptyToolPermissionContext } from '../../../Tool.js'
|
import { getEmptyToolPermissionContext } from '../../../Tool.js'
|
||||||
|
|
||||||
mock.module('src/utils/log.ts', () => ({
|
mock.module('src/utils/log.ts', logMock)
|
||||||
logError: () => {},
|
|
||||||
logToFile: () => {},
|
|
||||||
getLogDisplayTitle: () => '',
|
|
||||||
logEvent: () => {},
|
|
||||||
logMCPError: () => {},
|
|
||||||
logMCPDebug: () => {},
|
|
||||||
dateToFilename: (d: Date) => d.toISOString().replace(/[:.]/g, '-'),
|
|
||||||
getLogFilePath: () => '/tmp/mock-log',
|
|
||||||
attachErrorLogSink: () => {},
|
|
||||||
getInMemoryErrors: () => [],
|
|
||||||
loadErrorLogs: async () => [],
|
|
||||||
getErrorLogByIndex: async () => null,
|
|
||||||
captureAPIRequest: () => {},
|
|
||||||
_resetErrorLogForTesting: () => {},
|
|
||||||
}))
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
getDenyRuleForTool,
|
getDenyRuleForTool,
|
||||||
|
|||||||
@@ -1,20 +1,8 @@
|
|||||||
import { mock, describe, expect, test, afterEach } from "bun:test";
|
import { mock, describe, expect, test, afterEach } from "bun:test";
|
||||||
|
import { debugMock } from "../../../../tests/mocks/debug";
|
||||||
|
|
||||||
// Mock debug.ts to cut the bootstrap/state dependency chain
|
// Mock debug.ts to cut the bootstrap/state dependency chain
|
||||||
mock.module("src/utils/debug.ts", () => ({
|
mock.module("src/utils/debug.ts", debugMock);
|
||||||
logForDebugging: () => {},
|
|
||||||
isDebugMode: () => false,
|
|
||||||
isDebugToStdErr: () => false,
|
|
||||||
getDebugFilePath: () => null,
|
|
||||||
getDebugFilter: () => null,
|
|
||||||
getMinDebugLogLevel: () => "debug",
|
|
||||||
getDebugLogPath: () => "/tmp/mock-debug.log",
|
|
||||||
flushDebugLogs: async () => {},
|
|
||||||
enableDebugLogging: () => false,
|
|
||||||
setHasFormattedOutput: () => {},
|
|
||||||
getHasFormattedOutput: () => false,
|
|
||||||
logAntError: () => {},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
getMaxOutputLength,
|
getMaxOutputLength,
|
||||||
|
|||||||
25
tests/mocks/debug.ts
Normal file
25
tests/mocks/debug.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* Shared mock for src/utils/debug.ts
|
||||||
|
*
|
||||||
|
* Cuts the bootstrap/state.ts dependency chain (module-level realpathSync + randomUUID).
|
||||||
|
* Must be called via mock.module("src/utils/debug.ts", debugMock) BEFORE any import that
|
||||||
|
* transitively depends on debug.ts.
|
||||||
|
*
|
||||||
|
* Exported as a factory so each call produces a fresh object (mock.module requirement).
|
||||||
|
*/
|
||||||
|
export function debugMock() {
|
||||||
|
return {
|
||||||
|
getMinDebugLogLevel: () => "debug" as const,
|
||||||
|
isDebugMode: () => false,
|
||||||
|
enableDebugLogging: () => false,
|
||||||
|
getDebugFilter: () => null,
|
||||||
|
isDebugToStdErr: () => false,
|
||||||
|
getDebugFilePath: () => null as string | null,
|
||||||
|
setHasFormattedOutput: () => {},
|
||||||
|
getHasFormattedOutput: () => false,
|
||||||
|
flushDebugLogs: async () => {},
|
||||||
|
logForDebugging: () => {},
|
||||||
|
getDebugLogPath: () => "/tmp/mock-debug.log",
|
||||||
|
logAntError: () => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
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: () => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user