Merge branch 'claude-code-best:main' into main

This commit is contained in:
Jiguo Li
2026-04-02 10:12:49 +08:00
committed by GitHub
40 changed files with 6129 additions and 9 deletions

201
src/__tests__/Tool.test.ts Normal file
View File

@@ -0,0 +1,201 @@
import { describe, expect, test } from "bun:test";
import {
buildTool,
toolMatchesName,
findToolByName,
getEmptyToolPermissionContext,
filterToolProgressMessages,
} from "../Tool";
// Minimal tool definition for testing buildTool
function makeMinimalToolDef(overrides: Record<string, unknown> = {}) {
return {
name: "TestTool",
inputSchema: { type: "object" as const } as any,
maxResultSizeChars: 10000,
call: async () => ({ data: "ok" }),
description: async () => "A test tool",
prompt: async () => "test prompt",
mapToolResultToToolResultBlockParam: (content: unknown, toolUseID: string) => ({
type: "tool_result" as const,
tool_use_id: toolUseID,
content: String(content),
}),
renderToolUseMessage: () => null,
...overrides,
};
}
describe("buildTool", () => {
test("fills in default isEnabled as true", () => {
const tool = buildTool(makeMinimalToolDef());
expect(tool.isEnabled()).toBe(true);
});
test("fills in default isConcurrencySafe as false", () => {
const tool = buildTool(makeMinimalToolDef());
expect(tool.isConcurrencySafe({})).toBe(false);
});
test("fills in default isReadOnly as false", () => {
const tool = buildTool(makeMinimalToolDef());
expect(tool.isReadOnly({})).toBe(false);
});
test("fills in default isDestructive as false", () => {
const tool = buildTool(makeMinimalToolDef());
expect(tool.isDestructive!({})).toBe(false);
});
test("fills in default checkPermissions as allow", async () => {
const tool = buildTool(makeMinimalToolDef());
const input = { foo: "bar" };
const result = await tool.checkPermissions(input, {} as any);
expect(result).toEqual({ behavior: "allow", updatedInput: input });
});
test("fills in default userFacingName from tool name", () => {
const tool = buildTool(makeMinimalToolDef());
expect(tool.userFacingName(undefined)).toBe("TestTool");
});
test("fills in default toAutoClassifierInput as empty string", () => {
const tool = buildTool(makeMinimalToolDef());
expect(tool.toAutoClassifierInput({})).toBe("");
});
test("preserves explicitly provided methods", () => {
const tool = buildTool(
makeMinimalToolDef({
isEnabled: () => false,
isConcurrencySafe: () => true,
isReadOnly: () => true,
})
);
expect(tool.isEnabled()).toBe(false);
expect(tool.isConcurrencySafe({})).toBe(true);
expect(tool.isReadOnly({})).toBe(true);
});
test("preserves all non-defaultable properties", () => {
const tool = buildTool(makeMinimalToolDef());
expect(tool.name).toBe("TestTool");
expect(tool.maxResultSizeChars).toBe(10000);
expect(typeof tool.call).toBe("function");
expect(typeof tool.description).toBe("function");
expect(typeof tool.prompt).toBe("function");
});
});
describe("toolMatchesName", () => {
test("returns true for exact name match", () => {
expect(toolMatchesName({ name: "Bash" }, "Bash")).toBe(true);
});
test("returns false for non-matching name", () => {
expect(toolMatchesName({ name: "Bash" }, "Read")).toBe(false);
});
test("returns true when name matches an alias", () => {
expect(
toolMatchesName({ name: "Bash", aliases: ["BashTool", "Shell"] }, "BashTool")
).toBe(true);
});
test("returns false when aliases is undefined", () => {
expect(toolMatchesName({ name: "Bash" }, "BashTool")).toBe(false);
});
test("returns false when aliases is empty", () => {
expect(
toolMatchesName({ name: "Bash", aliases: [] }, "BashTool")
).toBe(false);
});
});
describe("findToolByName", () => {
const mockTools = [
buildTool(makeMinimalToolDef({ name: "Bash" })),
buildTool(makeMinimalToolDef({ name: "Read", aliases: ["FileRead"] })),
buildTool(makeMinimalToolDef({ name: "Edit" })),
];
test("finds tool by primary name", () => {
const tool = findToolByName(mockTools, "Bash");
expect(tool).toBeDefined();
expect(tool!.name).toBe("Bash");
});
test("finds tool by alias", () => {
const tool = findToolByName(mockTools, "FileRead");
expect(tool).toBeDefined();
expect(tool!.name).toBe("Read");
});
test("returns undefined when no match", () => {
expect(findToolByName(mockTools, "NonExistent")).toBeUndefined();
});
test("returns first match when duplicates exist", () => {
const dupeTools = [
buildTool(makeMinimalToolDef({ name: "Bash", maxResultSizeChars: 100 })),
buildTool(makeMinimalToolDef({ name: "Bash", maxResultSizeChars: 200 })),
];
const tool = findToolByName(dupeTools, "Bash");
expect(tool!.maxResultSizeChars).toBe(100);
});
});
describe("getEmptyToolPermissionContext", () => {
test("returns default permission mode", () => {
const ctx = getEmptyToolPermissionContext();
expect(ctx.mode).toBe("default");
});
test("returns empty maps and arrays", () => {
const ctx = getEmptyToolPermissionContext();
expect(ctx.additionalWorkingDirectories.size).toBe(0);
expect(ctx.alwaysAllowRules).toEqual({});
expect(ctx.alwaysDenyRules).toEqual({});
expect(ctx.alwaysAskRules).toEqual({});
});
test("returns isBypassPermissionsModeAvailable as false", () => {
const ctx = getEmptyToolPermissionContext();
expect(ctx.isBypassPermissionsModeAvailable).toBe(false);
});
});
describe("filterToolProgressMessages", () => {
test("filters out hook_progress messages", () => {
const messages = [
{ data: { type: "hook_progress", hookName: "pre" } },
{ data: { type: "tool_progress", toolName: "Bash" } },
] as any[];
const result = filterToolProgressMessages(messages);
expect(result).toHaveLength(1);
expect((result[0]!.data as any).type).toBe("tool_progress");
});
test("keeps tool progress messages", () => {
const messages = [
{ data: { type: "tool_progress", toolName: "Bash" } },
{ data: { type: "tool_progress", toolName: "Read" } },
] as any[];
const result = filterToolProgressMessages(messages);
expect(result).toHaveLength(2);
});
test("returns empty array for empty input", () => {
expect(filterToolProgressMessages([])).toEqual([]);
});
test("handles messages without type field", () => {
const messages = [
{ data: { toolName: "Bash" } },
{ data: { type: "hook_progress" } },
] as any[];
const result = filterToolProgressMessages(messages);
expect(result).toHaveLength(1);
});
});

View File

@@ -0,0 +1,82 @@
import { describe, expect, test } from "bun:test";
import { parseToolPreset, filterToolsByDenyRules } from "../tools";
import { getEmptyToolPermissionContext } from "../Tool";
describe("parseToolPreset", () => {
test('returns "default" for "default" input', () => {
expect(parseToolPreset("default")).toBe("default");
});
test('returns "default" for "Default" input (case-insensitive)', () => {
expect(parseToolPreset("Default")).toBe("default");
});
test("returns null for unknown preset", () => {
expect(parseToolPreset("unknown")).toBeNull();
});
test("returns null for empty string", () => {
expect(parseToolPreset("")).toBeNull();
});
test("returns null for random string", () => {
expect(parseToolPreset("custom-preset")).toBeNull();
});
});
// ─── filterToolsByDenyRules ─────────────────────────────────────────────
describe("filterToolsByDenyRules", () => {
const mockTools = [
{ name: "Bash", mcpInfo: undefined },
{ name: "Read", mcpInfo: undefined },
{ name: "Write", mcpInfo: undefined },
{ name: "mcp__server__tool", mcpInfo: { serverName: "server", toolName: "tool" } },
];
test("returns all tools when no deny rules", () => {
const ctx = getEmptyToolPermissionContext();
const result = filterToolsByDenyRules(mockTools, ctx);
expect(result).toHaveLength(4);
});
test("filters out denied tool by name", () => {
const ctx = {
...getEmptyToolPermissionContext(),
alwaysDenyRules: {
localSettings: ["Bash"],
},
};
const result = filterToolsByDenyRules(mockTools, ctx as any);
expect(result.find((t) => t.name === "Bash")).toBeUndefined();
expect(result).toHaveLength(3);
});
test("filters out multiple denied tools", () => {
const ctx = {
...getEmptyToolPermissionContext(),
alwaysDenyRules: {
localSettings: ["Bash", "Write"],
},
};
const result = filterToolsByDenyRules(mockTools, ctx as any);
expect(result).toHaveLength(2);
expect(result.map((t) => t.name)).toEqual(["Read", "mcp__server__tool"]);
});
test("returns empty array when all tools denied", () => {
const ctx = {
...getEmptyToolPermissionContext(),
alwaysDenyRules: {
localSettings: mockTools.map((t) => t.name),
},
};
const result = filterToolsByDenyRules(mockTools, ctx as any);
expect(result).toHaveLength(0);
});
test("handles empty tools array", () => {
const ctx = getEmptyToolPermissionContext();
expect(filterToolsByDenyRules([], ctx)).toEqual([]);
});
});

View File

@@ -0,0 +1,164 @@
import { mock, describe, expect, test } from "bun:test";
// Mock log.ts to cut the heavy dependency chain
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: () => {},
}));
const {
normalizeQuotes,
stripTrailingWhitespace,
findActualString,
preserveQuoteStyle,
applyEditToFile,
LEFT_SINGLE_CURLY_QUOTE,
RIGHT_SINGLE_CURLY_QUOTE,
LEFT_DOUBLE_CURLY_QUOTE,
RIGHT_DOUBLE_CURLY_QUOTE,
} = await import("../utils");
// ─── normalizeQuotes ────────────────────────────────────────────────────
describe("normalizeQuotes", () => {
test("converts left single curly to straight", () => {
expect(normalizeQuotes(`${LEFT_SINGLE_CURLY_QUOTE}hello`)).toBe("'hello");
});
test("converts right single curly to straight", () => {
expect(normalizeQuotes(`hello${RIGHT_SINGLE_CURLY_QUOTE}`)).toBe("hello'");
});
test("converts left double curly to straight", () => {
expect(normalizeQuotes(`${LEFT_DOUBLE_CURLY_QUOTE}hello`)).toBe('"hello');
});
test("converts right double curly to straight", () => {
expect(normalizeQuotes(`hello${RIGHT_DOUBLE_CURLY_QUOTE}`)).toBe('hello"');
});
test("leaves straight quotes unchanged", () => {
expect(normalizeQuotes("'hello' \"world\"")).toBe("'hello' \"world\"");
});
test("handles empty string", () => {
expect(normalizeQuotes("")).toBe("");
});
});
// ─── stripTrailingWhitespace ────────────────────────────────────────────
describe("stripTrailingWhitespace", () => {
test("strips trailing spaces from lines", () => {
expect(stripTrailingWhitespace("hello \nworld ")).toBe("hello\nworld");
});
test("strips trailing tabs", () => {
expect(stripTrailingWhitespace("hello\t\nworld\t")).toBe("hello\nworld");
});
test("preserves leading whitespace", () => {
expect(stripTrailingWhitespace(" hello \n world ")).toBe(
" hello\n world"
);
});
test("handles empty string", () => {
expect(stripTrailingWhitespace("")).toBe("");
});
test("handles CRLF line endings", () => {
expect(stripTrailingWhitespace("hello \r\nworld ")).toBe(
"hello\r\nworld"
);
});
test("handles no trailing whitespace", () => {
expect(stripTrailingWhitespace("hello\nworld")).toBe("hello\nworld");
});
});
// ─── findActualString ───────────────────────────────────────────────────
describe("findActualString", () => {
test("finds exact match", () => {
expect(findActualString("hello world", "hello")).toBe("hello");
});
test("finds match with curly quotes normalized", () => {
const fileContent = `${LEFT_DOUBLE_CURLY_QUOTE}hello${RIGHT_DOUBLE_CURLY_QUOTE}`;
const result = findActualString(fileContent, '"hello"');
expect(result).not.toBeNull();
});
test("returns null when not found", () => {
expect(findActualString("hello world", "xyz")).toBeNull();
});
test("returns null for empty search in non-empty content", () => {
// Empty string is always found at index 0 via includes()
const result = findActualString("hello", "");
expect(result).toBe("");
});
});
// ─── preserveQuoteStyle ─────────────────────────────────────────────────
describe("preserveQuoteStyle", () => {
test("returns newString unchanged when no normalization happened", () => {
expect(preserveQuoteStyle("hello", "hello", "world")).toBe("world");
});
test("converts straight double quotes to curly in replacement", () => {
const oldString = '"hello"';
const actualOldString = `${LEFT_DOUBLE_CURLY_QUOTE}hello${RIGHT_DOUBLE_CURLY_QUOTE}`;
const newString = '"world"';
const result = preserveQuoteStyle(oldString, actualOldString, newString);
expect(result).toContain(LEFT_DOUBLE_CURLY_QUOTE);
expect(result).toContain(RIGHT_DOUBLE_CURLY_QUOTE);
});
});
// ─── applyEditToFile ────────────────────────────────────────────────────
describe("applyEditToFile", () => {
test("replaces first occurrence by default", () => {
expect(applyEditToFile("foo bar foo", "foo", "baz")).toBe("baz bar foo");
});
test("replaces all occurrences with replaceAll=true", () => {
expect(applyEditToFile("foo bar foo", "foo", "baz", true)).toBe(
"baz bar baz"
);
});
test("handles deletion (empty newString) with trailing newline", () => {
const result = applyEditToFile("line1\nline2\nline3\n", "line2", "");
expect(result).toBe("line1\nline3\n");
});
test("handles deletion without trailing newline", () => {
const result = applyEditToFile("foobar", "foo", "");
expect(result).toBe("bar");
});
test("handles no match (returns original)", () => {
expect(applyEditToFile("hello world", "xyz", "abc")).toBe("hello world");
});
test("handles empty original content with insertion", () => {
expect(applyEditToFile("", "", "new content")).toBe("new content");
});
});

