test: 添加 json/truncate/path/tokens 模块测试
- json.test.ts: 27 tests (safeParseJSON, safeParseJSONC, parseJSONL, addItemToJSONCArray) - truncate.test.ts: 24 tests (truncateToWidth, truncateStartToWidth, truncatePathMiddle, truncate, wrapText) - path.test.ts: 15 tests (containsPathTraversal, normalizePathForConfigKey) - tokens.test.ts: 22 tests (getTokenCountFromUsage, getTokenUsage, tokenCountFromLastAPIResponse, etc.) 使用 mock.module() 切断 log.ts/tokenEstimation.ts/slowOperations.ts 重依赖链 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
fd2ad71a4e
commit
43af260322
153
src/utils/__tests__/json.test.ts
Normal file
153
src/utils/__tests__/json.test.ts
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
import { mock, describe, expect, test } from "bun:test";
|
||||||
|
|
||||||
|
// Mock log.ts to cut the heavy dependency chain (log.ts → bootstrap/state.ts → analytics)
|
||||||
|
mock.module("src/utils/log.ts", () => ({
|
||||||
|
logError: () => {},
|
||||||
|
logToFile: () => {},
|
||||||
|
getLogDisplayTitle: () => "",
|
||||||
|
logEvent: () => {},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { safeParseJSON, safeParseJSONC, parseJSONL, addItemToJSONCArray } =
|
||||||
|
await import("../json");
|
||||||
|
|
||||||
|
// ─── safeParseJSON ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("safeParseJSON", () => {
|
||||||
|
test("parses valid object", () => {
|
||||||
|
expect(safeParseJSON('{"a":1}')).toEqual({ a: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses valid array", () => {
|
||||||
|
expect(safeParseJSON("[1,2,3]")).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses string value", () => {
|
||||||
|
expect(safeParseJSON('"hello"')).toBe("hello");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses number value", () => {
|
||||||
|
expect(safeParseJSON("42")).toBe(42);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses boolean value", () => {
|
||||||
|
expect(safeParseJSON("true")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses null value", () => {
|
||||||
|
expect(safeParseJSON("null")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns null for invalid JSON", () => {
|
||||||
|
expect(safeParseJSON("{bad}")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns null for empty string", () => {
|
||||||
|
expect(safeParseJSON("")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns null for undefined input", () => {
|
||||||
|
expect(safeParseJSON(undefined as any)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns null for null input", () => {
|
||||||
|
expect(safeParseJSON(null as any)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles JSON with BOM", () => {
|
||||||
|
expect(safeParseJSON('\uFEFF{"a":1}')).toEqual({ a: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses nested objects", () => {
|
||||||
|
const input = '{"a":{"b":{"c":1}}}';
|
||||||
|
expect(safeParseJSON(input)).toEqual({ a: { b: { c: 1 } } });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── safeParseJSONC ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("safeParseJSONC", () => {
|
||||||
|
test("parses standard JSON", () => {
|
||||||
|
expect(safeParseJSONC('{"a":1}')).toEqual({ a: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses JSON with single-line comments", () => {
|
||||||
|
expect(safeParseJSONC('{\n// comment\n"a":1\n}')).toEqual({ a: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses JSON with block comments", () => {
|
||||||
|
expect(safeParseJSONC('{\n/* comment */\n"a":1\n}')).toEqual({ a: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses JSON with trailing commas", () => {
|
||||||
|
expect(safeParseJSONC('{"a":1,}')).toEqual({ a: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns null for null input", () => {
|
||||||
|
expect(safeParseJSONC(null as any)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns null for empty string", () => {
|
||||||
|
expect(safeParseJSONC("")).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── parseJSONL ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("parseJSONL", () => {
|
||||||
|
test("parses multiple lines", () => {
|
||||||
|
const result = parseJSONL('{"a":1}\n{"b":2}');
|
||||||
|
expect(result).toEqual([{ a: 1 }, { b: 2 }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns empty array for empty string", () => {
|
||||||
|
expect(parseJSONL("")).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses single line", () => {
|
||||||
|
expect(parseJSONL('{"a":1}')).toEqual([{ a: 1 }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("accepts Buffer input", () => {
|
||||||
|
const buf = Buffer.from('{"x":1}\n{"y":2}');
|
||||||
|
const result = parseJSONL(buf as any);
|
||||||
|
expect(result).toEqual([{ x: 1 }, { y: 2 }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// NOTE: Skipping malformed-line test — Bun.JSONL.parseChunk hangs
|
||||||
|
// indefinitely in its error-recovery loop when encountering bad lines.
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── addItemToJSONCArray ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("addItemToJSONCArray", () => {
|
||||||
|
test("appends to existing array", () => {
|
||||||
|
const result = addItemToJSONCArray('["a","b"]', "c");
|
||||||
|
const parsed = JSON.parse(result);
|
||||||
|
expect(parsed).toEqual(["a", "b", "c"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("appends to empty array", () => {
|
||||||
|
const result = addItemToJSONCArray("[]", "item");
|
||||||
|
const parsed = JSON.parse(result);
|
||||||
|
expect(parsed).toEqual(["item"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("creates array from empty content", () => {
|
||||||
|
const result = addItemToJSONCArray("", "first");
|
||||||
|
const parsed = JSON.parse(result);
|
||||||
|
expect(parsed).toEqual(["first"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles object item", () => {
|
||||||
|
const result = addItemToJSONCArray("[]", { key: "val" });
|
||||||
|
const parsed = JSON.parse(result);
|
||||||
|
expect(parsed).toEqual([{ key: "val" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("wraps item in new array for non-array content", () => {
|
||||||
|
const result = addItemToJSONCArray('{"a":1}', "item");
|
||||||
|
const parsed = JSON.parse(result);
|
||||||
|
expect(parsed).toEqual(["item"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
72
src/utils/__tests__/path.test.ts
Normal file
72
src/utils/__tests__/path.test.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { containsPathTraversal, normalizePathForConfigKey } from "../path";
|
||||||
|
|
||||||
|
// ─── containsPathTraversal ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("containsPathTraversal", () => {
|
||||||
|
test("detects ../ at start", () => {
|
||||||
|
expect(containsPathTraversal("../foo")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("detects ../ in middle", () => {
|
||||||
|
expect(containsPathTraversal("foo/../bar")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("detects .. at end", () => {
|
||||||
|
expect(containsPathTraversal("foo/..")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("detects standalone ..", () => {
|
||||||
|
expect(containsPathTraversal("..")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("detects backslash traversal", () => {
|
||||||
|
expect(containsPathTraversal("foo\\..\\bar")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns false for normal path", () => {
|
||||||
|
expect(containsPathTraversal("foo/bar/baz")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns false for single dot", () => {
|
||||||
|
expect(containsPathTraversal("./foo")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns false for ... in filename", () => {
|
||||||
|
expect(containsPathTraversal("foo/...bar")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns false for empty string", () => {
|
||||||
|
expect(containsPathTraversal("")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns false for dotdot in filename without separator", () => {
|
||||||
|
expect(containsPathTraversal("foo..bar")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── normalizePathForConfigKey ──────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("normalizePathForConfigKey", () => {
|
||||||
|
test("normalizes forward slashes (no change on POSIX)", () => {
|
||||||
|
expect(normalizePathForConfigKey("foo/bar/baz")).toBe("foo/bar/baz");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolves dot segments", () => {
|
||||||
|
expect(normalizePathForConfigKey("foo/./bar")).toBe("foo/bar");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolves double-dot segments", () => {
|
||||||
|
expect(normalizePathForConfigKey("foo/bar/../baz")).toBe("foo/baz");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles absolute path", () => {
|
||||||
|
const result = normalizePathForConfigKey("/Users/test/project");
|
||||||
|
expect(result).toBe("/Users/test/project");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("converts backslashes to forward slashes", () => {
|
||||||
|
const result = normalizePathForConfigKey("foo\\bar\\baz");
|
||||||
|
expect(result).toBe("foo/bar/baz");
|
||||||
|
});
|
||||||
|
});
|
||||||
296
src/utils/__tests__/tokens.test.ts
Normal file
296
src/utils/__tests__/tokens.test.ts
Normal file
@ -0,0 +1,296 @@
|
|||||||
|
import { mock, describe, expect, test } from "bun:test";
|
||||||
|
|
||||||
|
// Mock heavy dependency chain: tokenEstimation.ts → log.ts → bootstrap/state.ts
|
||||||
|
mock.module("src/utils/log.ts", () => ({
|
||||||
|
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.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)).toBe(180);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles missing cache fields", () => {
|
||||||
|
const usage = {
|
||||||
|
input_tokens: 100,
|
||||||
|
output_tokens: 50,
|
||||||
|
};
|
||||||
|
expect(getTokenCountFromUsage(usage)).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)).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);
|
||||||
|
});
|
||||||
|
});
|
||||||
146
src/utils/__tests__/truncate.test.ts
Normal file
146
src/utils/__tests__/truncate.test.ts
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import {
|
||||||
|
truncatePathMiddle,
|
||||||
|
truncateToWidth,
|
||||||
|
truncateStartToWidth,
|
||||||
|
truncateToWidthNoEllipsis,
|
||||||
|
truncate,
|
||||||
|
wrapText,
|
||||||
|
} from "../truncate";
|
||||||
|
|
||||||
|
// ─── truncateToWidth ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("truncateToWidth", () => {
|
||||||
|
test("returns original when within limit", () => {
|
||||||
|
expect(truncateToWidth("hello", 10)).toBe("hello");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("truncates long string with ellipsis", () => {
|
||||||
|
const result = truncateToWidth("hello world", 8);
|
||||||
|
expect(result.endsWith("…")).toBe(true);
|
||||||
|
expect(result.length).toBeLessThanOrEqual(9); // 8 visible + ellipsis char
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns ellipsis for maxWidth 1", () => {
|
||||||
|
expect(truncateToWidth("hello", 1)).toBe("…");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles empty string", () => {
|
||||||
|
expect(truncateToWidth("", 10)).toBe("");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── truncateStartToWidth ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("truncateStartToWidth", () => {
|
||||||
|
test("returns original when within limit", () => {
|
||||||
|
expect(truncateStartToWidth("hello", 10)).toBe("hello");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("truncates from start with ellipsis prefix", () => {
|
||||||
|
const result = truncateStartToWidth("hello world", 8);
|
||||||
|
expect(result.startsWith("…")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns ellipsis for maxWidth 1", () => {
|
||||||
|
expect(truncateStartToWidth("hello", 1)).toBe("…");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── truncateToWidthNoEllipsis ──────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("truncateToWidthNoEllipsis", () => {
|
||||||
|
test("returns original when within limit", () => {
|
||||||
|
expect(truncateToWidthNoEllipsis("hello", 10)).toBe("hello");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("truncates without ellipsis", () => {
|
||||||
|
const result = truncateToWidthNoEllipsis("hello world", 5);
|
||||||
|
expect(result).toBe("hello");
|
||||||
|
expect(result.includes("…")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns empty for maxWidth 0", () => {
|
||||||
|
expect(truncateToWidthNoEllipsis("hello", 0)).toBe("");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── truncatePathMiddle ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("truncatePathMiddle", () => {
|
||||||
|
test("returns original when path fits", () => {
|
||||||
|
expect(truncatePathMiddle("src/index.ts", 50)).toBe("src/index.ts");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("truncates middle of long path", () => {
|
||||||
|
const path = "src/components/deeply/nested/folder/MyComponent.tsx";
|
||||||
|
const result = truncatePathMiddle(path, 30);
|
||||||
|
expect(result).toContain("…");
|
||||||
|
expect(result.endsWith("MyComponent.tsx")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns ellipsis for maxLength 0", () => {
|
||||||
|
expect(truncatePathMiddle("src/index.ts", 0)).toBe("…");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles path without slashes", () => {
|
||||||
|
const result = truncatePathMiddle("verylongfilename.ts", 10);
|
||||||
|
expect(result).toContain("…");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles short maxLength < 5", () => {
|
||||||
|
const result = truncatePathMiddle("src/components/foo.ts", 4);
|
||||||
|
expect(result).toContain("…");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── truncate ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("truncate", () => {
|
||||||
|
test("returns original when within limit", () => {
|
||||||
|
expect(truncate("hello", 10)).toBe("hello");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("truncates long string", () => {
|
||||||
|
const result = truncate("hello world foo bar", 10);
|
||||||
|
expect(result).toContain("…");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("truncates at newline in singleLine mode", () => {
|
||||||
|
const result = truncate("first line\nsecond line", 50, true);
|
||||||
|
expect(result).toBe("first line…");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not truncate at newline when singleLine is false", () => {
|
||||||
|
const result = truncate("first\nsecond", 50, false);
|
||||||
|
expect(result).toBe("first\nsecond");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("truncates singleLine when first line exceeds maxWidth", () => {
|
||||||
|
const result = truncate("a very long first line\nsecond", 10, true);
|
||||||
|
expect(result).toContain("…");
|
||||||
|
expect(result).not.toContain("\n");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── wrapText ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("wrapText", () => {
|
||||||
|
test("wraps text at specified width", () => {
|
||||||
|
const result = wrapText("hello world", 6);
|
||||||
|
expect(result.length).toBeGreaterThan(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns single line when text fits", () => {
|
||||||
|
expect(wrapText("hello", 10)).toEqual(["hello"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles empty string", () => {
|
||||||
|
expect(wrapText("", 10)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("wraps each character on width 1", () => {
|
||||||
|
const result = wrapText("abc", 1);
|
||||||
|
expect(result).toEqual(["a", "b", "c"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user