Files
claude-code/src/utils/__tests__/tokens.test.ts
claude-code-best 1173a62301 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>
2026-04-23 11:39:53 +08:00

283 lines
8.9 KiB
TypeScript

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.module("src/utils/log.ts", logMock);
// Mock tokenEstimation to avoid pulling in API provider deps
mock.module("src/services/tokenEstimation.ts", () => ({
roughTokenCountEstimation: (text: string) => Math.ceil(text.length / 4),
roughTokenCountEstimationForMessages: (msgs: any[]) => msgs.length * 100,
roughTokenCountEstimationForMessage: () => 100,
roughTokenCountEstimationForFileType: () => 100,
bytesPerTokenForFileType: () => 4,
countTokensWithAPI: async () => 0,
countMessagesTokensWithAPI: async () => 0,
countTokensViaHaikuFallback: async () => 0,
}));
// Mock slowOperations to avoid bun:bundle import
mock.module('src/utils/slowOperations.ts', () => ({
jsonStringify: JSON.stringify,
jsonParse: JSON.parse,
slowLogging: { enabled: false },
clone: (v: any) => structuredClone(v),
cloneDeep: (v: any) => structuredClone(v),
callerFrame: () => '',
SLOW_OPERATION_THRESHOLD_MS: 100,
writeFileSync_DEPRECATED: () => {},
}))
const {
getTokenCountFromUsage,
getTokenUsage,
tokenCountFromLastAPIResponse,
messageTokenCountFromLastAPIResponse,
getCurrentUsage,
doesMostRecentAssistantMessageExceed200k,
getAssistantMessageContentLength,
} = await import("../tokens");
// ─── Helpers ────────────────────────────────────────────────────────────
function makeAssistantMessage(
content: any[],
usage?: any,
model?: string,
id?: string
) {
return {
type: "assistant" as const,
uuid: `test-${Math.random()}`,
message: {
id: id ?? `msg_${Math.random()}`,
role: "assistant" as const,
content,
model: model ?? "claude-sonnet-4-20250514",
usage: usage ?? {
input_tokens: 100,
output_tokens: 50,
cache_creation_input_tokens: 10,
cache_read_input_tokens: 5,
},
},
isApiErrorMessage: false,
};
}
function makeUserMessage(text: string) {
return {
type: "user" as const,
uuid: `test-${Math.random()}`,
message: { role: "user" as const, content: text },
};
}
// ─── getTokenCountFromUsage ─────────────────────────────────────────────
describe("getTokenCountFromUsage", () => {
test("sums all token fields", () => {
const usage = {
input_tokens: 100,
output_tokens: 50,
cache_creation_input_tokens: 20,
cache_read_input_tokens: 10,
};
expect(getTokenCountFromUsage(usage as any)).toBe(180);
});
test("handles missing cache fields", () => {
const usage = {
input_tokens: 100,
output_tokens: 50,
};
expect(getTokenCountFromUsage(usage as any)).toBe(150);
});
test("handles zero values", () => {
const usage = {
input_tokens: 0,
output_tokens: 0,
cache_creation_input_tokens: 0,
cache_read_input_tokens: 0,
};
expect(getTokenCountFromUsage(usage as any)).toBe(0);
});
});
// ─── getTokenUsage ──────────────────────────────────────────────────────
describe("getTokenUsage", () => {
test("returns usage for valid assistant message", () => {
const msg = makeAssistantMessage([{ type: "text", text: "hello" }]);
const usage = getTokenUsage(msg as any);
expect(usage).toBeDefined();
expect(usage!.input_tokens).toBe(100);
});
test("returns undefined for user message", () => {
const msg = makeUserMessage("hello");
expect(getTokenUsage(msg as any)).toBeUndefined();
});
test("returns undefined for synthetic model", () => {
const msg = makeAssistantMessage(
[{ type: "text", text: "hello" }],
{ input_tokens: 10, output_tokens: 5 },
"<synthetic>"
);
expect(getTokenUsage(msg as any)).toBeUndefined();
});
});
// ─── tokenCountFromLastAPIResponse ──────────────────────────────────────
describe("tokenCountFromLastAPIResponse", () => {
test("returns token count from last assistant message", () => {
const msgs = [
makeAssistantMessage([{ type: "text", text: "hi" }], {
input_tokens: 200,
output_tokens: 100,
cache_creation_input_tokens: 50,
cache_read_input_tokens: 25,
}),
];
expect(tokenCountFromLastAPIResponse(msgs as any)).toBe(375);
});
test("returns 0 for empty messages", () => {
expect(tokenCountFromLastAPIResponse([])).toBe(0);
});
test("skips user messages to find last assistant", () => {
const msgs = [
makeAssistantMessage([{ type: "text", text: "hi" }], {
input_tokens: 100,
output_tokens: 50,
}),
makeUserMessage("reply"),
];
expect(tokenCountFromLastAPIResponse(msgs as any)).toBe(150);
});
});
// ─── messageTokenCountFromLastAPIResponse ───────────────────────────────
describe("messageTokenCountFromLastAPIResponse", () => {
test("returns output_tokens from last assistant", () => {
const msgs = [
makeAssistantMessage([{ type: "text", text: "hi" }], {
input_tokens: 200,
output_tokens: 75,
}),
];
expect(messageTokenCountFromLastAPIResponse(msgs as any)).toBe(75);
});
test("returns 0 for empty messages", () => {
expect(messageTokenCountFromLastAPIResponse([])).toBe(0);
});
});
// ─── getCurrentUsage ────────────────────────────────────────────────────
describe("getCurrentUsage", () => {
test("returns usage object from last assistant", () => {
const msgs = [
makeAssistantMessage([{ type: "text", text: "hi" }], {
input_tokens: 100,
output_tokens: 50,
cache_creation_input_tokens: 10,
cache_read_input_tokens: 5,
}),
];
const usage = getCurrentUsage(msgs as any);
expect(usage).toEqual({
input_tokens: 100,
output_tokens: 50,
cache_creation_input_tokens: 10,
cache_read_input_tokens: 5,
});
});
test("returns null for empty messages", () => {
expect(getCurrentUsage([])).toBeNull();
});
test("defaults cache fields to 0", () => {
const msgs = [
makeAssistantMessage([{ type: "text", text: "hi" }], {
input_tokens: 100,
output_tokens: 50,
}),
];
const usage = getCurrentUsage(msgs as any);
expect(usage!.cache_creation_input_tokens).toBe(0);
expect(usage!.cache_read_input_tokens).toBe(0);
});
});
// ─── doesMostRecentAssistantMessageExceed200k ───────────────────────────
describe("doesMostRecentAssistantMessageExceed200k", () => {
test("returns false when under 200k", () => {
const msgs = [
makeAssistantMessage([{ type: "text", text: "hi" }], {
input_tokens: 1000,
output_tokens: 500,
}),
];
expect(doesMostRecentAssistantMessageExceed200k(msgs as any)).toBe(false);
});
test("returns true when over 200k", () => {
const msgs = [
makeAssistantMessage([{ type: "text", text: "hi" }], {
input_tokens: 190000,
output_tokens: 15000,
}),
];
expect(doesMostRecentAssistantMessageExceed200k(msgs as any)).toBe(true);
});
test("returns false for empty messages", () => {
expect(doesMostRecentAssistantMessageExceed200k([])).toBe(false);
});
});
// ─── getAssistantMessageContentLength ───────────────────────────────────
describe("getAssistantMessageContentLength", () => {
test("counts text content length", () => {
const msg = makeAssistantMessage([{ type: "text", text: "hello" }]);
expect(getAssistantMessageContentLength(msg as any)).toBe(5);
});
test("counts multiple blocks", () => {
const msg = makeAssistantMessage([
{ type: "text", text: "hello" },
{ type: "text", text: "world" },
]);
expect(getAssistantMessageContentLength(msg as any)).toBe(10);
});
test("counts thinking content", () => {
const msg = makeAssistantMessage([
{ type: "thinking", thinking: "let me think" },
]);
expect(getAssistantMessageContentLength(msg as any)).toBe(12);
});
test("returns 0 for empty content", () => {
const msg = makeAssistantMessage([]);
expect(getAssistantMessageContentLength(msg as any)).toBe(0);
});
test("counts tool_use input", () => {
const msg = makeAssistantMessage([
{ type: "tool_use", id: "t1", name: "Bash", input: { command: "ls" } },
]);
expect(getAssistantMessageContentLength(msg as any)).toBeGreaterThan(0);
});
});