View File

@@ -0,0 +1,134 @@
import { describe, expect, test } from "bun:test";
import { parseGitCommitId, detectGitOperation } from "../gitOperationTracking";
describe("parseGitCommitId", () => {
test("extracts commit hash from git commit output", () => {
expect(parseGitCommitId("[main abc1234] fix: some message")).toBe("abc1234");
});
test("extracts hash from root commit output", () => {
expect(
parseGitCommitId("[main (root-commit) abc1234] initial commit")
).toBe("abc1234");
});
test("returns undefined for non-commit output", () => {
expect(parseGitCommitId("nothing to commit")).toBeUndefined();
});
test("handles various branch name formats", () => {
expect(parseGitCommitId("[feature/foo abc1234] message")).toBe("abc1234");
expect(parseGitCommitId("[fix/bar-baz abc1234] message")).toBe("abc1234");
expect(parseGitCommitId("[v1.0.0 abc1234] message")).toBe("abc1234");
});
test("returns undefined for empty string", () => {
expect(parseGitCommitId("")).toBeUndefined();
});
});
describe("detectGitOperation", () => {
test("detects git commit operation", () => {
const result = detectGitOperation(
"git commit -m 'fix bug'",
"[main abc1234] fix bug"
);
expect(result.commit).toBeDefined();
expect(result.commit!.sha).toBe("abc123");
expect(result.commit!.kind).toBe("committed");
});
test("detects git commit --amend operation", () => {
const result = detectGitOperation(
"git commit --amend -m 'updated'",
"[main def5678] updated"
);
expect(result.commit).toBeDefined();
expect(result.commit!.kind).toBe("amended");
});
test("detects git cherry-pick operation", () => {
const result = detectGitOperation(
"git cherry-pick abc1234",
"[main def5678] cherry picked commit"
);
expect(result.commit).toBeDefined();
expect(result.commit!.kind).toBe("cherry-picked");
});
test("detects git push operation", () => {
const result = detectGitOperation(
"git push origin main",
" abc1234..def5678 main -> main"
);
expect(result.push).toBeDefined();
expect(result.push!.branch).toBe("main");
});
test("detects git merge operation", () => {
const result = detectGitOperation(
"git merge feature-branch",
"Merge made by the 'ort' strategy."
);
expect(result.branch).toBeDefined();
expect(result.branch!.action).toBe("merged");
expect(result.branch!.ref).toBe("feature-branch");
});
test("detects git rebase operation", () => {
const result = detectGitOperation(
"git rebase main",
"Successfully rebased and updated refs/heads/feature."
);
expect(result.branch).toBeDefined();
expect(result.branch!.action).toBe("rebased");
expect(result.branch!.ref).toBe("main");
});
test("returns null for non-git commands", () => {
const result = detectGitOperation("ls -la", "total 100\ndrwxr-xr-x");
expect(result.commit).toBeUndefined();
expect(result.push).toBeUndefined();
expect(result.branch).toBeUndefined();
expect(result.pr).toBeUndefined();
});
test("detects gh pr create operation", () => {
const result = detectGitOperation(
"gh pr create --title 'fix' --body 'desc'",
"https://github.com/owner/repo/pull/42"
);
expect(result.pr).toBeDefined();
expect(result.pr!.number).toBe(42);
expect(result.pr!.action).toBe("created");
});
test("detects gh pr merge operation", () => {
const result = detectGitOperation(
"gh pr merge 42",
"✓ Merged pull request owner/repo#42"
);
expect(result.pr).toBeDefined();
expect(result.pr!.number).toBe(42);
expect(result.pr!.action).toBe("merged");
});
test("handles git commit with -c options", () => {
const result = detectGitOperation(
"git -c commit.gpgsign=false commit -m 'msg'",
"[main aaa1111] msg"
);
expect(result.commit).toBeDefined();
expect(result.commit!.sha).toBe("aaa111");
});
test("detects fast-forward merge", () => {
const result = detectGitOperation(
"git merge develop",
"Fast-forward\n file.txt | 1 +\n 1 file changed"
);
expect(result.branch).toBeDefined();
expect(result.branch!.action).toBe("merged");
expect(result.branch!.ref).toBe("develop");
});
});

View File

@@ -0,0 +1,123 @@
import { describe, expect, test } from "bun:test";
import {
stripHtmlComments,
isMemoryFilePath,
getLargeMemoryFiles,
MAX_MEMORY_CHARACTER_COUNT,
type MemoryFileInfo,
} from "../claudemd";
function mockMemoryFile(overrides: Partial<MemoryFileInfo> = {}): MemoryFileInfo {
return {
path: "/project/CLAUDE.md",
type: "Project",
content: "test content",
...overrides,
};
}
describe("stripHtmlComments", () => {
test("strips block-level HTML comments (own line)", () => {
// CommonMark type-2 HTML blocks: comment must start at beginning of line
const result = stripHtmlComments("text\n<!-- block comment -->\nmore");
expect(result.content).not.toContain("block comment");
expect(result.stripped).toBe(true);
});
test("returns stripped: false when no comments", () => {
const result = stripHtmlComments("no comments here");
expect(result.stripped).toBe(false);
expect(result.content).toBe("no comments here");
});
test("returns stripped: true when block comments exist", () => {
const result = stripHtmlComments("hello\n<!-- world -->\nend");
expect(result.stripped).toBe(true);
});
test("handles empty string", () => {
const result = stripHtmlComments("");
expect(result.content).toBe("");
expect(result.stripped).toBe(false);
});
test("handles multiple block comments", () => {
const result = stripHtmlComments(
"a\n<!-- c1 -->\nb\n<!-- c2 -->\nc"
);
expect(result.content).not.toContain("c1");
expect(result.content).not.toContain("c2");
expect(result.stripped).toBe(true);
});
test("preserves code block content", () => {
const input = "text\n```html\n<!-- not stripped -->\n```\nmore";
const result = stripHtmlComments(input);
expect(result.content).toContain("<!-- not stripped -->");
});
test("preserves inline comments within paragraphs", () => {
// Inline comments are NOT stripped (CommonMark paragraph semantics)
const result = stripHtmlComments("text <!-- inline --> more");
expect(result.content).toContain("<!-- inline -->");
expect(result.stripped).toBe(false);
});
});
describe("isMemoryFilePath", () => {
test("returns true for CLAUDE.md path", () => {
expect(isMemoryFilePath("/project/CLAUDE.md")).toBe(true);
});
test("returns true for CLAUDE.local.md path", () => {
expect(isMemoryFilePath("/project/CLAUDE.local.md")).toBe(true);
});
test("returns true for .claude/rules/ path", () => {
expect(isMemoryFilePath("/project/.claude/rules/foo.md")).toBe(true);
});
test("returns false for regular file", () => {
expect(isMemoryFilePath("/project/src/main.ts")).toBe(false);
});
test("returns false for unrelated .md file", () => {
expect(isMemoryFilePath("/project/README.md")).toBe(false);
});
test("returns false for .claude directory non-rules file", () => {
expect(isMemoryFilePath("/project/.claude/settings.json")).toBe(false);
});
});
describe("getLargeMemoryFiles", () => {
test("returns files exceeding threshold", () => {
const largeContent = "x".repeat(MAX_MEMORY_CHARACTER_COUNT + 1);
const files = [
mockMemoryFile({ content: "small" }),
mockMemoryFile({ content: largeContent, path: "/big.md" }),
];
const result = getLargeMemoryFiles(files);
expect(result).toHaveLength(1);
expect(result[0].path).toBe("/big.md");
});
test("returns empty array when all files are small", () => {
const files = [
mockMemoryFile({ content: "small" }),
mockMemoryFile({ content: "also small" }),
];
expect(getLargeMemoryFiles(files)).toEqual([]);
});
test("correctly identifies threshold boundary", () => {
const atThreshold = "x".repeat(MAX_MEMORY_CHARACTER_COUNT);
const overThreshold = "x".repeat(MAX_MEMORY_CHARACTER_COUNT + 1);
const files = [
mockMemoryFile({ content: atThreshold }),
mockMemoryFile({ content: overThreshold }),
];
const result = getLargeMemoryFiles(files);
expect(result).toHaveLength(1);
});
});

View File

@@ -0,0 +1,253 @@
import { describe, expect, test } from "bun:test";
import { parseCronExpression, computeNextCronRun, cronToHuman } from "../cron";
describe("parseCronExpression", () => {
describe("valid expressions", () => {
test("parses wildcard fields", () => {
const result = parseCronExpression("* * * * *");
expect(result).not.toBeNull();
expect(result!.minute).toHaveLength(60);
expect(result!.hour).toHaveLength(24);
expect(result!.dayOfMonth).toHaveLength(31);
expect(result!.month).toHaveLength(12);
expect(result!.dayOfWeek).toHaveLength(7);
});
test("parses specific values", () => {
const result = parseCronExpression("30 14 1 6 3");
expect(result).not.toBeNull();
expect(result!.minute).toEqual([30]);
expect(result!.hour).toEqual([14]);
expect(result!.dayOfMonth).toEqual([1]);
expect(result!.month).toEqual([6]);
expect(result!.dayOfWeek).toEqual([3]);
});
test("parses step syntax", () => {
const result = parseCronExpression("*/5 * * * *");
expect(result).not.toBeNull();
expect(result!.minute).toEqual([0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55]);
});
test("parses range syntax", () => {
const result = parseCronExpression("1-5 * * * *");
expect(result).not.toBeNull();
expect(result!.minute).toEqual([1, 2, 3, 4, 5]);
});
test("parses range with step", () => {
const result = parseCronExpression("1-10/3 * * * *");
expect(result).not.toBeNull();
expect(result!.minute).toEqual([1, 4, 7, 10]);
});
test("parses comma-separated list", () => {
const result = parseCronExpression("1,15,30 * * * *");
expect(result).not.toBeNull();
expect(result!.minute).toEqual([1, 15, 30]);
});
test("parses day-of-week 7 as Sunday alias", () => {
const result = parseCronExpression("0 0 * * 7");
expect(result).not.toBeNull();
expect(result!.dayOfWeek).toEqual([0]);
});
test("parses range with day-of-week 7", () => {
const result = parseCronExpression("0 0 * * 5-7");
expect(result).not.toBeNull();
expect(result!.dayOfWeek).toEqual([0, 5, 6]);
});
test("parses complex combined expression", () => {
const result = parseCronExpression("0,30 9-17 * * 1-5");
expect(result).not.toBeNull();
expect(result!.minute).toEqual([0, 30]);
expect(result!.hour).toEqual([9, 10, 11, 12, 13, 14, 15, 16, 17]);
expect(result!.dayOfWeek).toEqual([1, 2, 3, 4, 5]);
});
});
describe("invalid expressions", () => {
test("returns null for wrong field count", () => {
expect(parseCronExpression("* * *")).toBeNull();
});
test("returns null for out-of-range values", () => {
expect(parseCronExpression("60 * * * *")).toBeNull();
});
test("returns null for invalid step", () => {
expect(parseCronExpression("*/0 * * * *")).toBeNull();
});
test("returns null for reversed range", () => {
expect(parseCronExpression("10-5 * * * *")).toBeNull();
});
test("returns null for empty string", () => {
expect(parseCronExpression("")).toBeNull();
});
test("returns null for non-numeric tokens", () => {
expect(parseCronExpression("abc * * * *")).toBeNull();
});
});
describe("field range validation", () => {
test("minute: 0-59", () => {
expect(parseCronExpression("0 * * * *")).not.toBeNull();
expect(parseCronExpression("59 * * * *")).not.toBeNull();
expect(parseCronExpression("60 * * * *")).toBeNull();
});
test("hour: 0-23", () => {
expect(parseCronExpression("* 0 * * *")).not.toBeNull();
expect(parseCronExpression("* 23 * * *")).not.toBeNull();
expect(parseCronExpression("* 24 * * *")).toBeNull();
});
test("dayOfMonth: 1-31", () => {
expect(parseCronExpression("* * 1 * *")).not.toBeNull();
expect(parseCronExpression("* * 31 * *")).not.toBeNull();
expect(parseCronExpression("* * 0 * *")).toBeNull();
expect(parseCronExpression("* * 32 * *")).toBeNull();
});
test("month: 1-12", () => {
expect(parseCronExpression("* * * 1 *")).not.toBeNull();
expect(parseCronExpression("* * * 12 *")).not.toBeNull();
expect(parseCronExpression("* * * 0 *")).toBeNull();
expect(parseCronExpression("* * * 13 *")).toBeNull();
});
test("dayOfWeek: 0-6 (plus 7 alias)", () => {
expect(parseCronExpression("* * * * 0")).not.toBeNull();
expect(parseCronExpression("* * * * 6")).not.toBeNull();
expect(parseCronExpression("* * * * 7")).not.toBeNull(); // alias for 0
expect(parseCronExpression("* * * * 8")).toBeNull();
});
});
});
describe("computeNextCronRun", () => {
test("finds next minute", () => {
const fields = parseCronExpression("31 14 * * *")!;
const from = new Date(2026, 0, 15, 14, 30, 45); // 14:30:45
const next = computeNextCronRun(fields, from);
expect(next).not.toBeNull();
expect(next!.getHours()).toBe(14);
expect(next!.getMinutes()).toBe(31);
});
test("finds next hour", () => {
const fields = parseCronExpression("0 15 * * *")!;
const from = new Date(2026, 0, 15, 14, 30);
const next = computeNextCronRun(fields, from);
expect(next).not.toBeNull();
expect(next!.getHours()).toBe(15);
expect(next!.getMinutes()).toBe(0);
});
test("rolls to next day", () => {
const fields = parseCronExpression("0 10 * * *")!;
const from = new Date(2026, 0, 15, 14, 30);
const next = computeNextCronRun(fields, from);
expect(next).not.toBeNull();
expect(next!.getDate()).toBe(16);
expect(next!.getHours()).toBe(10);
});
test("is strictly after from date", () => {
const fields = parseCronExpression("30 14 * * *")!;
const from = new Date(2026, 0, 15, 14, 30, 0); // exactly on cron time
const next = computeNextCronRun(fields, from);
expect(next).not.toBeNull();
expect(next!.getTime()).toBeGreaterThan(from.getTime());
});
test("every 5 minutes from arbitrary time", () => {
const fields = parseCronExpression("*/5 * * * *")!;
const from = new Date(2026, 0, 15, 14, 32);
const next = computeNextCronRun(fields, from);
expect(next).not.toBeNull();
expect(next!.getMinutes()).toBe(35);
});
test("every minute", () => {
const fields = parseCronExpression("* * * * *")!;
const from = new Date(2026, 0, 15, 14, 32, 45);
const next = computeNextCronRun(fields, from);
expect(next).not.toBeNull();
expect(next!.getMinutes()).toBe(33);
});
test("handles step across midnight", () => {
const fields = parseCronExpression("0 0 * * *")!;
const from = new Date(2026, 0, 15, 23, 59);
const next = computeNextCronRun(fields, from);
expect(next).not.toBeNull();
expect(next!.getHours()).toBe(0);
expect(next!.getDate()).toBe(16);
});
test("OR semantics when both dom and dow constrained", () => {
// dom=15, dow=3(Wed) - matches 15th OR Wednesday
const fields = parseCronExpression("0 0 15 * 3")!;
const from = new Date(2026, 0, 12, 0, 0); // Monday Jan 12
const next = computeNextCronRun(fields, from);
expect(next).not.toBeNull();
// Should match the first of either: next Wednesday(Jan 14) or 15th(Jan 15)
const dayOfWeek = next!.getDay();
const dayOfMonth = next!.getDate();
expect(dayOfWeek === 3 || dayOfMonth === 15).toBe(true);
});
});
describe("cronToHuman", () => {
test("every N minutes", () => {
expect(cronToHuman("*/5 * * * *")).toBe("Every 5 minutes");
});
test("every minute", () => {
expect(cronToHuman("*/1 * * * *")).toBe("Every minute");
});
test("every hour at :00", () => {
expect(cronToHuman("0 * * * *")).toBe("Every hour");
});
test("every hour at :30", () => {
expect(cronToHuman("30 * * * *")).toBe("Every hour at :30");
});
test("every N hours", () => {
expect(cronToHuman("0 */2 * * *")).toBe("Every 2 hours");
});
test("daily at specific time", () => {
const result = cronToHuman("30 9 * * *");
expect(result).toContain("Every day at");
expect(result).toContain("9:30");
});
test("specific day of week", () => {
const result = cronToHuman("0 9 * * 3");
expect(result).toContain("Wednesday");
expect(result).toContain("9:00");
});
test("weekdays", () => {
const result = cronToHuman("0 9 * * 1-5");
expect(result).toContain("Weekdays");
expect(result).toContain("9:00");
});
test("returns raw cron for complex patterns", () => {
expect(cronToHuman("0,30 9-17 * * 1-5")).toBe("0,30 9-17 * * 1-5");
});
test("returns raw cron for wrong field count", () => {
expect(cronToHuman("* * *")).toBe("* * *");
});
});

View File

@@ -0,0 +1,77 @@
import { describe, expect, test } from "bun:test";
import { adjustHunkLineNumbers, getPatchFromContents } from "../diff";
describe("adjustHunkLineNumbers", () => {
test("shifts hunk line numbers by offset", () => {
const hunks = [
{ oldStart: 1, oldLines: 3, newStart: 1, newLines: 4, lines: [" a", "-b", "+c", "+d", " e"] },
] as any[];
const result = adjustHunkLineNumbers(hunks, 10);
expect(result[0].oldStart).toBe(11);
expect(result[0].newStart).toBe(11);
});
test("returns original hunks for zero offset", () => {
const hunks = [
{ oldStart: 5, oldLines: 2, newStart: 5, newLines: 2, lines: [] },
] as any[];
const result = adjustHunkLineNumbers(hunks, 0);
expect(result).toBe(hunks); // same reference
});
test("handles negative offset", () => {
const hunks = [
{ oldStart: 10, oldLines: 2, newStart: 10, newLines: 2, lines: [] },
] as any[];
const result = adjustHunkLineNumbers(hunks, -5);
expect(result[0].oldStart).toBe(5);
expect(result[0].newStart).toBe(5);
});
test("handles empty hunks array", () => {
expect(adjustHunkLineNumbers([], 10)).toEqual([]);
});
});
describe("getPatchFromContents", () => {
test("returns hunks for different content", () => {
const hunks = getPatchFromContents({
filePath: "test.txt",
oldContent: "hello\nworld",
newContent: "hello\nplanet",
});
expect(hunks.length).toBeGreaterThan(0);
expect(hunks[0].lines.some((l: string) => l.startsWith("-"))).toBe(true);
expect(hunks[0].lines.some((l: string) => l.startsWith("+"))).toBe(true);
});
test("returns empty hunks for identical content", () => {
const hunks = getPatchFromContents({
filePath: "test.txt",
oldContent: "same content",
newContent: "same content",
});
expect(hunks).toEqual([]);
});
test("handles content with ampersands", () => {
const hunks = getPatchFromContents({
filePath: "test.txt",
oldContent: "a & b",
newContent: "a & c",
});
expect(hunks.length).toBeGreaterThan(0);
// Verify ampersands are unescaped in the output
const allLines = hunks.flatMap((h: any) => h.lines);
expect(allLines.some((l: string) => l.includes("&"))).toBe(true);
});
test("handles empty old content (new file)", () => {
const hunks = getPatchFromContents({
filePath: "test.txt",
oldContent: "",
newContent: "new content",
});
expect(hunks.length).toBeGreaterThan(0);
});
});

View File

@@ -0,0 +1,95 @@
import { describe, expect, test } from "bun:test";
import {
convertLeadingTabsToSpaces,
addLineNumbers,
stripLineNumberPrefix,
pathsEqual,
normalizePathForComparison,
} from "../file";
describe("convertLeadingTabsToSpaces", () => {
test("converts leading tabs to 2 spaces each", () => {
expect(convertLeadingTabsToSpaces("\t\thello")).toBe(" hello");
});
test("only converts leading tabs", () => {
expect(convertLeadingTabsToSpaces("\thello\tworld")).toBe(" hello\tworld");
});
test("returns unchanged if no tabs", () => {
expect(convertLeadingTabsToSpaces("no tabs")).toBe("no tabs");
});
test("handles empty string", () => {
expect(convertLeadingTabsToSpaces("")).toBe("");
});
test("handles multiline content", () => {
const input = "\tline1\n\t\tline2\nline3";
const expected = " line1\n line2\nline3";
expect(convertLeadingTabsToSpaces(input)).toBe(expected);
});
});
describe("addLineNumbers", () => {
test("adds line numbers starting from 1", () => {
const result = addLineNumbers({ content: "a\nb\nc", startLine: 1 });
expect(result).toContain("1");
expect(result).toContain("a");
expect(result).toContain("b");
expect(result).toContain("c");
});
test("returns empty string for empty content", () => {
expect(addLineNumbers({ content: "", startLine: 1 })).toBe("");
});
test("respects startLine offset", () => {
const result = addLineNumbers({ content: "hello", startLine: 10 });
expect(result).toContain("10");
});
});
describe("stripLineNumberPrefix", () => {
test("strips arrow-separated prefix", () => {
expect(stripLineNumberPrefix(" 1→content")).toBe("content");
});
test("strips tab-separated prefix", () => {
expect(stripLineNumberPrefix("1\tcontent")).toBe("content");
});
test("returns line unchanged if no prefix", () => {
expect(stripLineNumberPrefix("no prefix")).toBe("no prefix");
});
test("handles large line numbers", () => {
expect(stripLineNumberPrefix("123456→content")).toBe("content");
});
});
describe("normalizePathForComparison", () => {
test("normalizes redundant separators", () => {
const result = normalizePathForComparison("/a//b/c");
expect(result).toBe("/a/b/c");
});
test("resolves dot segments", () => {
const result = normalizePathForComparison("/a/./b/../c");
expect(result).toBe("/a/c");
});
});
describe("pathsEqual", () => {
test("returns true for identical paths", () => {
expect(pathsEqual("/a/b/c", "/a/b/c")).toBe(true);
});
test("returns true for equivalent paths with dot segments", () => {
expect(pathsEqual("/a/./b", "/a/b")).toBe(true);
});
test("returns false for different paths", () => {
expect(pathsEqual("/a/b", "/a/c")).toBe(false);
});
});

View File

@@ -0,0 +1,133 @@
import { describe, expect, test } from "bun:test";
import {
formatFileSize,
formatSecondsShort,
formatDuration,
formatNumber,
formatTokens,
formatRelativeTime,
} from "../format";
describe("formatFileSize", () => {
test("formats bytes", () => {
expect(formatFileSize(500)).toBe("500 bytes");
});
test("formats kilobytes", () => {
expect(formatFileSize(1536)).toBe("1.5KB");
});
test("formats megabytes", () => {
expect(formatFileSize(1.5 * 1024 * 1024)).toBe("1.5MB");
});
test("formats gigabytes", () => {
expect(formatFileSize(2 * 1024 * 1024 * 1024)).toBe("2GB");
});
test("removes trailing .0", () => {
expect(formatFileSize(1024)).toBe("1KB");
});
});
describe("formatSecondsShort", () => {
test("formats milliseconds to seconds", () => {
expect(formatSecondsShort(1234)).toBe("1.2s");
});
test("formats zero", () => {
expect(formatSecondsShort(0)).toBe("0.0s");
});
test("formats sub-second", () => {
expect(formatSecondsShort(500)).toBe("0.5s");
});
});
describe("formatDuration", () => {
test("formats 0 as 0s", () => {
expect(formatDuration(0)).toBe("0s");
});
test("formats seconds", () => {
expect(formatDuration(5000)).toBe("5s");
});
test("formats minutes and seconds", () => {
expect(formatDuration(125000)).toBe("2m 5s");
});
test("formats hours", () => {
expect(formatDuration(3661000)).toBe("1h 1m 1s");
});
test("formats days", () => {
expect(formatDuration(90000000)).toBe("1d 1h 0m");
});
test("hideTrailingZeros removes zero components", () => {
expect(formatDuration(3600000, { hideTrailingZeros: true })).toBe("1h");
expect(formatDuration(60000, { hideTrailingZeros: true })).toBe("1m");
});
test("mostSignificantOnly returns largest unit", () => {
expect(formatDuration(90000000, { mostSignificantOnly: true })).toBe("1d");
expect(formatDuration(3661000, { mostSignificantOnly: true })).toBe("1h");
});
});
describe("formatNumber", () => {
test("formats small numbers as-is", () => {
expect(formatNumber(900)).toBe("900");
});
test("formats thousands with k suffix", () => {
const result = formatNumber(1321);
expect(result).toContain("k");
});
test("formats millions", () => {
const result = formatNumber(1500000);
expect(result).toContain("m");
});
});
describe("formatTokens", () => {
test("removes .0 from formatted number", () => {
const result = formatTokens(1000);
expect(result).not.toContain(".0");
});
test("formats small numbers", () => {
expect(formatTokens(500)).toBe("500");
});
});
describe("formatRelativeTime", () => {
const now = new Date("2026-01-15T12:00:00Z");
test("formats seconds ago", () => {
const date = new Date("2026-01-15T11:59:30Z");
const result = formatRelativeTime(date, { now });
expect(result).toContain("30");
expect(result).toContain("ago");
});
test("formats minutes ago", () => {
const date = new Date("2026-01-15T11:55:00Z");
const result = formatRelativeTime(date, { now });
expect(result).toContain("5");
expect(result).toContain("ago");
});
test("formats future time", () => {
const date = new Date("2026-01-15T13:00:00Z");
const result = formatRelativeTime(date, { now });
expect(result).toContain("in");
});
test("handles zero difference", () => {
const result = formatRelativeTime(now, { now });
expect(result).toContain("0");
});
});

View File

@@ -0,0 +1,164 @@
import { describe, expect, test } from "bun:test";
import {
parseFrontmatter,
splitPathInFrontmatter,
parsePositiveIntFromFrontmatter,
parseBooleanFrontmatter,
parseShellFrontmatter,
} from "../frontmatterParser";
describe("parseFrontmatter", () => {
test("parses valid frontmatter", () => {
const md = `---
description: A test
type: user
---
Content here`;
const result = parseFrontmatter(md);
expect(result.frontmatter.description).toBe("A test");
expect(result.frontmatter.type).toBe("user");
expect(result.content).toBe("Content here");
});
test("returns empty frontmatter when none exists", () => {
const md = "Just content, no frontmatter";
const result = parseFrontmatter(md);
expect(result.frontmatter).toEqual({});
expect(result.content).toBe(md);
});
test("handles empty frontmatter block", () => {
const md = `---
---
Content`;
const result = parseFrontmatter(md);
expect(result.frontmatter).toEqual({});
expect(result.content).toBe("Content");
});
test("handles frontmatter with list values", () => {
const md = `---
allowed-tools:
- Bash
- Read
---
Content`;
const result = parseFrontmatter(md);
expect(result.frontmatter["allowed-tools"]).toEqual(["Bash", "Read"]);
});
});
describe("splitPathInFrontmatter", () => {
test("splits comma-separated paths", () => {
expect(splitPathInFrontmatter("a, b, c")).toEqual(["a", "b", "c"]);
});
test("expands brace patterns", () => {
expect(splitPathInFrontmatter("src/*.{ts,tsx}")).toEqual([
"src/*.ts",
"src/*.tsx",
]);
});
test("handles nested brace expansion", () => {
expect(splitPathInFrontmatter("{a,b}/{c,d}")).toEqual([
"a/c", "a/d", "b/c", "b/d",
]);
});
test("handles array input", () => {
expect(splitPathInFrontmatter(["a", "b"])).toEqual(["a", "b"]);
});
test("returns empty array for non-string", () => {
expect(splitPathInFrontmatter(123 as any)).toEqual([]);
});
test("preserves braces in comma-separated list", () => {
expect(splitPathInFrontmatter("a, src/*.{ts,tsx}")).toEqual([
"a",
"src/*.ts",
"src/*.tsx",
]);
});
});
describe("parsePositiveIntFromFrontmatter", () => {
test("returns number for positive integer", () => {
expect(parsePositiveIntFromFrontmatter(5)).toBe(5);
});
test("parses string number", () => {
expect(parsePositiveIntFromFrontmatter("10")).toBe(10);
});
test("returns undefined for zero", () => {
expect(parsePositiveIntFromFrontmatter(0)).toBeUndefined();
});
test("returns undefined for negative number", () => {
expect(parsePositiveIntFromFrontmatter(-1)).toBeUndefined();
});
test("returns undefined for float", () => {
expect(parsePositiveIntFromFrontmatter(1.5)).toBeUndefined();
});
test("returns undefined for null/undefined", () => {
expect(parsePositiveIntFromFrontmatter(null)).toBeUndefined();
expect(parsePositiveIntFromFrontmatter(undefined)).toBeUndefined();
});
test("returns undefined for non-numeric string", () => {
expect(parsePositiveIntFromFrontmatter("abc")).toBeUndefined();
});
});
describe("parseBooleanFrontmatter", () => {
test("returns true for boolean true", () => {
expect(parseBooleanFrontmatter(true)).toBe(true);
});
test("returns true for string 'true'", () => {
expect(parseBooleanFrontmatter("true")).toBe(true);
});
test("returns false for boolean false", () => {
expect(parseBooleanFrontmatter(false)).toBe(false);
});
test("returns false for string 'false'", () => {
expect(parseBooleanFrontmatter("false")).toBe(false);
});
test("returns false for null/undefined", () => {
expect(parseBooleanFrontmatter(null)).toBe(false);
expect(parseBooleanFrontmatter(undefined)).toBe(false);
});
});
describe("parseShellFrontmatter", () => {
test("returns bash for 'bash'", () => {
expect(parseShellFrontmatter("bash", "test")).toBe("bash");
});
test("returns powershell for 'powershell'", () => {
expect(parseShellFrontmatter("powershell", "test")).toBe("powershell");
});
test("returns undefined for null", () => {
expect(parseShellFrontmatter(null, "test")).toBeUndefined();
});
test("returns undefined for unrecognized value", () => {
expect(parseShellFrontmatter("zsh", "test")).toBeUndefined();
});
test("is case insensitive", () => {
expect(parseShellFrontmatter("BASH", "test")).toBe("bash");
});
test("returns undefined for empty string", () => {
expect(parseShellFrontmatter("", "test")).toBeUndefined();
});
});

View File

@@ -0,0 +1,124 @@
import { describe, expect, test } from "bun:test";
import { normalizeGitRemoteUrl } from "../git";
describe("normalizeGitRemoteUrl", () => {
describe("SSH format (git@host:owner/repo)", () => {
test("normalizes basic SSH URL", () => {
expect(normalizeGitRemoteUrl("git@github.com:owner/repo.git")).toBe(
"github.com/owner/repo"
);
});
test("handles SSH URL without .git suffix", () => {
expect(normalizeGitRemoteUrl("git@github.com:owner/repo")).toBe(
"github.com/owner/repo"
);
});
test("handles nested paths", () => {
expect(normalizeGitRemoteUrl("git@gitlab.com:group/sub/repo.git")).toBe(
"gitlab.com/group/sub/repo"
);
});
});
describe("HTTPS format", () => {
test("normalizes basic HTTPS URL", () => {
expect(
normalizeGitRemoteUrl("https://github.com/owner/repo.git")
).toBe("github.com/owner/repo");
});
test("handles HTTPS without .git suffix", () => {
expect(normalizeGitRemoteUrl("https://github.com/owner/repo")).toBe(
"github.com/owner/repo"
);
});
test("handles HTTP URL", () => {
expect(normalizeGitRemoteUrl("http://github.com/owner/repo.git")).toBe(
"github.com/owner/repo"
);
});
test("handles HTTPS with auth", () => {
expect(
normalizeGitRemoteUrl("https://user@github.com/owner/repo.git")
).toBe("github.com/owner/repo");
});
});
describe("ssh:// format", () => {
test("normalizes ssh:// URL", () => {
expect(
normalizeGitRemoteUrl("ssh://git@github.com/owner/repo")
).toBe("github.com/owner/repo");
});
test("handles ssh:// with .git suffix", () => {
expect(
normalizeGitRemoteUrl("ssh://git@github.com/owner/repo.git")
).toBe("github.com/owner/repo");
});
});
describe("CCR proxy URLs", () => {
test("handles legacy proxy format (assumes github.com)", () => {
expect(
normalizeGitRemoteUrl(
"http://local_proxy@127.0.0.1:16583/git/owner/repo"
)
).toBe("github.com/owner/repo");
});
test("handles GHE proxy format (host in path)", () => {
expect(
normalizeGitRemoteUrl(
"http://local_proxy@127.0.0.1:16583/git/ghe.company.com/owner/repo"
)
).toBe("ghe.company.com/owner/repo");
});
test("handles localhost proxy", () => {
expect(
normalizeGitRemoteUrl(
"http://proxy@localhost:8080/git/owner/repo"
)
).toBe("github.com/owner/repo");
});
});
describe("case normalization", () => {
test("converts to lowercase", () => {
expect(normalizeGitRemoteUrl("git@GitHub.COM:Owner/Repo.git")).toBe(
"github.com/owner/repo"
);
});
test("converts HTTPS to lowercase", () => {
expect(
normalizeGitRemoteUrl("https://GitHub.COM/Owner/Repo.git")
).toBe("github.com/owner/repo");
});
});
describe("edge cases", () => {
test("returns null for empty string", () => {
expect(normalizeGitRemoteUrl("")).toBeNull();
});
test("returns null for whitespace only", () => {
expect(normalizeGitRemoteUrl(" ")).toBeNull();
});
test("returns null for unrecognized format", () => {
expect(normalizeGitRemoteUrl("not-a-url")).toBeNull();
});
test("trims whitespace before parsing", () => {
expect(
normalizeGitRemoteUrl(" git@github.com:owner/repo.git ")
).toBe("github.com/owner/repo");
});
});
});

View File

@@ -0,0 +1,40 @@
import { describe, expect, test } from "bun:test";
import { extractGlobBaseDirectory } from "../glob";
describe("extractGlobBaseDirectory", () => {
test("extracts base dir from glob with *", () => {
const result = extractGlobBaseDirectory("src/utils/*.ts");
expect(result.baseDir).toBe("src/utils");
expect(result.relativePattern).toBe("*.ts");
});
test("extracts base dir from glob with **", () => {
const result = extractGlobBaseDirectory("src/**/*.ts");
expect(result.baseDir).toBe("src");
expect(result.relativePattern).toBe("**/*.ts");
});
test("returns dirname for literal path", () => {
const result = extractGlobBaseDirectory("src/utils/file.ts");
expect(result.baseDir).toBe("src/utils");
expect(result.relativePattern).toBe("file.ts");
});
test("handles glob starting with pattern", () => {
const result = extractGlobBaseDirectory("*.ts");
expect(result.baseDir).toBe("");
expect(result.relativePattern).toBe("*.ts");
});
test("handles braces pattern", () => {
const result = extractGlobBaseDirectory("src/{a,b}/*.ts");
expect(result.baseDir).toBe("src");
expect(result.relativePattern).toBe("{a,b}/*.ts");
});
test("handles question mark pattern", () => {
const result = extractGlobBaseDirectory("src/?.ts");
expect(result.baseDir).toBe("src");
expect(result.relativePattern).toBe("?.ts");
});
});

View File

@@ -0,0 +1,57 @@
import { describe, expect, test } from "bun:test";
import { djb2Hash, hashContent, hashPair } from "../hash";
describe("djb2Hash", () => {
test("returns a number", () => {
expect(typeof djb2Hash("hello")).toBe("number");
});
test("returns 0 for empty string", () => {
expect(djb2Hash("")).toBe(0);
});
test("is deterministic", () => {
expect(djb2Hash("test")).toBe(djb2Hash("test"));
});
test("different strings produce different hashes", () => {
expect(djb2Hash("abc")).not.toBe(djb2Hash("def"));
});
test("returns 32-bit integer", () => {
const hash = djb2Hash("some long string to hash");
expect(hash).toBe(hash | 0); // bitwise OR with 0 preserves 32-bit int
});
});
describe("hashContent", () => {
test("returns a string", () => {
expect(typeof hashContent("hello")).toBe("string");
});
test("is deterministic", () => {
expect(hashContent("test")).toBe(hashContent("test"));
});
test("different strings produce different hashes", () => {
expect(hashContent("abc")).not.toBe(hashContent("def"));
});
});
describe("hashPair", () => {
test("returns a string", () => {
expect(typeof hashPair("a", "b")).toBe("string");
});
test("is deterministic", () => {
expect(hashPair("a", "b")).toBe(hashPair("a", "b"));
});
test("order matters", () => {
expect(hashPair("a", "b")).not.toBe(hashPair("b", "a"));
});
test("disambiguates different splits", () => {
expect(hashPair("ts", "code")).not.toBe(hashPair("tsc", "ode"));
});
});

View 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"]);
});
});

View File

@@ -0,0 +1,473 @@
import { describe, expect, test } from "bun:test";
import {
deriveShortMessageId,
INTERRUPT_MESSAGE,
INTERRUPT_MESSAGE_FOR_TOOL_USE,
CANCEL_MESSAGE,
REJECT_MESSAGE,
NO_RESPONSE_REQUESTED,
SYNTHETIC_MESSAGES,
isSyntheticMessage,
getLastAssistantMessage,
hasToolCallsInLastAssistantTurn,
createAssistantMessage,
createAssistantAPIErrorMessage,
createUserMessage,
createUserInterruptionMessage,
prepareUserContent,
createToolResultStopMessage,
extractTag,
isNotEmptyMessage,
deriveUUID,
normalizeMessages,
isClassifierDenial,
buildYoloRejectionMessage,
buildClassifierUnavailableMessage,
AUTO_REJECT_MESSAGE,
DONT_ASK_REJECT_MESSAGE,
SYNTHETIC_MODEL,
} from "../messages";
import type { Message, AssistantMessage, UserMessage } from "../../types/message";
// ─── Helpers ─────────────────────────────────────────────────────────────
function makeAssistantMsg(
contentBlocks: Array<{ type: string; text?: string; [key: string]: any }>
): AssistantMessage {
return createAssistantMessage({
content: contentBlocks as any,
});
}
function makeUserMsg(text: string): UserMessage {
return createUserMessage({ content: text });
}
// ─── deriveShortMessageId ───────────────────────────────────────────────
describe("deriveShortMessageId", () => {
test("returns 6-char string", () => {
const id = deriveShortMessageId("550e8400-e29b-41d4-a716-446655440000");
expect(id).toHaveLength(6);
});
test("is deterministic for same input", () => {
const uuid = "a0b1c2d3-e4f5-6789-abcd-ef0123456789";
expect(deriveShortMessageId(uuid)).toBe(deriveShortMessageId(uuid));
});
test("produces different IDs for different UUIDs", () => {
const id1 = deriveShortMessageId("00000000-0000-0000-0000-000000000001");
const id2 = deriveShortMessageId("ffffffff-ffff-ffff-ffff-ffffffffffff");
expect(id1).not.toBe(id2);
});
});
// ─── Constants ──────────────────────────────────────────────────────────
describe("message constants", () => {
test("SYNTHETIC_MESSAGES contains expected messages", () => {
expect(SYNTHETIC_MESSAGES.has(INTERRUPT_MESSAGE)).toBe(true);
expect(SYNTHETIC_MESSAGES.has(INTERRUPT_MESSAGE_FOR_TOOL_USE)).toBe(true);
expect(SYNTHETIC_MESSAGES.has(CANCEL_MESSAGE)).toBe(true);
expect(SYNTHETIC_MESSAGES.has(REJECT_MESSAGE)).toBe(true);
expect(SYNTHETIC_MESSAGES.has(NO_RESPONSE_REQUESTED)).toBe(true);
});
test("SYNTHETIC_MODEL is <synthetic>", () => {
expect(SYNTHETIC_MODEL).toBe("<synthetic>");
});
});
// ─── Message factories ──────────────────────────────────────────────────
describe("createAssistantMessage", () => {
test("creates assistant message with string content", () => {
const msg = createAssistantMessage({ content: "hello" });
expect(msg.type).toBe("assistant");
expect(msg.message.role).toBe("assistant");
expect(msg.message.content).toHaveLength(1);
expect((msg.message.content[0] as any).text).toBe("hello");
});
test("creates assistant message with content blocks", () => {
const blocks = [{ type: "text" as const, text: "hello" }];
const msg = createAssistantMessage({ content: blocks as any });
expect(msg.type).toBe("assistant");
expect(msg.message.content).toHaveLength(1);
});
test("generates unique uuid per call", () => {
const msg1 = createAssistantMessage({ content: "a" });
const msg2 = createAssistantMessage({ content: "b" });
expect(msg1.uuid).not.toBe(msg2.uuid);
});
test("has isApiErrorMessage false", () => {
const msg = createAssistantMessage({ content: "test" });
expect(msg.isApiErrorMessage).toBe(false);
});
});
describe("createAssistantAPIErrorMessage", () => {
test("sets isApiErrorMessage to true", () => {
const msg = createAssistantAPIErrorMessage({ content: "error" });
expect(msg.isApiErrorMessage).toBe(true);
});
test("includes error details", () => {
const msg = createAssistantAPIErrorMessage({
content: "fail",
errorDetails: "rate limited",
});
expect(msg.errorDetails).toBe("rate limited");
});
});
describe("createUserMessage", () => {
test("creates user message with string content", () => {
const msg = createUserMessage({ content: "hello" });
expect(msg.type).toBe("user");
expect(msg.message.role).toBe("user");
expect(msg.message.content).toBe("hello");
});
test("generates unique uuid", () => {
const msg1 = createUserMessage({ content: "a" });
const msg2 = createUserMessage({ content: "b" });
expect(msg1.uuid).not.toBe(msg2.uuid);
});
test("uses provided uuid when given", () => {
const msg = createUserMessage({
content: "test",
uuid: "custom-uuid-1234-5678-abcd-ef0123456789",
});
expect(msg.uuid).toBe("custom-uuid-1234-5678-abcd-ef0123456789");
});
test("sets isMeta flag", () => {
const msg = createUserMessage({ content: "test", isMeta: true });
expect(msg.isMeta).toBe(true);
});
});
describe("createUserInterruptionMessage", () => {
test("creates interrupt message without tool use", () => {
const msg = createUserInterruptionMessage({});
expect(msg.type).toBe("user");
expect((msg.message.content as any)[0].text).toBe(INTERRUPT_MESSAGE);
});
test("creates interrupt message with tool use", () => {
const msg = createUserInterruptionMessage({ toolUse: true });
expect((msg.message.content as any)[0].text).toBe(
INTERRUPT_MESSAGE_FOR_TOOL_USE
);
});
});
describe("prepareUserContent", () => {
test("returns string when no preceding blocks", () => {
const result = prepareUserContent({
inputString: "hello",
precedingInputBlocks: [],
});
expect(result).toBe("hello");
});
test("returns array when preceding blocks exist", () => {
const blocks = [{ type: "image" as const, source: {} } as any];
const result = prepareUserContent({
inputString: "describe this",
precedingInputBlocks: blocks,
});
expect(Array.isArray(result)).toBe(true);
expect((result as any[]).length).toBe(2);
expect((result as any[])[1].text).toBe("describe this");
});
});
describe("createToolResultStopMessage", () => {
test("creates tool result with error flag", () => {
const result = createToolResultStopMessage("tool-123");
expect(result.type).toBe("tool_result");
expect(result.is_error).toBe(true);
expect(result.tool_use_id).toBe("tool-123");
expect(result.content).toBe(CANCEL_MESSAGE);
});
});
// ─── isSyntheticMessage ─────────────────────────────────────────────────
describe("isSyntheticMessage", () => {
test("identifies interrupt message as synthetic", () => {
const msg: any = {
type: "user",
message: { content: [{ type: "text", text: INTERRUPT_MESSAGE }] },
};
expect(isSyntheticMessage(msg)).toBe(true);
});
test("identifies cancel message as synthetic", () => {
const msg: any = {
type: "user",
message: { content: [{ type: "text", text: CANCEL_MESSAGE }] },
};
expect(isSyntheticMessage(msg)).toBe(true);
});
test("returns false for normal user message", () => {
const msg: any = {
type: "user",
message: { content: [{ type: "text", text: "hello" }] },
};
expect(isSyntheticMessage(msg)).toBe(false);
});
test("returns false for progress message", () => {
const msg: any = {
type: "progress",
message: { content: [{ type: "text", text: INTERRUPT_MESSAGE }] },
};
expect(isSyntheticMessage(msg)).toBe(false);
});
test("returns false for string content", () => {
const msg: any = {
type: "user",
message: { content: INTERRUPT_MESSAGE },
};
expect(isSyntheticMessage(msg)).toBe(false);
});
});
// ─── getLastAssistantMessage ────────────────────────────────────────────
describe("getLastAssistantMessage", () => {
test("returns last assistant message", () => {
const a1 = makeAssistantMsg([{ type: "text", text: "first" }]);
const u = makeUserMsg("mid");
const a2 = makeAssistantMsg([{ type: "text", text: "last" }]);
const result = getLastAssistantMessage([a1, u, a2]);
expect(result).toBe(a2);
});
test("returns undefined for empty array", () => {
expect(getLastAssistantMessage([])).toBeUndefined();
});
test("returns undefined when no assistant messages", () => {
const u = makeUserMsg("hello");
expect(getLastAssistantMessage([u])).toBeUndefined();
});
});
// ─── hasToolCallsInLastAssistantTurn ────────────────────────────────────
describe("hasToolCallsInLastAssistantTurn", () => {
test("returns true when last assistant has tool_use", () => {
const msg = makeAssistantMsg([
{ type: "text", text: "let me check" },
{ type: "tool_use", id: "t1", name: "Bash", input: {} },
]);
expect(hasToolCallsInLastAssistantTurn([msg])).toBe(true);
});
test("returns false when last assistant has only text", () => {
const msg = makeAssistantMsg([{ type: "text", text: "done" }]);
expect(hasToolCallsInLastAssistantTurn([msg])).toBe(false);
});
test("returns false for empty messages", () => {
expect(hasToolCallsInLastAssistantTurn([])).toBe(false);
});
});
// ─── extractTag ─────────────────────────────────────────────────────────
describe("extractTag", () => {
test("extracts simple tag content", () => {
expect(extractTag("<foo>bar</foo>", "foo")).toBe("bar");
});
test("extracts tag with attributes", () => {
expect(extractTag('<foo class="a">bar</foo>', "foo")).toBe("bar");
});
test("handles multiline content", () => {
expect(extractTag("<foo>\nline1\nline2\n</foo>", "foo")).toBe(
"\nline1\nline2\n"
);
});
test("returns null for missing tag", () => {
expect(extractTag("<foo>bar</foo>", "baz")).toBeNull();
});
test("returns null for empty html", () => {
expect(extractTag("", "foo")).toBeNull();
});
test("returns null for empty tagName", () => {
expect(extractTag("<foo>bar</foo>", "")).toBeNull();
});
test("is case-insensitive", () => {
expect(extractTag("<FOO>bar</FOO>", "foo")).toBe("bar");
});
});
// ─── isNotEmptyMessage ──────────────────────────────────────────────────
describe("isNotEmptyMessage", () => {
test("returns true for message with text content", () => {
const msg: any = {
type: "user",
message: { content: "hello" },
};
expect(isNotEmptyMessage(msg)).toBe(true);
});
test("returns false for empty string content", () => {
const msg: any = {
type: "user",
message: { content: " " },
};
expect(isNotEmptyMessage(msg)).toBe(false);
});
test("returns false for empty content array", () => {
const msg: any = {
type: "user",
message: { content: [] },
};
expect(isNotEmptyMessage(msg)).toBe(false);
});
test("returns true for progress message", () => {
const msg: any = {
type: "progress",
message: { content: [] },
};
expect(isNotEmptyMessage(msg)).toBe(true);
});
test("returns true for multi-block content", () => {
const msg: any = {
type: "user",
message: {
content: [
{ type: "text", text: "a" },
{ type: "text", text: "b" },
],
},
};
expect(isNotEmptyMessage(msg)).toBe(true);
});
test("returns true for non-text block", () => {
const msg: any = {
type: "user",
message: {
content: [{ type: "tool_use", id: "t1", name: "Bash", input: {} }],
},
};
expect(isNotEmptyMessage(msg)).toBe(true);
});
});
// ─── deriveUUID ─────────────────────────────────────────────────────────
describe("deriveUUID", () => {
test("produces deterministic output", () => {
const parent = "550e8400-e29b-41d4-a716-446655440000" as any;
expect(deriveUUID(parent, 0)).toBe(deriveUUID(parent, 0));
});
test("produces different output for different indices", () => {
const parent = "550e8400-e29b-41d4-a716-446655440000" as any;
expect(deriveUUID(parent, 0)).not.toBe(deriveUUID(parent, 1));
});
test("preserves UUID-like length", () => {
const parent = "550e8400-e29b-41d4-a716-446655440000" as any;
const derived = deriveUUID(parent, 5);
expect(derived.length).toBe(parent.length);
});
});
// ─── isClassifierDenial ─────────────────────────────────────────────────
describe("isClassifierDenial", () => {
test("returns true for classifier denial prefix", () => {
expect(
isClassifierDenial(
"Permission for this action has been denied. Reason: unsafe"
)
).toBe(true);
});
test("returns false for normal content", () => {
expect(isClassifierDenial("hello world")).toBe(false);
});
});
// ─── Message builder functions ──────────────────────────────────────────
describe("AUTO_REJECT_MESSAGE", () => {
test("includes tool name", () => {
const msg = AUTO_REJECT_MESSAGE("Bash");
expect(msg).toContain("Bash");
expect(msg).toContain("denied");
});
});
describe("DONT_ASK_REJECT_MESSAGE", () => {
test("includes tool name and dont ask mode", () => {
const msg = DONT_ASK_REJECT_MESSAGE("Write");
expect(msg).toContain("Write");
expect(msg).toContain("don't ask mode");
});
});
describe("buildYoloRejectionMessage", () => {
test("includes reason", () => {
const msg = buildYoloRejectionMessage("potentially destructive");
expect(msg).toContain("potentially destructive");
expect(msg).toContain("denied");
});
});
describe("buildClassifierUnavailableMessage", () => {
test("includes tool name and model", () => {
const msg = buildClassifierUnavailableMessage("Bash", "classifier-v1");
expect(msg).toContain("Bash");
expect(msg).toContain("classifier-v1");
expect(msg).toContain("unavailable");
});
});
// ─── normalizeMessages ──────────────────────────────────────────────────
describe("normalizeMessages", () => {
test("splits multi-block assistant message into individual messages", () => {
const msg = makeAssistantMsg([
{ type: "text", text: "first" },
{ type: "text", text: "second" },
]);
const normalized = normalizeMessages([msg]);
expect(normalized.length).toBe(2);
});
test("handles empty array", () => {
const result = normalizeMessages([] as AssistantMessage[]);
expect(result).toEqual([]);
});
test("preserves single-block message", () => {
const msg = makeAssistantMsg([{ type: "text", text: "hello" }]);
const normalized = normalizeMessages([msg]);
expect(normalized.length).toBe(1);
});
});

View 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");
});
});

View File

@@ -0,0 +1,98 @@
import { describe, expect, test } from "bun:test";
import { gt, gte, lt, lte, satisfies, order } from "../semver";
describe("gt", () => {
test("returns true when a > b", () => {
expect(gt("2.0.0", "1.0.0")).toBe(true);
});
test("returns false when a < b", () => {
expect(gt("1.0.0", "2.0.0")).toBe(false);
});
test("returns false when equal", () => {
expect(gt("1.0.0", "1.0.0")).toBe(false);
});
});
describe("gte", () => {
test("returns true when a > b", () => {
expect(gte("2.0.0", "1.0.0")).toBe(true);
});
test("returns true when equal", () => {
expect(gte("1.0.0", "1.0.0")).toBe(true);
});
test("returns false when a < b", () => {
expect(gte("1.0.0", "2.0.0")).toBe(false);
});
});
describe("lt", () => {
test("returns true when a < b", () => {
expect(lt("1.0.0", "2.0.0")).toBe(true);
});
test("returns false when a > b", () => {
expect(lt("2.0.0", "1.0.0")).toBe(false);
});
test("returns false when equal", () => {
expect(lt("1.0.0", "1.0.0")).toBe(false);
});
});
describe("lte", () => {
test("returns true when a < b", () => {
expect(lte("1.0.0", "2.0.0")).toBe(true);
});
test("returns true when equal", () => {
expect(lte("1.0.0", "1.0.0")).toBe(true);
});
test("returns false when a > b", () => {
expect(lte("2.0.0", "1.0.0")).toBe(false);
});
});
describe("satisfies", () => {
test("matches exact version", () => {
expect(satisfies("1.2.3", "1.2.3")).toBe(true);
});
test("matches range", () => {
expect(satisfies("1.2.3", ">=1.0.0")).toBe(true);
});
test("does not match out-of-range version", () => {
expect(satisfies("0.9.0", ">=1.0.0")).toBe(false);
});
test("matches caret range", () => {
expect(satisfies("1.2.3", "^1.0.0")).toBe(true);
});
test("does not match major bump in caret", () => {
expect(satisfies("2.0.0", "^1.0.0")).toBe(false);
});
});
describe("order", () => {
test("returns 1 when a > b", () => {
expect(order("2.0.0", "1.0.0")).toBe(1);
});
test("returns -1 when a < b", () => {
expect(order("1.0.0", "2.0.0")).toBe(-1);
});
test("returns 0 when equal", () => {
expect(order("1.0.0", "1.0.0")).toBe(0);
});
test("compares patch versions", () => {
expect(order("1.0.1", "1.0.0")).toBe(1);
});
});

View File

@@ -0,0 +1,195 @@
import { describe, expect, test } from "bun:test";
import {
escapeRegExp,
capitalize,
plural,
firstLineOf,
countCharInString,
normalizeFullWidthDigits,
normalizeFullWidthSpace,
safeJoinLines,
EndTruncatingAccumulator,
truncateToLines,
} from "../stringUtils";
describe("escapeRegExp", () => {
test("escapes special regex chars", () => {
expect(escapeRegExp("a.b*c?d")).toBe("a\\.b\\*c\\?d");
});
test("escapes brackets and parens", () => {
expect(escapeRegExp("[foo](bar)")).toBe("\\[foo\\]\\(bar\\)");
});
test("escapes all special chars", () => {
expect(escapeRegExp("^${}()|[]\\.*+?")).toBe(
"\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\\\.\\*\\+\\?"
);
});
test("returns normal string unchanged", () => {
expect(escapeRegExp("hello")).toBe("hello");
});
});
describe("capitalize", () => {
test("uppercases first char", () => {
expect(capitalize("hello")).toBe("Hello");
});
test("does NOT lowercase rest", () => {
expect(capitalize("fooBar")).toBe("FooBar");
});
test("handles single char", () => {
expect(capitalize("a")).toBe("A");
});
test("handles empty string", () => {
expect(capitalize("")).toBe("");
});
});
describe("plural", () => {
test("returns singular for 1", () => {
expect(plural(1, "file")).toBe("file");
});
test("returns plural for 0", () => {
expect(plural(0, "file")).toBe("files");
});
test("returns plural for many", () => {
expect(plural(3, "file")).toBe("files");
});
test("uses custom plural form", () => {
expect(plural(2, "entry", "entries")).toBe("entries");
});
});
describe("firstLineOf", () => {
test("returns first line of multiline string", () => {
expect(firstLineOf("line1\nline2\nline3")).toBe("line1");
});
test("returns whole string if no newline", () => {
expect(firstLineOf("single line")).toBe("single line");
});
test("returns empty string for leading newline", () => {
expect(firstLineOf("\nline2")).toBe("");
});
});
describe("countCharInString", () => {
test("counts occurrences of a character", () => {
expect(countCharInString("hello world", "l")).toBe(3);
});
test("returns 0 for no match", () => {
expect(countCharInString("hello", "z")).toBe(0);
});
test("counts from start offset", () => {
expect(countCharInString("aabaa", "a", 2)).toBe(2);
});
test("returns 0 for empty string", () => {
expect(countCharInString("", "a")).toBe(0);
});
});
describe("normalizeFullWidthDigits", () => {
test("converts full-width digits to half-width", () => {
expect(normalizeFullWidthDigits("")).toBe("0123456789");
});
test("leaves half-width digits unchanged", () => {
expect(normalizeFullWidthDigits("0123")).toBe("0123");
});
test("handles mixed content", () => {
expect(normalizeFullWidthDigits("test")).toBe("test123");
});
});
describe("normalizeFullWidthSpace", () => {
test("converts full-width space to half-width", () => {
expect(normalizeFullWidthSpace("a\u3000b")).toBe("a b");
});
test("leaves normal spaces unchanged", () => {
expect(normalizeFullWidthSpace("a b")).toBe("a b");
});
});
describe("safeJoinLines", () => {
test("joins lines with delimiter", () => {
expect(safeJoinLines(["a", "b", "c"], ",")).toBe("a,b,c");
});
test("truncates when exceeding maxSize", () => {
const result = safeJoinLines(["hello", "world", "foo"], ",", 12);
expect(result.length).toBeLessThanOrEqual(12 + "...[truncated]".length);
expect(result).toContain("...[truncated]");
});
test("returns empty string for empty input", () => {
expect(safeJoinLines([])).toBe("");
});
});
describe("EndTruncatingAccumulator", () => {
test("accumulates text", () => {
const acc = new EndTruncatingAccumulator(100);
acc.append("hello ");
acc.append("world");
expect(acc.toString()).toBe("hello world");
});
test("truncates when exceeding maxSize", () => {
const acc = new EndTruncatingAccumulator(10);
acc.append("12345678901234567890");
expect(acc.truncated).toBe(true);
expect(acc.length).toBe(10);
});
test("reports total bytes received", () => {
const acc = new EndTruncatingAccumulator(5);
acc.append("1234567890");
expect(acc.totalBytes).toBe(10);
});
test("clear resets state", () => {
const acc = new EndTruncatingAccumulator(100);
acc.append("hello");
acc.clear();
expect(acc.toString()).toBe("");
expect(acc.length).toBe(0);
expect(acc.truncated).toBe(false);
});
test("stops accepting data once truncated and full", () => {
const acc = new EndTruncatingAccumulator(5);
acc.append("12345");
acc.append("67890");
expect(acc.length).toBe(5);
acc.append("more");
expect(acc.length).toBe(5);
});
});
describe("truncateToLines", () => {
test("returns text unchanged if within limit", () => {
expect(truncateToLines("a\nb\nc", 5)).toBe("a\nb\nc");
});
test("truncates text exceeding limit", () => {
expect(truncateToLines("a\nb\nc\nd\ne", 3)).toBe("a\nb\nc…");
});
test("handles single line", () => {
expect(truncateToLines("hello", 1)).toBe("hello");
});
});

View File

@@ -0,0 +1,88 @@
import { describe, expect, test } from "bun:test";
import { buildEffectiveSystemPrompt } from "../systemPrompt";
const defaultPrompt = ["You are a helpful assistant.", "Follow instructions."];
function buildPrompt(overrides: Record<string, unknown> = {}) {
return buildEffectiveSystemPrompt({
mainThreadAgentDefinition: undefined,
toolUseContext: { options: {} as any },
customSystemPrompt: undefined,
defaultSystemPrompt: defaultPrompt,
appendSystemPrompt: undefined,
...overrides,
});
}
describe("buildEffectiveSystemPrompt", () => {
test("returns default system prompt when no overrides", () => {
const result = buildPrompt();
expect(Array.from(result)).toEqual(defaultPrompt);
});
test("overrideSystemPrompt replaces everything", () => {
const result = buildPrompt({ overrideSystemPrompt: "override" });
expect(Array.from(result)).toEqual(["override"]);
});
test("customSystemPrompt replaces default", () => {
const result = buildPrompt({ customSystemPrompt: "custom" });
expect(Array.from(result)).toEqual(["custom"]);
});
test("appendSystemPrompt is appended after main prompt", () => {
const result = buildPrompt({ appendSystemPrompt: "appended" });
expect(Array.from(result)).toEqual([...defaultPrompt, "appended"]);
});
test("agent definition replaces default prompt", () => {
const agentDef = {
getSystemPrompt: () => "agent prompt",
agentType: "custom",
} as any;
const result = buildPrompt({ mainThreadAgentDefinition: agentDef });
expect(Array.from(result)).toEqual(["agent prompt"]);
});
test("agent definition with append combines both", () => {
const agentDef = {
getSystemPrompt: () => "agent prompt",
agentType: "custom",
} as any;
const result = buildPrompt({
mainThreadAgentDefinition: agentDef,
appendSystemPrompt: "extra",
});
expect(Array.from(result)).toEqual(["agent prompt", "extra"]);
});
test("override takes precedence over agent and custom", () => {
const agentDef = {
getSystemPrompt: () => "agent prompt",
agentType: "custom",
} as any;
const result = buildPrompt({
mainThreadAgentDefinition: agentDef,
customSystemPrompt: "custom",
appendSystemPrompt: "extra",
overrideSystemPrompt: "override",
});
expect(Array.from(result)).toEqual(["override"]);
});
test("returns array of strings", () => {
const result = buildPrompt();
expect(Array.isArray(result)).toBe(true);
for (const item of result) {
expect(typeof item).toBe("string");
}
});
test("custom + append combines both", () => {
const result = buildPrompt({
customSystemPrompt: "custom",
appendSystemPrompt: "extra",
});
expect(Array.from(result)).toEqual(["custom", "extra"]);
});
});

View 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);
});
});

View 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"]);
});
});

View File

@@ -0,0 +1,34 @@
import { describe, expect, test } from "bun:test";
import { validateUuid } from "../uuid";
describe("validateUuid", () => {
test("validates correct UUID", () => {
const result = validateUuid("550e8400-e29b-41d4-a716-446655440000");
expect(result).toBe("550e8400-e29b-41d4-a716-446655440000");
});
test("validates uppercase UUID", () => {
const result = validateUuid("550E8400-E29B-41D4-A716-446655440000");
expect(result).not.toBeNull();
});
test("returns null for non-string", () => {
expect(validateUuid(123)).toBeNull();
expect(validateUuid(null)).toBeNull();
expect(validateUuid(undefined)).toBeNull();
});
test("returns null for invalid UUID format", () => {
expect(validateUuid("not-a-uuid")).toBeNull();
expect(validateUuid("550e8400-e29b-41d4-a716")).toBeNull();
expect(validateUuid("550e8400e29b41d4a716446655440000")).toBeNull();
});
test("returns null for empty string", () => {
expect(validateUuid("")).toBeNull();
});
test("returns null for UUID with invalid chars", () => {
expect(validateUuid("550e8400-e29b-41d4-a716-44665544000g")).toBeNull();
});
});

View File

@@ -0,0 +1,42 @@
import { describe, expect, test } from "bun:test";
import { escapeXml, escapeXmlAttr } from "../xml";
describe("escapeXml", () => {
test("escapes ampersand", () => {
expect(escapeXml("a & b")).toBe("a &amp; b");
});
test("escapes less-than", () => {
expect(escapeXml("<div>")).toBe("&lt;div&gt;");
});
test("escapes greater-than", () => {
expect(escapeXml("a > b")).toBe("a &gt; b");
});
test("escapes multiple special chars", () => {
expect(escapeXml("<a & b>")).toBe("&lt;a &amp; b&gt;");
});
test("returns empty string unchanged", () => {
expect(escapeXml("")).toBe("");
});
test("returns normal text unchanged", () => {
expect(escapeXml("hello world")).toBe("hello world");
});
});
describe("escapeXmlAttr", () => {
test("escapes double quotes", () => {
expect(escapeXmlAttr('say "hello"')).toBe("say &quot;hello&quot;");
});
test("escapes single quotes", () => {
expect(escapeXmlAttr("it's")).toBe("it&apos;s");
});
test("escapes all special chars", () => {
expect(escapeXmlAttr('<a & "b">')).toBe("&lt;a &amp; &quot;b&quot;&gt;");
});
});

View File

@@ -0,0 +1,70 @@
import { describe, expect, test } from "bun:test";
import { isModelAlias, isModelFamilyAlias } from "../aliases";
describe("isModelAlias", () => {
test('returns true for "sonnet"', () => {
expect(isModelAlias("sonnet")).toBe(true);
});
test('returns true for "opus"', () => {
expect(isModelAlias("opus")).toBe(true);
});
test('returns true for "haiku"', () => {
expect(isModelAlias("haiku")).toBe(true);
});
test('returns true for "best"', () => {
expect(isModelAlias("best")).toBe(true);
});
test('returns true for "sonnet[1m]"', () => {
expect(isModelAlias("sonnet[1m]")).toBe(true);
});
test('returns true for "opus[1m]"', () => {
expect(isModelAlias("opus[1m]")).toBe(true);
});
test('returns true for "opusplan"', () => {
expect(isModelAlias("opusplan")).toBe(true);
});
test("returns false for full model ID", () => {
expect(isModelAlias("claude-sonnet-4-6-20250514")).toBe(false);
});
test("returns false for unknown string", () => {
expect(isModelAlias("gpt-4")).toBe(false);
});
test("is case-sensitive", () => {
expect(isModelAlias("Sonnet")).toBe(false);
});
});
describe("isModelFamilyAlias", () => {
test('returns true for "sonnet"', () => {
expect(isModelFamilyAlias("sonnet")).toBe(true);
});
test('returns true for "opus"', () => {
expect(isModelFamilyAlias("opus")).toBe(true);
});
test('returns true for "haiku"', () => {
expect(isModelFamilyAlias("haiku")).toBe(true);
});
test('returns false for "best"', () => {
expect(isModelFamilyAlias("best")).toBe(false);
});
test('returns false for "opusplan"', () => {
expect(isModelFamilyAlias("opusplan")).toBe(false);
});
test('returns false for "sonnet[1m]"', () => {
expect(isModelFamilyAlias("sonnet[1m]")).toBe(false);
});
});

View File

@@ -0,0 +1,92 @@
import { describe, expect, test } from "bun:test";
import { firstPartyNameToCanonical } from "../model";
describe("firstPartyNameToCanonical", () => {
test("maps opus-4-6 full name to canonical", () => {
expect(firstPartyNameToCanonical("claude-opus-4-6-20250514")).toBe(
"claude-opus-4-6"
);
});
test("maps sonnet-4-6 full name", () => {
expect(firstPartyNameToCanonical("claude-sonnet-4-6-20250514")).toBe(
"claude-sonnet-4-6"
);
});
test("maps haiku-4-5", () => {
expect(firstPartyNameToCanonical("claude-haiku-4-5-20251001")).toBe(
"claude-haiku-4-5"
);
});
test("maps 3P provider format", () => {
expect(
firstPartyNameToCanonical("us.anthropic.claude-opus-4-6-v1:0")
).toBe("claude-opus-4-6");
});
test("maps claude-3-7-sonnet", () => {
expect(firstPartyNameToCanonical("claude-3-7-sonnet-20250219")).toBe(
"claude-3-7-sonnet"
);
});
test("maps claude-3-5-sonnet", () => {
expect(firstPartyNameToCanonical("claude-3-5-sonnet-20241022")).toBe(
"claude-3-5-sonnet"
);
});
test("maps claude-3-5-haiku", () => {
expect(firstPartyNameToCanonical("claude-3-5-haiku-20241022")).toBe(
"claude-3-5-haiku"
);
});
test("maps claude-3-opus", () => {
expect(firstPartyNameToCanonical("claude-3-opus-20240229")).toBe(
"claude-3-opus"
);
});
test("is case insensitive", () => {
expect(firstPartyNameToCanonical("Claude-Opus-4-6-20250514")).toBe(
"claude-opus-4-6"
);
});
test("falls back to input for unknown model", () => {
expect(firstPartyNameToCanonical("unknown-model")).toBe("unknown-model");
});
test("differentiates opus-4 vs opus-4-5 vs opus-4-6", () => {
expect(firstPartyNameToCanonical("claude-opus-4-20240101")).toBe(
"claude-opus-4"
);
expect(firstPartyNameToCanonical("claude-opus-4-5-20240101")).toBe(
"claude-opus-4-5"
);
expect(firstPartyNameToCanonical("claude-opus-4-6-20240101")).toBe(
"claude-opus-4-6"
);
});
test("maps opus-4-1", () => {
expect(firstPartyNameToCanonical("claude-opus-4-1-20240101")).toBe(
"claude-opus-4-1"
);
});
test("maps sonnet-4-5", () => {
expect(firstPartyNameToCanonical("claude-sonnet-4-5-20240101")).toBe(
"claude-sonnet-4-5"
);
});
test("maps sonnet-4", () => {
expect(firstPartyNameToCanonical("claude-sonnet-4-20240101")).toBe(
"claude-sonnet-4"
);
});
});

View File

@@ -0,0 +1,84 @@
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import { getAPIProvider, isFirstPartyAnthropicBaseUrl } from "../providers";
describe("getAPIProvider", () => {
const originalEnv = { ...process.env };
afterEach(() => {
delete process.env.CLAUDE_CODE_USE_BEDROCK;
delete process.env.CLAUDE_CODE_USE_VERTEX;
delete process.env.CLAUDE_CODE_USE_FOUNDRY;
});
test('returns "firstParty" by default', () => {
delete process.env.CLAUDE_CODE_USE_BEDROCK;
delete process.env.CLAUDE_CODE_USE_VERTEX;
delete process.env.CLAUDE_CODE_USE_FOUNDRY;
expect(getAPIProvider()).toBe("firstParty");
});
test('returns "bedrock" when CLAUDE_CODE_USE_BEDROCK is set', () => {
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
expect(getAPIProvider()).toBe("bedrock");
});
test('returns "vertex" when CLAUDE_CODE_USE_VERTEX is set', () => {
process.env.CLAUDE_CODE_USE_VERTEX = "1";
expect(getAPIProvider()).toBe("vertex");
});
test('returns "foundry" when CLAUDE_CODE_USE_FOUNDRY is set', () => {
process.env.CLAUDE_CODE_USE_FOUNDRY = "1";
expect(getAPIProvider()).toBe("foundry");
});
test("bedrock takes precedence over vertex", () => {
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
process.env.CLAUDE_CODE_USE_VERTEX = "1";
expect(getAPIProvider()).toBe("bedrock");
});
});
describe("isFirstPartyAnthropicBaseUrl", () => {
const originalBaseUrl = process.env.ANTHROPIC_BASE_URL;
const originalUserType = process.env.USER_TYPE;
afterEach(() => {
if (originalBaseUrl !== undefined) {
process.env.ANTHROPIC_BASE_URL = originalBaseUrl;
} else {
delete process.env.ANTHROPIC_BASE_URL;
}
if (originalUserType !== undefined) {
process.env.USER_TYPE = originalUserType;
} else {
delete process.env.USER_TYPE;
}
});
test("returns true when ANTHROPIC_BASE_URL is not set", () => {
delete process.env.ANTHROPIC_BASE_URL;
expect(isFirstPartyAnthropicBaseUrl()).toBe(true);
});
test("returns true for api.anthropic.com", () => {
process.env.ANTHROPIC_BASE_URL = "https://api.anthropic.com";
expect(isFirstPartyAnthropicBaseUrl()).toBe(true);
});
test("returns false for custom URL", () => {
process.env.ANTHROPIC_BASE_URL = "https://my-proxy.com";
expect(isFirstPartyAnthropicBaseUrl()).toBe(false);
});
test("returns false for invalid URL", () => {
process.env.ANTHROPIC_BASE_URL = "not-a-url";
expect(isFirstPartyAnthropicBaseUrl()).toBe(false);
});
test("returns true for staging URL when USER_TYPE is ant", () => {
process.env.ANTHROPIC_BASE_URL = "https://api-staging.anthropic.com";
process.env.USER_TYPE = "ant";
expect(isFirstPartyAnthropicBaseUrl()).toBe(true);
});
});

View File

@@ -0,0 +1,152 @@
import { describe, expect, test } from "bun:test";
import {
escapeRuleContent,
unescapeRuleContent,
permissionRuleValueFromString,
permissionRuleValueToString,
normalizeLegacyToolName,
} from "../permissionRuleParser";
describe("escapeRuleContent", () => {
test("escapes backslashes first", () => {
expect(escapeRuleContent("a\\b")).toBe("a\\\\b");
});
test("escapes opening parentheses", () => {
expect(escapeRuleContent("fn(x)")).toBe("fn\\(x\\)");
});
test("escapes backslash before parens correctly", () => {
expect(escapeRuleContent('echo "test\\nvalue"')).toBe(
'echo "test\\\\nvalue"'
);
});
test("returns unchanged string with no special chars", () => {
expect(escapeRuleContent("npm install")).toBe("npm install");
});
test("handles empty string", () => {
expect(escapeRuleContent("")).toBe("");
});
});
describe("unescapeRuleContent", () => {
test("unescapes parentheses", () => {
expect(unescapeRuleContent("fn\\(x\\)")).toBe("fn(x)");
});
test("unescapes backslashes", () => {
expect(unescapeRuleContent("a\\\\b")).toBe("a\\b");
});
test("roundtrips with escapeRuleContent", () => {
const original = 'python -c "print(1)"';
expect(unescapeRuleContent(escapeRuleContent(original))).toBe(original);
});
test("handles content with backslash-paren combo", () => {
const original = 'echo "test\\nvalue"';
expect(unescapeRuleContent(escapeRuleContent(original))).toBe(original);
});
test("returns unchanged string with no escapes", () => {
expect(unescapeRuleContent("npm install")).toBe("npm install");
});
});
describe("permissionRuleValueFromString", () => {
test("parses tool name only", () => {
expect(permissionRuleValueFromString("Bash")).toEqual({
toolName: "Bash",
});
});
test("parses tool name with content", () => {
expect(permissionRuleValueFromString("Bash(npm install)")).toEqual({
toolName: "Bash",
ruleContent: "npm install",
});
});
test("handles escaped parens in content", () => {
const result = permissionRuleValueFromString(
'Bash(python -c "print\\(1\\)")'
);
expect(result.toolName).toBe("Bash");
expect(result.ruleContent).toBe('python -c "print(1)"');
});
test("treats empty content as tool-wide rule", () => {
expect(permissionRuleValueFromString("Bash()")).toEqual({
toolName: "Bash",
});
});
test("treats wildcard content as tool-wide rule", () => {
expect(permissionRuleValueFromString("Bash(*)")).toEqual({
toolName: "Bash",
});
});
test("normalizes legacy tool names", () => {
const result = permissionRuleValueFromString("Task");
expect(result.toolName).toBe("Agent");
});
test("handles MCP-style tool names", () => {
expect(permissionRuleValueFromString("mcp__server__tool")).toEqual({
toolName: "mcp__server__tool",
});
});
});
describe("permissionRuleValueToString", () => {
test("formats tool name only", () => {
expect(permissionRuleValueToString({ toolName: "Bash" })).toBe("Bash");
});
test("formats tool name with content", () => {
expect(
permissionRuleValueToString({
toolName: "Bash",
ruleContent: "npm install",
})
).toBe("Bash(npm install)");
});
test("escapes parens in content", () => {
expect(
permissionRuleValueToString({
toolName: "Bash",
ruleContent: 'python -c "print(1)"',
})
).toBe('Bash(python -c "print\\(1\\)")');
});
test("roundtrips with permissionRuleValueFromString", () => {
const original = { toolName: "Bash", ruleContent: "npm install" };
const str = permissionRuleValueToString(original);
const parsed = permissionRuleValueFromString(str);
expect(parsed).toEqual(original);
});
});
describe("normalizeLegacyToolName", () => {
test("maps Task to Agent", () => {
expect(normalizeLegacyToolName("Task")).toBe("Agent");
});
test("maps KillShell to TaskStop", () => {
expect(normalizeLegacyToolName("KillShell")).toBe("TaskStop");
});
test("returns unknown name as-is", () => {
expect(normalizeLegacyToolName("UnknownTool")).toBe("UnknownTool");
});
test("preserves current canonical names", () => {
expect(normalizeLegacyToolName("Bash")).toBe("Bash");
expect(normalizeLegacyToolName("Agent")).toBe("Agent");
});
});

View File

@@ -0,0 +1,165 @@
import { mock, describe, expect, test } from "bun:test";
// Mock log.ts to cut the heavy dependency chain
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 slowOperations to avoid bun:bundle
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 {
getDenyRuleForTool,
getAskRuleForTool,
getDenyRuleForAgent,
filterDeniedAgents,
} = await import("../permissions");
import { getEmptyToolPermissionContext } from "../../../Tool";
// ─── Helper ─────────────────────────────────────────────────────────────
function makeContext(opts: {
denyRules?: string[];
askRules?: string[];
}) {
const ctx = getEmptyToolPermissionContext();
const deny: Record<string, string[]> = {};
const ask: Record<string, string[]> = {};
// alwaysDenyRules stores raw rule strings — getDenyRules() calls
// permissionRuleValueFromString internally
if (opts.denyRules?.length) {
deny["localSettings"] = opts.denyRules;
}
if (opts.askRules?.length) {
ask["localSettings"] = opts.askRules;
}
return {
...ctx,
alwaysDenyRules: deny,
alwaysAskRules: ask,
} as any;
}
function makeTool(name: string, mcpInfo?: { serverName: string; toolName: string }) {
return { name, mcpInfo };
}
// ─── getDenyRuleForTool ─────────────────────────────────────────────────
describe("getDenyRuleForTool", () => {
test("returns null when no deny rules", () => {
const ctx = makeContext({});
expect(getDenyRuleForTool(ctx, makeTool("Bash"))).toBeNull();
});
test("returns matching deny rule for tool", () => {
const ctx = makeContext({ denyRules: ["Bash"] });
const result = getDenyRuleForTool(ctx, makeTool("Bash"));
expect(result).not.toBeNull();
expect(result!.ruleValue.toolName).toBe("Bash");
});
test("returns null for non-matching tool", () => {
const ctx = makeContext({ denyRules: ["Bash"] });
expect(getDenyRuleForTool(ctx, makeTool("Read"))).toBeNull();
});
test("rule with content does not match whole-tool deny", () => {
// getDenyRuleForTool uses toolMatchesRule which requires ruleContent === undefined
// Rules like "Bash(rm -rf)" only match specific invocations, not the entire tool
const ctx = makeContext({ denyRules: ["Bash(rm -rf)"] });
const result = getDenyRuleForTool(ctx, makeTool("Bash"));
expect(result).toBeNull();
});
});
// ─── getAskRuleForTool ──────────────────────────────────────────────────
describe("getAskRuleForTool", () => {
test("returns null when no ask rules", () => {
const ctx = makeContext({});
expect(getAskRuleForTool(ctx, makeTool("Bash"))).toBeNull();
});
test("returns matching ask rule", () => {
const ctx = makeContext({ askRules: ["Write"] });
const result = getAskRuleForTool(ctx, makeTool("Write"));
expect(result).not.toBeNull();
});
test("returns null for non-matching tool", () => {
const ctx = makeContext({ askRules: ["Write"] });
expect(getAskRuleForTool(ctx, makeTool("Bash"))).toBeNull();
});
});
// ─── getDenyRuleForAgent ────────────────────────────────────────────────
describe("getDenyRuleForAgent", () => {
test("returns null when no deny rules", () => {
const ctx = makeContext({});
expect(getDenyRuleForAgent(ctx, "Agent", "Explore")).toBeNull();
});
test("returns matching deny rule for agent type", () => {
const ctx = makeContext({ denyRules: ["Agent(Explore)"] });
const result = getDenyRuleForAgent(ctx, "Agent", "Explore");
expect(result).not.toBeNull();
});
test("returns null for non-matching agent type", () => {
const ctx = makeContext({ denyRules: ["Agent(Explore)"] });
expect(getDenyRuleForAgent(ctx, "Agent", "Research")).toBeNull();
});
});
// ─── filterDeniedAgents ─────────────────────────────────────────────────
describe("filterDeniedAgents", () => {
test("returns all agents when no deny rules", () => {
const ctx = makeContext({});
const agents = [{ agentType: "Explore" }, { agentType: "Research" }];
expect(filterDeniedAgents(agents, ctx, "Agent")).toEqual(agents);
});
test("filters out denied agent type", () => {
const ctx = makeContext({ denyRules: ["Agent(Explore)"] });
const agents = [{ agentType: "Explore" }, { agentType: "Research" }];
const result = filterDeniedAgents(agents, ctx, "Agent");
expect(result).toHaveLength(1);
expect(result[0]!.agentType).toBe("Research");
});
test("returns empty array when all agents denied", () => {
const ctx = makeContext({
denyRules: ["Agent(Explore)", "Agent(Research)"],
});
const agents = [{ agentType: "Explore" }, { agentType: "Research" }];
expect(filterDeniedAgents(agents, ctx, "Agent")).toEqual([]);
});
});

View File

@@ -0,0 +1,476 @@
import { describe, expect, test } from "bun:test";
import {
SettingsSchema,
EnvironmentVariablesSchema,
PermissionsSchema,
AllowedMcpServerEntrySchema,
DeniedMcpServerEntrySchema,
isMcpServerNameEntry,
isMcpServerCommandEntry,
isMcpServerUrlEntry,
CUSTOMIZATION_SURFACES,
} from "../types";
import {
SETTING_SOURCES,
SOURCES,
CLAUDE_CODE_SETTINGS_SCHEMA_URL,
getSettingSourceName,
getSourceDisplayName,
getSettingSourceDisplayNameLowercase,
getSettingSourceDisplayNameCapitalized,
parseSettingSourcesFlag,
} from "../constants";
import {
formatZodError,
filterInvalidPermissionRules,
validateSettingsFileContent,
} from "../validation";
// ─── Settings Schema Validation ──────────────────────────────────────────
describe("SettingsSchema", () => {
test("accepts empty object", () => {
const result = SettingsSchema().safeParse({});
expect(result.success).toBe(true);
});
test("accepts model string", () => {
const result = SettingsSchema().safeParse({ model: "sonnet" });
expect(result.success).toBe(true);
});
test("accepts permissions block with allow rules", () => {
const result = SettingsSchema().safeParse({
permissions: { allow: ["Bash(npm install)"] },
});
expect(result.success).toBe(true);
});
test("accepts permissions block with deny rules", () => {
const result = SettingsSchema().safeParse({
permissions: { deny: ["Bash(rm -rf *)"] },
});
expect(result.success).toBe(true);
});
test("accepts env variables", () => {
const result = SettingsSchema().safeParse({
env: { FOO: "bar", DEBUG: "1" },
});
expect(result.success).toBe(true);
});
test("accepts hooks configuration", () => {
const result = SettingsSchema().safeParse({
hooks: {
PreToolUse: [
{
matcher: "Bash",
hooks: [{ type: "command", command: "echo test" }],
},
],
},
});
expect(result.success).toBe(true);
});
test("accepts attribution settings", () => {
const result = SettingsSchema().safeParse({
attribution: {
commit: "Generated by AI",
pr: "AI-generated PR",
},
});
expect(result.success).toBe(true);
});
test("accepts worktree settings", () => {
const result = SettingsSchema().safeParse({
worktree: {
symlinkDirectories: ["node_modules", ".cache"],
sparsePaths: ["src/"],
},
});
expect(result.success).toBe(true);
});
test("accepts $schema field", () => {
const result = SettingsSchema().safeParse({
$schema: CLAUDE_CODE_SETTINGS_SCHEMA_URL,
});
expect(result.success).toBe(true);
});
test("passes through unknown keys (passthrough mode)", () => {
const result = SettingsSchema().safeParse({ unknownKey: "value" });
expect(result.success).toBe(true);
if (result.success) {
expect((result.data as any).unknownKey).toBe("value");
}
});
test("coerces env var numbers to strings", () => {
const result = EnvironmentVariablesSchema().safeParse({ PORT: 3000 });
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.PORT).toBe("3000");
}
});
test("accepts boolean settings", () => {
const result = SettingsSchema().safeParse({
includeCoAuthoredBy: true,
respectGitignore: false,
disableAllHooks: true,
});
expect(result.success).toBe(true);
});
test("accepts cleanupPeriodDays", () => {
const result = SettingsSchema().safeParse({ cleanupPeriodDays: 30 });
expect(result.success).toBe(true);
});
test("rejects negative cleanupPeriodDays", () => {
const result = SettingsSchema().safeParse({ cleanupPeriodDays: -1 });
expect(result.success).toBe(false);
});
test("accepts statusLine configuration", () => {
const result = SettingsSchema().safeParse({
statusLine: { type: "command", command: "echo status" },
});
expect(result.success).toBe(true);
});
test("accepts sshConfigs", () => {
const result = SettingsSchema().safeParse({
sshConfigs: [
{
id: "dev-server",
name: "Development Server",
sshHost: "dev.example.com",
sshPort: 22,
},
],
});
expect(result.success).toBe(true);
});
});
// ─── Permissions Schema ─────────────────────────────────────────────────
describe("PermissionsSchema", () => {
test("accepts defaultMode", () => {
const result = PermissionsSchema().safeParse({
defaultMode: "acceptEdits",
});
expect(result.success).toBe(true);
});
test("accepts additionalDirectories", () => {
const result = PermissionsSchema().safeParse({
additionalDirectories: ["/tmp/extra"],
});
expect(result.success).toBe(true);
});
test("accepts disableBypassPermissionsMode", () => {
const result = PermissionsSchema().safeParse({
disableBypassPermissionsMode: "disable",
});
expect(result.success).toBe(true);
});
});
// ─── AllowedMcpServerEntrySchema ────────────────────────────────────────
describe("AllowedMcpServerEntrySchema", () => {
test("accepts serverName entry", () => {
const result = AllowedMcpServerEntrySchema().safeParse({
serverName: "my-server",
});
expect(result.success).toBe(true);
});
test("accepts serverCommand entry", () => {
const result = AllowedMcpServerEntrySchema().safeParse({
serverCommand: ["npx", "mcp-server"],
});
expect(result.success).toBe(true);
});
test("accepts serverUrl entry", () => {
const result = AllowedMcpServerEntrySchema().safeParse({
serverUrl: "https://*.example.com/*",
});
expect(result.success).toBe(true);
});
test("rejects entry with no fields", () => {
const result = AllowedMcpServerEntrySchema().safeParse({});
expect(result.success).toBe(false);
});
test("rejects entry with multiple fields", () => {
const result = AllowedMcpServerEntrySchema().safeParse({
serverName: "my-server",
serverUrl: "https://example.com",
});
expect(result.success).toBe(false);
});
test("rejects invalid serverName characters", () => {
const result = AllowedMcpServerEntrySchema().safeParse({
serverName: "my server with spaces",
});
expect(result.success).toBe(false);
});
test("rejects empty serverCommand array", () => {
const result = AllowedMcpServerEntrySchema().safeParse({
serverCommand: [],
});
expect(result.success).toBe(false);
});
});
// ─── Type guards ─────────────────────────────────────────────────────────
describe("MCP server entry type guards", () => {
test("isMcpServerNameEntry identifies name entry", () => {
expect(isMcpServerNameEntry({ serverName: "test" })).toBe(true);
});
test("isMcpServerNameEntry rejects non-name entry", () => {
expect(isMcpServerNameEntry({ serverUrl: "https://example.com" })).toBe(
false
);
});
test("isMcpServerCommandEntry identifies command entry", () => {
expect(isMcpServerCommandEntry({ serverCommand: ["npx", "srv"] })).toBe(
true
);
});
test("isMcpServerCommandEntry rejects non-command entry", () => {
expect(isMcpServerCommandEntry({ serverName: "test" })).toBe(false);
});
test("isMcpServerUrlEntry identifies url entry", () => {
expect(
isMcpServerUrlEntry({ serverUrl: "https://example.com" })
).toBe(true);
});
test("isMcpServerUrlEntry rejects non-url entry", () => {
expect(isMcpServerUrlEntry({ serverName: "test" })).toBe(false);
});
});
// ─── Constants ──────────────────────────────────────────────────────────
describe("SETTING_SOURCES", () => {
test("contains all five sources in order", () => {
expect(SETTING_SOURCES).toEqual([
"userSettings",
"projectSettings",
"localSettings",
"flagSettings",
"policySettings",
]);
});
});
describe("SOURCES (editable)", () => {
test("contains three editable sources", () => {
expect(SOURCES).toEqual([
"localSettings",
"projectSettings",
"userSettings",
]);
});
});
describe("CUSTOMIZATION_SURFACES", () => {
test("contains expected surfaces", () => {
expect(CUSTOMIZATION_SURFACES).toEqual([
"skills",
"agents",
"hooks",
"mcp",
]);
});
});
describe("getSettingSourceName", () => {
test("maps userSettings to user", () => {
expect(getSettingSourceName("userSettings")).toBe("user");
});
test("maps projectSettings to project", () => {
expect(getSettingSourceName("projectSettings")).toBe("project");
});
test("maps localSettings to project, gitignored", () => {
expect(getSettingSourceName("localSettings")).toBe("project, gitignored");
});
test("maps flagSettings to cli flag", () => {
expect(getSettingSourceName("flagSettings")).toBe("cli flag");
});
test("maps policySettings to managed", () => {
expect(getSettingSourceName("policySettings")).toBe("managed");
});
});
describe("getSourceDisplayName", () => {
test("maps userSettings to User", () => {
expect(getSourceDisplayName("userSettings")).toBe("User");
});
test("maps plugin to Plugin", () => {
expect(getSourceDisplayName("plugin")).toBe("Plugin");
});
test("maps built-in to Built-in", () => {
expect(getSourceDisplayName("built-in")).toBe("Built-in");
});
});
describe("getSettingSourceDisplayNameLowercase", () => {
test("maps policySettings correctly", () => {
expect(getSettingSourceDisplayNameLowercase("policySettings")).toBe(
"enterprise managed settings"
);
});
test("maps cliArg correctly", () => {
expect(getSettingSourceDisplayNameLowercase("cliArg")).toBe("CLI argument");
});
test("maps session correctly", () => {
expect(getSettingSourceDisplayNameLowercase("session")).toBe(
"current session"
);
});
});
describe("getSettingSourceDisplayNameCapitalized", () => {
test("maps userSettings correctly", () => {
expect(getSettingSourceDisplayNameCapitalized("userSettings")).toBe(
"User settings"
);
});
test("maps command correctly", () => {
expect(getSettingSourceDisplayNameCapitalized("command")).toBe(
"Command configuration"
);
});
});
describe("parseSettingSourcesFlag", () => {
test("parses comma-separated sources", () => {
expect(parseSettingSourcesFlag("user,project,local")).toEqual([
"userSettings",
"projectSettings",
"localSettings",
]);
});
test("parses single source", () => {
expect(parseSettingSourcesFlag("user")).toEqual(["userSettings"]);
});
test("returns empty array for empty string", () => {
expect(parseSettingSourcesFlag("")).toEqual([]);
});
test("trims whitespace", () => {
expect(parseSettingSourcesFlag("user , project")).toEqual([
"userSettings",
"projectSettings",
]);
});
test("throws for invalid source name", () => {
expect(() => parseSettingSourcesFlag("invalid")).toThrow(
"Invalid setting source"
);
});
});
// ─── Validation ─────────────────────────────────────────────────────────
describe("filterInvalidPermissionRules", () => {
test("returns empty for non-object input", () => {
expect(filterInvalidPermissionRules(null, "test.json")).toEqual([]);
expect(filterInvalidPermissionRules("string", "test.json")).toEqual([]);
});
test("returns empty when no permissions", () => {
expect(filterInvalidPermissionRules({}, "test.json")).toEqual([]);
});
test("filters non-string rules and returns warnings", () => {
const data = { permissions: { allow: ["Bash", 123, "Read"] } };
const warnings = filterInvalidPermissionRules(data, "test.json");
expect(warnings.length).toBe(1);
expect(warnings[0]!.path).toBe("permissions.allow");
expect((data.permissions as any).allow).toEqual(["Bash", "Read"]);
});
test("preserves valid rules", () => {
const data = {
permissions: { allow: ["Bash(npm install)", "Read", "Write"] },
};
const warnings = filterInvalidPermissionRules(data, "test.json");
expect(warnings).toEqual([]);
expect((data.permissions as any).allow).toEqual([
"Bash(npm install)",
"Read",
"Write",
]);
});
});
describe("validateSettingsFileContent", () => {
test("accepts valid JSON settings", () => {
const result = validateSettingsFileContent('{"model": "sonnet"}');
expect(result.isValid).toBe(true);
});
test("accepts empty object", () => {
const result = validateSettingsFileContent("{}");
expect(result.isValid).toBe(true);
});
test("rejects invalid JSON", () => {
const result = validateSettingsFileContent("not json");
expect(result.isValid).toBe(false);
if (!result.isValid) {
expect(result.error).toContain("Invalid JSON");
}
});
test("rejects unknown keys in strict mode", () => {
const result = validateSettingsFileContent('{"unknownField": true}');
expect(result.isValid).toBe(false);
});
});
describe("formatZodError", () => {
test("formats invalid type error", () => {
const result = SettingsSchema().safeParse({ model: 123 });
expect(result.success).toBe(false);
if (!result.success) {
const errors = formatZodError(result.error, "settings.json");
expect(errors.length).toBeGreaterThan(0);
expect(errors[0]!.file).toBe("settings.json");
expect(errors[0]!.path).toContain("model");
}
});
});