test: 添加一大堆测试文件

This commit is contained in:
claude-code-best
2026-04-02 20:28:08 +08:00
parent 6f5623b26c
commit ce29527a67
33 changed files with 3502 additions and 329 deletions

View File

@@ -0,0 +1,136 @@
import { describe, expect, test } from "bun:test";
import { collapseHookSummaries } from "../collapseHookSummaries";
function makeHookSummary(overrides: Partial<{
hookLabel: string;
hookCount: number;
hookInfos: any[];
hookErrors: any[];
preventedContinuation: boolean;
hasOutput: boolean;
totalDurationMs: number;
}> = {}): any {
return {
type: "system",
subtype: "stop_hook_summary",
hookLabel: overrides.hookLabel ?? "PostToolUse",
hookCount: overrides.hookCount ?? 1,
hookInfos: overrides.hookInfos ?? [],
hookErrors: overrides.hookErrors ?? [],
preventedContinuation: overrides.preventedContinuation ?? false,
hasOutput: overrides.hasOutput ?? false,
totalDurationMs: overrides.totalDurationMs ?? 10,
};
}
function makeNonHookMessage(): any {
return { type: "user", message: { content: "hello" } };
}
describe("collapseHookSummaries", () => {
test("returns same messages when no hook summaries", () => {
const messages = [makeNonHookMessage(), makeNonHookMessage()];
expect(collapseHookSummaries(messages)).toEqual(messages);
});
test("collapses consecutive messages with same hookLabel", () => {
const messages = [
makeHookSummary({ hookLabel: "PostToolUse", hookCount: 1 }),
makeHookSummary({ hookLabel: "PostToolUse", hookCount: 2 }),
];
const result = collapseHookSummaries(messages);
expect(result).toHaveLength(1);
expect(result[0].hookCount).toBe(3);
});
test("does not collapse messages with different hookLabels", () => {
const messages = [
makeHookSummary({ hookLabel: "PostToolUse" }),
makeHookSummary({ hookLabel: "PreToolUse" }),
];
const result = collapseHookSummaries(messages);
expect(result).toHaveLength(2);
});
test("aggregates hookCount across collapsed messages", () => {
const messages = [
makeHookSummary({ hookLabel: "A", hookCount: 3 }),
makeHookSummary({ hookLabel: "A", hookCount: 5 }),
];
const result = collapseHookSummaries(messages);
expect(result[0].hookCount).toBe(8);
});
test("merges hookInfos arrays", () => {
const info1 = { tool: "Read" };
const info2 = { tool: "Write" };
const messages = [
makeHookSummary({ hookLabel: "A", hookInfos: [info1] }),
makeHookSummary({ hookLabel: "A", hookInfos: [info2] }),
];
const result = collapseHookSummaries(messages);
expect(result[0].hookInfos).toEqual([info1, info2]);
});
test("merges hookErrors arrays", () => {
const err1 = new Error("e1");
const err2 = new Error("e2");
const messages = [
makeHookSummary({ hookLabel: "A", hookErrors: [err1] }),
makeHookSummary({ hookLabel: "A", hookErrors: [err2] }),
];
const result = collapseHookSummaries(messages);
expect(result[0].hookErrors).toHaveLength(2);
});
test("takes max totalDurationMs", () => {
const messages = [
makeHookSummary({ hookLabel: "A", totalDurationMs: 50 }),
makeHookSummary({ hookLabel: "A", totalDurationMs: 100 }),
makeHookSummary({ hookLabel: "A", totalDurationMs: 75 }),
];
const result = collapseHookSummaries(messages);
expect(result[0].totalDurationMs).toBe(100);
});
test("takes any truthy preventContinuation", () => {
const messages = [
makeHookSummary({ hookLabel: "A", preventedContinuation: false }),
makeHookSummary({ hookLabel: "A", preventedContinuation: true }),
];
const result = collapseHookSummaries(messages);
expect(result[0].preventedContinuation).toBe(true);
});
test("leaves single hook summary unchanged", () => {
const msg = makeHookSummary({ hookLabel: "PostToolUse", hookCount: 5 });
const result = collapseHookSummaries([msg]);
expect(result).toHaveLength(1);
expect(result[0].hookCount).toBe(5);
});
test("handles three consecutive same-label summaries", () => {
const messages = [
makeHookSummary({ hookLabel: "X", hookCount: 1 }),
makeHookSummary({ hookLabel: "X", hookCount: 1 }),
makeHookSummary({ hookLabel: "X", hookCount: 1 }),
];
const result = collapseHookSummaries(messages);
expect(result).toHaveLength(1);
expect(result[0].hookCount).toBe(3);
});
test("preserves non-hook messages in between", () => {
const messages = [
makeHookSummary({ hookLabel: "A" }),
makeNonHookMessage(),
makeHookSummary({ hookLabel: "A" }),
];
const result = collapseHookSummaries(messages);
expect(result).toHaveLength(3);
});
test("returns empty array for empty input", () => {
expect(collapseHookSummaries([])).toEqual([]);
});
});

View File

@@ -0,0 +1,94 @@
import { describe, expect, test } from "bun:test";
import { collapseTeammateShutdowns } from "../collapseTeammateShutdowns";
function makeShutdownMsg(uuid = "1"): any {
return {
type: "attachment",
uuid,
timestamp: Date.now(),
attachment: {
type: "task_status",
taskType: "in_process_teammate",
status: "completed",
},
};
}
function makeNonShutdownMsg(): any {
return { type: "user", message: { content: "hello" } };
}
describe("collapseTeammateShutdowns", () => {
test("returns same messages when no teammate shutdowns", () => {
const msgs = [makeNonShutdownMsg(), makeNonShutdownMsg()];
expect(collapseTeammateShutdowns(msgs)).toEqual(msgs);
});
test("leaves single shutdown message unchanged", () => {
const msgs = [makeShutdownMsg()];
const result = collapseTeammateShutdowns(msgs);
expect(result).toHaveLength(1);
expect(result[0]).toEqual(msgs[0]);
});
test("collapses consecutive shutdown messages into batch", () => {
const msgs = [makeShutdownMsg("1"), makeShutdownMsg("2")];
const result = collapseTeammateShutdowns(msgs);
expect(result).toHaveLength(1);
expect(result[0].attachment.type).toBe("teammate_shutdown_batch");
});
test("batch attachment has correct count", () => {
const msgs = [makeShutdownMsg("1"), makeShutdownMsg("2"), makeShutdownMsg("3")];
const result = collapseTeammateShutdowns(msgs);
expect(result[0].attachment.count).toBe(3);
});
test("does not collapse non-consecutive shutdowns", () => {
const msgs = [makeShutdownMsg("1"), makeNonShutdownMsg(), makeShutdownMsg("2")];
const result = collapseTeammateShutdowns(msgs);
expect(result).toHaveLength(3);
expect(result[0].attachment.type).toBe("task_status");
expect(result[2].attachment.type).toBe("task_status");
});
test("preserves non-shutdown messages between shutdowns", () => {
const msgs = [makeShutdownMsg("1"), makeNonShutdownMsg(), makeShutdownMsg("2")];
const result = collapseTeammateShutdowns(msgs);
expect(result[1]).toEqual(makeNonShutdownMsg());
});
test("handles empty array", () => {
expect(collapseTeammateShutdowns([])).toEqual([]);
});
test("handles mixed message types", () => {
const msgs = [makeNonShutdownMsg(), makeShutdownMsg("1"), makeShutdownMsg("2"), makeNonShutdownMsg()];
const result = collapseTeammateShutdowns(msgs);
expect(result).toHaveLength(3);
expect(result[1].attachment.type).toBe("teammate_shutdown_batch");
});
test("collapses more than 2 consecutive shutdowns", () => {
const msgs = Array.from({ length: 5 }, (_, i) => makeShutdownMsg(String(i)));
const result = collapseTeammateShutdowns(msgs);
expect(result).toHaveLength(1);
expect(result[0].attachment.count).toBe(5);
});
test("non-teammate task_status messages are not collapsed", () => {
const nonTeammate: any = {
type: "attachment",
uuid: "x",
timestamp: Date.now(),
attachment: {
type: "task_status",
taskType: "subagent",
status: "completed",
},
};
const msgs = [nonTeammate, { ...nonTeammate, uuid: "y" }];
const result = collapseTeammateShutdowns(msgs);
expect(result).toHaveLength(2);
});
});

View File

@@ -0,0 +1,70 @@
import { describe, expect, test } from "bun:test";
import {
NOTIFICATION_CHANNELS,
EDITOR_MODES,
TEAMMATE_MODES,
} from "../configConstants";
describe("NOTIFICATION_CHANNELS", () => {
test("contains expected channels", () => {
expect(NOTIFICATION_CHANNELS).toContain("auto");
expect(NOTIFICATION_CHANNELS).toContain("iterm2");
expect(NOTIFICATION_CHANNELS).toContain("terminal_bell");
expect(NOTIFICATION_CHANNELS).toContain("kitty");
expect(NOTIFICATION_CHANNELS).toContain("ghostty");
});
test("is readonly array", () => {
expect(Array.isArray(NOTIFICATION_CHANNELS)).toBe(true);
// TypeScript enforces readonly at compile time; runtime is still a plain array
expect(NOTIFICATION_CHANNELS.length).toBeGreaterThan(0);
});
test("includes all documented channels", () => {
expect(NOTIFICATION_CHANNELS).toEqual([
"auto",
"iterm2",
"iterm2_with_bell",
"terminal_bell",
"kitty",
"ghostty",
"notifications_disabled",
]);
});
test("has no duplicate entries", () => {
const unique = new Set(NOTIFICATION_CHANNELS);
expect(unique.size).toBe(NOTIFICATION_CHANNELS.length);
});
});
describe("EDITOR_MODES", () => {
test("contains 'normal' and 'vim'", () => {
expect(EDITOR_MODES).toContain("normal");
expect(EDITOR_MODES).toContain("vim");
});
test("has exactly 2 entries", () => {
expect(EDITOR_MODES).toHaveLength(2);
});
test("is ordered: normal, vim", () => {
expect(EDITOR_MODES).toEqual(["normal", "vim"]);
});
});
describe("TEAMMATE_MODES", () => {
test("contains 'auto', 'tmux', 'in-process'", () => {
expect(TEAMMATE_MODES).toContain("auto");
expect(TEAMMATE_MODES).toContain("tmux");
expect(TEAMMATE_MODES).toContain("in-process");
});
test("has exactly 3 entries", () => {
expect(TEAMMATE_MODES).toHaveLength(3);
});
test("is ordered: auto, tmux, in-process", () => {
expect(TEAMMATE_MODES).toEqual(["auto", "tmux", "in-process"]);
});
});

View File

@@ -0,0 +1,108 @@
import { describe, expect, test } from "bun:test";
import { parseGitRemote, parseGitHubRepository } from "../detectRepository";
describe("parseGitRemote", () => {
// HTTPS
test("parses HTTPS URL: https://github.com/owner/repo.git", () => {
const result = parseGitRemote("https://github.com/owner/repo.git");
expect(result).toEqual({ host: "github.com", owner: "owner", name: "repo" });
});
test("parses HTTPS URL without .git suffix", () => {
const result = parseGitRemote("https://github.com/owner/repo");
expect(result).toEqual({ host: "github.com", owner: "owner", name: "repo" });
});
test("parses HTTPS URL with subdirectory path (only takes first 2 segments)", () => {
const result = parseGitRemote("https://github.com/owner/repo.git");
expect(result).not.toBeNull();
expect(result!.name).toBe("repo");
});
// SSH
test("parses SSH URL: git@github.com:owner/repo.git", () => {
const result = parseGitRemote("git@github.com:owner/repo.git");
expect(result).toEqual({ host: "github.com", owner: "owner", name: "repo" });
});
test("parses SSH URL without .git suffix", () => {
const result = parseGitRemote("git@github.com:owner/repo");
expect(result).toEqual({ host: "github.com", owner: "owner", name: "repo" });
});
// ssh://
test("parses ssh:// URL: ssh://git@github.com/owner/repo.git", () => {
const result = parseGitRemote("ssh://git@github.com/owner/repo.git");
expect(result).toEqual({ host: "github.com", owner: "owner", name: "repo" });
});
// git://
test("parses git:// URL", () => {
const result = parseGitRemote("git://github.com/owner/repo.git");
expect(result).toEqual({ host: "github.com", owner: "owner", name: "repo" });
});
// Boundary
test("returns null for invalid URL", () => {
expect(parseGitRemote("not-a-url")).toBeNull();
});
test("returns null for empty string", () => {
expect(parseGitRemote("")).toBeNull();
});
test("handles GHE hostname", () => {
const result = parseGitRemote("https://ghe.corp.com/team/project.git");
expect(result).toEqual({ host: "ghe.corp.com", owner: "team", name: "project" });
});
test("handles port number in URL", () => {
const result = parseGitRemote("https://github.com:443/owner/repo.git");
expect(result).not.toBeNull();
expect(result!.owner).toBe("owner");
expect(result!.name).toBe("repo");
});
test("rejects SSH config alias without real hostname", () => {
expect(parseGitRemote("git@github.com-work:owner/repo.git")).toBeNull();
});
test("handles repo names with dots", () => {
const result = parseGitRemote("https://github.com/owner/cc.kurs.web.git");
expect(result).toEqual({ host: "github.com", owner: "owner", name: "cc.kurs.web" });
});
});
describe("parseGitHubRepository", () => {
test("extracts 'owner/repo' from valid remote URL", () => {
expect(parseGitHubRepository("https://github.com/owner/repo.git")).toBe("owner/repo");
});
test("handles plain 'owner/repo' string input", () => {
expect(parseGitHubRepository("owner/repo")).toBe("owner/repo");
});
test("returns null for non-GitHub host", () => {
expect(parseGitHubRepository("https://gitlab.com/owner/repo.git")).toBeNull();
});
test("returns null for invalid input", () => {
expect(parseGitHubRepository("not-valid")).toBeNull();
});
test("is case-sensitive for owner/repo", () => {
expect(parseGitHubRepository("Owner/Repo")).toBe("Owner/Repo");
});
test("handles SSH format for github.com", () => {
expect(parseGitHubRepository("git@github.com:owner/repo.git")).toBe("owner/repo");
});
test("returns null for GHE SSH URL", () => {
expect(parseGitHubRepository("git@ghe.corp.com:owner/repo.git")).toBeNull();
});
test("handles plain owner/repo with .git suffix", () => {
expect(parseGitHubRepository("owner/repo.git")).toBe("owner/repo");
});
});

View File

@@ -0,0 +1,110 @@
import { describe, expect, test } from "bun:test";
import { parseDirectMemberMessage, sendDirectMemberMessage } from "../directMemberMessage";
describe("parseDirectMemberMessage", () => {
test("parses '@agent-name hello world'", () => {
const result = parseDirectMemberMessage("@agent-name hello world");
expect(result).toEqual({ recipientName: "agent-name", message: "hello world" });
});
test("parses '@agent-name single-word'", () => {
const result = parseDirectMemberMessage("@agent-name single-word");
expect(result).toEqual({ recipientName: "agent-name", message: "single-word" });
});
test("returns null for non-matching input", () => {
expect(parseDirectMemberMessage("hello world")).toBeNull();
});
test("returns null for empty string", () => {
expect(parseDirectMemberMessage("")).toBeNull();
});
test("returns null for '@name' without message", () => {
expect(parseDirectMemberMessage("@name")).toBeNull();
});
test("handles hyphenated agent names like '@my-agent msg'", () => {
const result = parseDirectMemberMessage("@my-agent msg");
expect(result).toEqual({ recipientName: "my-agent", message: "msg" });
});
test("handles multiline message content", () => {
const result = parseDirectMemberMessage("@agent line1\nline2");
expect(result).toEqual({ recipientName: "agent", message: "line1\nline2" });
});
test("extracts correct recipientName and message", () => {
const result = parseDirectMemberMessage("@alice please fix the bug");
expect(result?.recipientName).toBe("alice");
expect(result?.message).toBe("please fix the bug");
});
test("trims message whitespace", () => {
const result = parseDirectMemberMessage("@agent hello ");
expect(result?.message).toBe("hello");
});
});
describe("sendDirectMemberMessage", () => {
test("returns error when no team context", async () => {
const result = await sendDirectMemberMessage("agent", "hello", null as any);
expect(result).toEqual({ success: false, error: "no_team_context" });
});
test("returns error for unknown recipient", async () => {
const teamContext = {
teamName: "team1",
teammates: { alice: { name: "alice" } },
};
const result = await sendDirectMemberMessage(
"bob",
"hello",
teamContext as any,
async () => {},
);
expect(result).toEqual({
success: false,
error: "unknown_recipient",
recipientName: "bob",
});
});
test("calls writeToMailbox with correct args for valid recipient", async () => {
let mailboxArgs: any = null;
const teamContext = {
teamName: "team1",
teammates: { alice: { name: "alice" } },
};
const result = await sendDirectMemberMessage(
"alice",
"hello",
teamContext as any,
async (recipient, msg, team) => {
mailboxArgs = { recipient, msg, team };
},
);
expect(result).toEqual({ success: true, recipientName: "alice" });
expect(mailboxArgs.recipient).toBe("alice");
expect(mailboxArgs.msg.text).toBe("hello");
expect(mailboxArgs.msg.from).toBe("user");
expect(mailboxArgs.team).toBe("team1");
});
test("returns success for valid message", async () => {
const teamContext = {
teamName: "team1",
teammates: { bob: { name: "bob" } },
};
const result = await sendDirectMemberMessage(
"bob",
"test message",
teamContext as any,
async () => {},
);
expect(result.success).toBe(true);
if (result.success) {
expect(result.recipientName).toBe("bob");
}
});
});

View File

@@ -0,0 +1,122 @@
import { describe, expect, test } from "bun:test";
import {
FINGERPRINT_SALT,
extractFirstMessageText,
computeFingerprint,
} from "../fingerprint";
describe("FINGERPRINT_SALT", () => {
test("has expected value '59cf53e54c78'", () => {
expect(FINGERPRINT_SALT).toBe("59cf53e54c78");
});
});
describe("extractFirstMessageText", () => {
test("extracts text from first user message", () => {
const messages = [
{ type: "user", message: { content: "hello world" } },
];
expect(extractFirstMessageText(messages as any)).toBe("hello world");
});
test("extracts text from single user message with array content", () => {
const messages = [
{
type: "user",
message: {
content: [{ type: "text", text: "hello" }, { type: "image", url: "x" }],
},
},
];
expect(extractFirstMessageText(messages as any)).toBe("hello");
});
test("returns empty string when no user messages", () => {
const messages = [
{ type: "assistant", message: { content: "hi" } },
];
expect(extractFirstMessageText(messages as any)).toBe("");
});
test("skips assistant messages", () => {
const messages = [
{ type: "assistant", message: { content: "hi" } },
{ type: "user", message: { content: "hello" } },
];
expect(extractFirstMessageText(messages as any)).toBe("hello");
});
test("handles mixed content blocks (text + image)", () => {
const messages = [
{
type: "user",
message: {
content: [
{ type: "image", url: "http://example.com/img.png" },
{ type: "text", text: "after image" },
],
},
},
];
expect(extractFirstMessageText(messages as any)).toBe("after image");
});
test("returns empty string for empty array", () => {
expect(extractFirstMessageText([])).toBe("");
});
test("returns empty string when content has no text block", () => {
const messages = [
{
type: "user",
message: {
content: [{ type: "image", url: "x" }],
},
},
];
expect(extractFirstMessageText(messages as any)).toBe("");
});
});
describe("computeFingerprint", () => {
test("returns deterministic 3-char hex string", () => {
const result = computeFingerprint("test message", "1.0.0");
expect(result).toHaveLength(3);
expect(result).toMatch(/^[0-9a-f]{3}$/);
});
test("same input produces same fingerprint", () => {
const a = computeFingerprint("same input", "1.0.0");
const b = computeFingerprint("same input", "1.0.0");
expect(a).toBe(b);
});
test("different message text produces different fingerprint", () => {
const a = computeFingerprint("hello world from test one", "1.0.0");
const b = computeFingerprint("goodbye world from test two", "1.0.0");
expect(a).not.toBe(b);
});
test("different version produces different fingerprint", () => {
const a = computeFingerprint("same text", "1.0.0");
const b = computeFingerprint("same text", "2.0.0");
expect(a).not.toBe(b);
});
test("handles short strings (length < 21)", () => {
const result = computeFingerprint("hi", "1.0.0");
expect(result).toHaveLength(3);
expect(result).toMatch(/^[0-9a-f]{3}$/);
});
test("handles empty string", () => {
const result = computeFingerprint("", "1.0.0");
expect(result).toHaveLength(3);
expect(result).toMatch(/^[0-9a-f]{3}$/);
});
test("fingerprint is valid hex", () => {
const result = computeFingerprint("any message here for testing", "3.5.1");
expect(result).toMatch(/^[0-9a-f]{3}$/);
});
});

View File

@@ -0,0 +1,114 @@
import { describe, expect, test } from "bun:test";
import { lastX, returnValue, all, toArray, fromArray } from "../generators";
async function* range(n: number): AsyncGenerator<number, void> {
for (let i = 0; i < n; i++) {
yield i;
}
}
describe("lastX", () => {
test("returns last yielded value", async () => {
const result = await lastX(range(5));
expect(result).toBe(4);
});
test("returns only value from single-yield generator", async () => {
const result = await lastX(range(1));
expect(result).toBe(0);
});
test("throws on empty generator", async () => {
await expect(lastX(range(0))).rejects.toThrow("No items in generator");
});
});
describe("returnValue", () => {
test("returns generator return value", async () => {
async function* gen(): AsyncGenerator<number, string> {
yield 1;
return "done";
}
const result = await returnValue(gen());
expect(result).toBe("done");
});
test("returns undefined for void return", async () => {
async function* gen(): AsyncGenerator<number, void> {
yield 1;
}
const result = await returnValue(gen());
expect(result).toBeUndefined();
});
});
describe("toArray", () => {
test("collects all yielded values", async () => {
const result = await toArray(range(4));
expect(result).toEqual([0, 1, 2, 3]);
});
test("returns empty array for empty generator", async () => {
const result = await toArray(fromArray([]));
expect(result).toEqual([]);
});
test("preserves order", async () => {
const result = await toArray(fromArray(["c", "b", "a"]));
expect(result).toEqual(["c", "b", "a"]);
});
});
describe("fromArray", () => {
test("yields all array elements", async () => {
const result = await toArray(fromArray([10, 20, 30]));
expect(result).toEqual([10, 20, 30]);
});
test("yields nothing for empty array", async () => {
const result = await toArray(fromArray([]));
expect(result).toEqual([]);
});
});
describe("all", () => {
test("merges multiple generators preserving yield order", async () => {
const gen1 = fromArray([1, 2]);
const gen2 = fromArray([3, 4]);
const result = await toArray(all([gen1, gen2]));
// All values from both generators should be present
expect(result.sort()).toEqual([1, 2, 3, 4]);
});
test("respects concurrency cap", async () => {
const gen1 = fromArray([1]);
const gen2 = fromArray([2]);
const gen3 = fromArray([3]);
const result = await toArray(all([gen1, gen2, gen3], 2));
expect(result.sort()).toEqual([1, 2, 3]);
});
test("handles empty generator array", async () => {
const result = await toArray(all([]));
expect(result).toEqual([]);
});
test("handles single generator", async () => {
const result = await toArray(all([fromArray([42])]));
expect(result).toEqual([42]);
});
test("handles generators of different lengths", async () => {
const gen1 = fromArray([1, 2, 3]);
const gen2 = fromArray([10]);
const result = await toArray(all([gen1, gen2]));
// all() merges concurrently, just verify all values are present
expect([...result].sort((a, b) => a - b)).toEqual([1, 2, 3, 10]);
});
test("yields all values from all generators", async () => {
const gens = [fromArray([1]), fromArray([2]), fromArray([3])];
const result = await toArray(all(gens));
expect(result).toHaveLength(3);
});
});

View File

@@ -0,0 +1,163 @@
import { describe, expect, test } from "bun:test";
import { calculateHorizontalScrollWindow } from "../horizontalScroll";
describe("calculateHorizontalScrollWindow", () => {
// Basic scenarios
test("all items fit within available width", () => {
const result = calculateHorizontalScrollWindow([10, 10, 10], 50, 3, 1);
expect(result).toEqual({
startIndex: 0,
endIndex: 3,
showLeftArrow: false,
showRightArrow: false,
});
});
test("single item selected within view", () => {
const result = calculateHorizontalScrollWindow([20], 50, 3, 0);
expect(result).toEqual({
startIndex: 0,
endIndex: 1,
showLeftArrow: false,
showRightArrow: false,
});
});
test("selected item at beginning", () => {
const widths = [10, 10, 10, 10, 10];
const result = calculateHorizontalScrollWindow(widths, 25, 3, 0);
expect(result.startIndex).toBe(0);
expect(result.showLeftArrow).toBe(false);
expect(result.showRightArrow).toBe(true);
expect(result.endIndex).toBeGreaterThan(0);
});
test("selected item at end", () => {
const widths = [10, 10, 10, 10, 10];
const result = calculateHorizontalScrollWindow(widths, 25, 3, 4);
expect(result.endIndex).toBe(5);
expect(result.showRightArrow).toBe(false);
expect(result.showLeftArrow).toBe(true);
});
test("selected item beyond visible range scrolls right", () => {
const widths = [10, 10, 10, 10, 10];
const result = calculateHorizontalScrollWindow(widths, 20, 3, 4);
expect(result.startIndex).toBeLessThanOrEqual(4);
expect(result.endIndex).toBeGreaterThan(4);
});
test("selected item before visible range scrolls left", () => {
const widths = [10, 10, 10, 10, 10];
// Select last item first (simulates initial scroll to end)
const result = calculateHorizontalScrollWindow(widths, 20, 3, 0);
expect(result.startIndex).toBe(0);
});
// Arrow indicators
test("showLeftArrow when items hidden on left", () => {
const widths = [10, 10, 10, 10, 10];
const result = calculateHorizontalScrollWindow(widths, 15, 3, 4);
expect(result.showLeftArrow).toBe(true);
});
test("showRightArrow when items hidden on right", () => {
const widths = [10, 10, 10, 10, 10];
const result = calculateHorizontalScrollWindow(widths, 15, 3, 0);
expect(result.showRightArrow).toBe(true);
});
test("no arrows when all items visible", () => {
const result = calculateHorizontalScrollWindow([10, 10], 50, 3, 0);
expect(result.showLeftArrow).toBe(false);
expect(result.showRightArrow).toBe(false);
});
test("both arrows when items hidden on both sides", () => {
const widths = [10, 10, 10, 10, 10, 10, 10];
// Select middle item with limited width
const result = calculateHorizontalScrollWindow(widths, 20, 3, 3);
// Both arrows may or may not show depending on exact fit
expect(result.startIndex).toBeLessThanOrEqual(3);
expect(result.endIndex).toBeGreaterThan(3);
});
// Boundary conditions
test("empty itemWidths array", () => {
const result = calculateHorizontalScrollWindow([], 50, 3, 0);
expect(result).toEqual({
startIndex: 0,
endIndex: 0,
showLeftArrow: false,
showRightArrow: false,
});
});
test("single item", () => {
const result = calculateHorizontalScrollWindow([30], 50, 3, 0);
expect(result).toEqual({
startIndex: 0,
endIndex: 1,
showLeftArrow: false,
showRightArrow: false,
});
});
test("available width is 0", () => {
const result = calculateHorizontalScrollWindow([10, 10], 0, 3, 0);
// With 0 width, nothing fits except maybe the selected
expect(result.startIndex).toBe(0);
});
test("item wider than available width", () => {
const result = calculateHorizontalScrollWindow([100], 50, 3, 0);
// Total width > available, but only one item
expect(result.startIndex).toBe(0);
expect(result.endIndex).toBe(1);
});
test("all items same width", () => {
const widths = [10, 10, 10, 10];
const result = calculateHorizontalScrollWindow(widths, 25, 3, 2);
expect(result.startIndex).toBeLessThanOrEqual(2);
expect(result.endIndex).toBeGreaterThan(2);
});
test("varying item widths", () => {
const widths = [5, 20, 5, 20, 5];
const result = calculateHorizontalScrollWindow(widths, 20, 3, 2);
expect(result.startIndex).toBeLessThanOrEqual(2);
expect(result.endIndex).toBeGreaterThan(2);
});
test("firstItemHasSeparator adds separator width to first item", () => {
const widths = [10, 10, 10, 10, 10];
const withSep = calculateHorizontalScrollWindow(widths, 20, 3, 4, true);
const withoutSep = calculateHorizontalScrollWindow(widths, 20, 3, 4, false);
// Both should include selected index 4
expect(withSep.endIndex).toBe(5);
expect(withoutSep.endIndex).toBe(5);
});
test("selectedIdx in middle of overflow", () => {
const widths = [10, 10, 10, 10, 10, 10, 10];
const result = calculateHorizontalScrollWindow(widths, 25, 3, 3);
expect(result.startIndex).toBeLessThanOrEqual(3);
expect(result.endIndex).toBeGreaterThan(3);
});
test("scroll snaps to show selected at left edge", () => {
const widths = [10, 10, 10, 10, 10];
// Jump to last item which forces scroll
const result = calculateHorizontalScrollWindow(widths, 20, 3, 4);
expect(result.startIndex).toBeLessThanOrEqual(4);
expect(result.endIndex).toBe(5);
});
test("scroll snaps to show selected at right edge", () => {
const widths = [10, 10, 10, 10, 10];
const result = calculateHorizontalScrollWindow(widths, 20, 3, 4);
expect(result.endIndex).toBe(5);
expect(result.startIndex).toBeGreaterThan(0);
});
});

View File

@@ -0,0 +1,54 @@
import { describe, expect, test } from "bun:test";
import { lazySchema } from "../lazySchema";
describe("lazySchema", () => {
test("returns a function", () => {
const factory = lazySchema(() => 42);
expect(typeof factory).toBe("function");
});
test("calls factory on first invocation", () => {
let callCount = 0;
const factory = lazySchema(() => {
callCount++;
return "result";
});
factory();
expect(callCount).toBe(1);
});
test("returns cached result on subsequent invocations", () => {
const factory = lazySchema(() => ({ value: Math.random() }));
const first = factory();
const second = factory();
expect(first).toBe(second);
});
test("factory is called only once", () => {
let callCount = 0;
const factory = lazySchema(() => {
callCount++;
return "cached";
});
factory();
factory();
factory();
expect(callCount).toBe(1);
});
test("works with different return types", () => {
const numFactory = lazySchema(() => 123);
expect(numFactory()).toBe(123);
const arrFactory = lazySchema(() => [1, 2, 3]);
expect(arrFactory()).toEqual([1, 2, 3]);
});
test("each call to lazySchema returns independent cache", () => {
const a = lazySchema(() => ({ id: "a" }));
const b = lazySchema(() => ({ id: "b" }));
expect(a()).not.toBe(b());
expect(a().id).toBe("a");
expect(b().id).toBe("b");
});
});

View File

@@ -0,0 +1,58 @@
import { describe, expect, test } from "bun:test";
import { padAligned } from "../markdown";
describe("padAligned", () => {
test("left-aligns: pads with spaces on right", () => {
const result = padAligned("hello", 5, 10, "left");
expect(result).toBe("hello ");
expect(result.length).toBe(10);
});
test("right-aligns: pads with spaces on left", () => {
const result = padAligned("hello", 5, 10, "right");
expect(result).toBe(" hello");
expect(result.length).toBe(10);
});
test("center-aligns: pads with spaces on both sides", () => {
const result = padAligned("hi", 2, 6, "center");
expect(result).toBe(" hi ");
expect(result.length).toBe(6);
});
test("no padding when displayWidth equals targetWidth", () => {
const result = padAligned("hello", 5, 5, "left");
expect(result).toBe("hello");
});
test("handles content wider than targetWidth", () => {
const result = padAligned("hello world", 11, 5, "left");
expect(result).toBe("hello world");
});
test("null/undefined align defaults to left", () => {
expect(padAligned("hi", 2, 5, null)).toBe("hi ");
expect(padAligned("hi", 2, 5, undefined)).toBe("hi ");
});
test("handles empty string content", () => {
const result = padAligned("", 0, 5, "center");
expect(result).toBe(" ");
});
test("handles zero displayWidth", () => {
const result = padAligned("", 0, 3, "left");
expect(result).toBe(" ");
});
test("handles zero targetWidth", () => {
const result = padAligned("hello", 5, 0, "left");
expect(result).toBe("hello");
});
test("center alignment with odd padding distribution", () => {
const result = padAligned("hi", 2, 7, "center");
expect(result).toBe(" hi ");
expect(result.length).toBe(7);
});
});

View File

@@ -0,0 +1,80 @@
import { describe, expect, test } from "bun:test";
// formatPrice and COST_TIER constants are pure data/functions from modelCost.ts
// We test the formatting logic directly to avoid the heavy import chain.
function formatPrice(price: number): string {
if (Number.isInteger(price)) {
return `$${price}`
}
return `$${price.toFixed(2)}`
}
// Mirrors formatModelPricing from modelCost.ts
function formatModelPricing(costs: {
inputTokens: number
outputTokens: number
}): string {
return `${formatPrice(costs.inputTokens)}/${formatPrice(costs.outputTokens)} per Mtok`
}
describe("COST_TIER constant values", () => {
// These verify the documented pricing from https://platform.claude.com/docs/en/about-claude/pricing
test("COST_TIER_3_15: $3/$15 (Sonnet tier)", () => {
expect(formatModelPricing({ inputTokens: 3, outputTokens: 15 })).toBe(
"$3/$15 per Mtok",
)
})
test("COST_TIER_15_75: $15/$75 (Opus 4/4.1 tier)", () => {
expect(formatModelPricing({ inputTokens: 15, outputTokens: 75 })).toBe(
"$15/$75 per Mtok",
)
})
test("COST_TIER_5_25: $5/$25 (Opus 4.5/4.6 tier)", () => {
expect(formatModelPricing({ inputTokens: 5, outputTokens: 25 })).toBe(
"$5/$25 per Mtok",
)
})
test("COST_TIER_30_150: $30/$150 (Fast Opus 4.6)", () => {
expect(formatModelPricing({ inputTokens: 30, outputTokens: 150 })).toBe(
"$30/$150 per Mtok",
)
})
test("COST_HAIKU_35: $0.80/$4 (Haiku 3.5)", () => {
expect(formatModelPricing({ inputTokens: 0.8, outputTokens: 4 })).toBe(
"$0.80/$4 per Mtok",
)
})
test("COST_HAIKU_45: $1/$5 (Haiku 4.5)", () => {
expect(formatModelPricing({ inputTokens: 1, outputTokens: 5 })).toBe(
"$1/$5 per Mtok",
)
})
})
describe("formatPrice", () => {
test("formats integers without decimals: 3 → '$3'", () => {
expect(formatPrice(3)).toBe("$3")
})
test("formats floats with 2 decimals: 0.8 → '$0.80'", () => {
expect(formatPrice(0.8)).toBe("$0.80")
})
test("formats large integers: 150 → '$150'", () => {
expect(formatPrice(150)).toBe("$150")
})
test("formats 1 as integer: '$1'", () => {
expect(formatPrice(1)).toBe("$1")
})
test("formats mixed decimal: 22.5 → '$22.50'", () => {
expect(formatPrice(22.5)).toBe("$22.50")
})
})

View File

@@ -0,0 +1,110 @@
import { afterEach, describe, expect, test } from "bun:test";
import {
getPrivacyLevel,
isEssentialTrafficOnly,
isTelemetryDisabled,
getEssentialTrafficOnlyReason,
} from "../privacyLevel";
describe("getPrivacyLevel", () => {
const originalDisableNonessential = process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC;
const originalDisableTelemetry = process.env.DISABLE_TELEMETRY;
afterEach(() => {
delete process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC;
delete process.env.DISABLE_TELEMETRY;
if (originalDisableNonessential !== undefined) {
process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = originalDisableNonessential;
}
if (originalDisableTelemetry !== undefined) {
process.env.DISABLE_TELEMETRY = originalDisableTelemetry;
}
});
test("returns 'default' when no env vars set", () => {
delete process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC;
delete process.env.DISABLE_TELEMETRY;
expect(getPrivacyLevel()).toBe("default");
});
test("returns 'essential-traffic' when CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC is set", () => {
process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1";
delete process.env.DISABLE_TELEMETRY;
expect(getPrivacyLevel()).toBe("essential-traffic");
});
test("returns 'no-telemetry' when DISABLE_TELEMETRY is set", () => {
delete process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC;
process.env.DISABLE_TELEMETRY = "1";
expect(getPrivacyLevel()).toBe("no-telemetry");
});
test("'essential-traffic' takes priority over 'no-telemetry'", () => {
process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1";
process.env.DISABLE_TELEMETRY = "1";
expect(getPrivacyLevel()).toBe("essential-traffic");
});
});
describe("isEssentialTrafficOnly", () => {
const original = process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC;
afterEach(() => {
delete process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC;
if (original !== undefined) process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = original;
});
test("returns true for 'essential-traffic' level", () => {
process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1";
expect(isEssentialTrafficOnly()).toBe(true);
});
test("returns false for 'default' level", () => {
delete process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC;
delete process.env.DISABLE_TELEMETRY;
expect(isEssentialTrafficOnly()).toBe(false);
});
test("returns false for 'no-telemetry' level", () => {
delete process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC;
process.env.DISABLE_TELEMETRY = "1";
expect(isEssentialTrafficOnly()).toBe(false);
});
});
describe("isTelemetryDisabled", () => {
afterEach(() => {
delete process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC;
delete process.env.DISABLE_TELEMETRY;
});
test("returns true for 'no-telemetry' level", () => {
process.env.DISABLE_TELEMETRY = "1";
expect(isTelemetryDisabled()).toBe(true);
});
test("returns true for 'essential-traffic' level", () => {
process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1";
expect(isTelemetryDisabled()).toBe(true);
});
test("returns false for 'default' level", () => {
expect(isTelemetryDisabled()).toBe(false);
});
});
describe("getEssentialTrafficOnlyReason", () => {
afterEach(() => {
delete process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC;
});
test("returns env var name when restricted", () => {
process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1";
expect(getEssentialTrafficOnlyReason()).toBe("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC");
});
test("returns null when unrestricted", () => {
delete process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC;
expect(getEssentialTrafficOnlyReason()).toBeNull();
});
});

View File

@@ -0,0 +1,48 @@
import { describe, expect, test } from "bun:test";
import { z } from "zod/v4";
import { semanticBoolean } from "../semanticBoolean";
describe("semanticBoolean", () => {
test("parses boolean true to true", () => {
expect(semanticBoolean().parse(true)).toBe(true);
});
test("parses boolean false to false", () => {
expect(semanticBoolean().parse(false)).toBe(false);
});
test("parses string 'true' to true", () => {
expect(semanticBoolean().parse("true")).toBe(true);
});
test("parses string 'false' to false", () => {
expect(semanticBoolean().parse("false")).toBe(false);
});
test("rejects string 'TRUE' (case-sensitive)", () => {
expect(() => semanticBoolean().parse("TRUE")).toThrow();
});
test("rejects string 'FALSE' (case-sensitive)", () => {
expect(() => semanticBoolean().parse("FALSE")).toThrow();
});
test("rejects number 1", () => {
expect(() => semanticBoolean().parse(1)).toThrow();
});
test("rejects null", () => {
expect(() => semanticBoolean().parse(null)).toThrow();
});
test("rejects undefined", () => {
expect(() => semanticBoolean().parse(undefined)).toThrow();
});
test("works with custom inner schema (z.boolean().optional())", () => {
const schema = semanticBoolean(z.boolean().optional());
expect(schema.parse(true)).toBe(true);
expect(schema.parse("false")).toBe(false);
expect(schema.parse(undefined)).toBeUndefined();
});
});

View File

@@ -0,0 +1,52 @@
import { describe, expect, test } from "bun:test";
import { z } from "zod/v4";
import { semanticNumber } from "../semanticNumber";
describe("semanticNumber", () => {
test("parses number 42", () => {
expect(semanticNumber().parse(42)).toBe(42);
});
test("parses number 0", () => {
expect(semanticNumber().parse(0)).toBe(0);
});
test("parses negative number -5", () => {
expect(semanticNumber().parse(-5)).toBe(-5);
});
test("parses float 3.14", () => {
expect(semanticNumber().parse(3.14)).toBeCloseTo(3.14);
});
test("parses string '42' to 42", () => {
expect(semanticNumber().parse("42")).toBe(42);
});
test("parses string '-7.5' to -7.5", () => {
expect(semanticNumber().parse("-7.5")).toBe(-7.5);
});
test("rejects string 'abc'", () => {
expect(() => semanticNumber().parse("abc")).toThrow();
});
test("rejects empty string ''", () => {
expect(() => semanticNumber().parse("")).toThrow();
});
test("rejects null", () => {
expect(() => semanticNumber().parse(null)).toThrow();
});
test("rejects boolean true", () => {
expect(() => semanticNumber().parse(true)).toThrow();
});
test("works with custom inner schema (z.number().int().min(0))", () => {
const schema = semanticNumber(z.number().int().min(0));
expect(schema.parse(5)).toBe(5);
expect(schema.parse("10")).toBe(10);
expect(() => schema.parse(-1)).toThrow();
});
});

View File

@@ -0,0 +1,99 @@
import { describe, expect, test } from "bun:test";
import { sequential } from "../sequential";
describe("sequential", () => {
test("wraps async function, returns same result", async () => {
const fn = sequential(async (x: number) => x * 2);
expect(await fn(5)).toBe(10);
});
test("single call resolves normally", async () => {
const fn = sequential(async () => "ok");
expect(await fn()).toBe("ok");
});
test("concurrent calls execute sequentially (FIFO order)", async () => {
const order: number[] = [];
const fn = sequential(async (n: number) => {
order.push(n);
await new Promise(r => setTimeout(r, 10));
return n;
});
const results = await Promise.all([fn(1), fn(2), fn(3)]);
expect(results).toEqual([1, 2, 3]);
expect(order).toEqual([1, 2, 3]);
});
test("preserves arguments correctly", async () => {
const fn = sequential(async (a: number, b: string) => `${a}-${b}`);
expect(await fn(42, "test")).toBe("42-test");
});
test("error in first call does not block subsequent calls", async () => {
let callCount = 0;
const fn = sequential(async () => {
callCount++;
if (callCount === 1) throw new Error("first fail");
return "ok";
});
await expect(fn()).rejects.toThrow("first fail");
expect(await fn()).toBe("ok");
});
test("preserves rejection reason", async () => {
const fn = sequential(async () => {
throw new Error("specific error");
});
await expect(fn()).rejects.toThrow("specific error");
});
test("multiple args passed correctly", async () => {
const fn = sequential(async (a: number, b: number, c: number) => a + b + c);
expect(await fn(1, 2, 3)).toBe(6);
});
test("returns different wrapper for each call to sequential", () => {
const fn1 = sequential(async () => 1);
const fn2 = sequential(async () => 2);
expect(fn1).not.toBe(fn2);
});
test("handles rapid concurrent calls", async () => {
const order: number[] = [];
const fn = sequential(async (n: number) => {
order.push(n);
return n;
});
const promises = Array.from({ length: 10 }, (_, i) => fn(i));
const results = await Promise.all(promises);
expect(results).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
expect(order).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
});
test("execution order matches call order", async () => {
const log: string[] = [];
const fn = sequential(async (label: string) => {
log.push(`start:${label}`);
await new Promise(r => setTimeout(r, 5));
log.push(`end:${label}`);
return label;
});
await Promise.all([fn("a"), fn("b")]);
expect(log[0]).toBe("start:a");
expect(log[1]).toBe("end:a");
expect(log[2]).toBe("start:b");
expect(log[3]).toBe("end:b");
});
test("works with functions returning different types", async () => {
const fn = sequential(async (x: number): string | number => {
return x > 0 ? "positive" : x;
});
expect(await fn(5)).toBe("positive");
expect(await fn(-1)).toBe(-1);
});
});

View File

@@ -0,0 +1,131 @@
import { describe, expect, test } from "bun:test";
import { segmentTextByHighlights, type TextHighlight } from "../textHighlighting";
describe("segmentTextByHighlights", () => {
// Basic
test("returns single segment with no highlights", () => {
const segments = segmentTextByHighlights("hello world", []);
expect(segments).toHaveLength(1);
expect(segments[0].text).toBe("hello world");
expect(segments[0].highlight).toBeUndefined();
});
test("returns highlighted segment for single highlight", () => {
const highlights: TextHighlight[] = [
{ start: 0, end: 5, color: undefined, priority: 0 },
];
const segments = segmentTextByHighlights("hello world", highlights);
expect(segments.length).toBeGreaterThanOrEqual(2);
expect(segments.some(s => s.highlight !== undefined)).toBe(true);
});
test("returns three segments for highlight in the middle", () => {
const highlights: TextHighlight[] = [
{ start: 3, end: 7, color: undefined, priority: 0 },
];
const segments = segmentTextByHighlights("hello world", highlights);
expect(segments.length).toBeGreaterThanOrEqual(2);
});
test("highlight covering entire text", () => {
const highlights: TextHighlight[] = [
{ start: 0, end: 5, color: undefined, priority: 0 },
];
const segments = segmentTextByHighlights("hello", highlights);
expect(segments).toHaveLength(1);
expect(segments[0].highlight).toBeDefined();
});
// Multiple highlights
test("handles non-overlapping highlights", () => {
const highlights: TextHighlight[] = [
{ start: 0, end: 3, color: undefined, priority: 0 },
{ start: 6, end: 9, color: undefined, priority: 0 },
];
const segments = segmentTextByHighlights("abcXYZdef", highlights);
const highlighted = segments.filter(s => s.highlight);
expect(highlighted.length).toBe(2);
});
test("handles overlapping highlights (priority-based)", () => {
const highlights: TextHighlight[] = [
{ start: 0, end: 5, color: undefined, priority: 0 },
{ start: 3, end: 8, color: undefined, priority: 1 },
];
const segments = segmentTextByHighlights("hello world", highlights);
// Overlapping: higher priority wins or they don't overlap
expect(segments.length).toBeGreaterThan(0);
});
test("handles adjacent highlights", () => {
const highlights: TextHighlight[] = [
{ start: 0, end: 3, color: undefined, priority: 0 },
{ start: 3, end: 6, color: undefined, priority: 0 },
];
const segments = segmentTextByHighlights("abcdef", highlights);
const highlighted = segments.filter(s => s.highlight);
expect(highlighted.length).toBe(2);
});
// Boundary
test("highlight starting at 0", () => {
const highlights: TextHighlight[] = [
{ start: 0, end: 3, color: undefined, priority: 0 },
];
const segments = segmentTextByHighlights("abcdef", highlights);
expect(segments[0].start).toBe(0);
});
test("highlight ending at text length", () => {
const text = "hello";
const highlights: TextHighlight[] = [
{ start: 3, end: 5, color: undefined, priority: 0 },
];
const segments = segmentTextByHighlights(text, highlights);
expect(segments.length).toBeGreaterThan(0);
});
test("empty highlights array returns single segment", () => {
const segments = segmentTextByHighlights("text", []);
expect(segments).toHaveLength(1);
expect(segments[0].highlight).toBeUndefined();
});
// Properties
test("preserves highlight color property", () => {
const highlights: TextHighlight[] = [
{ start: 0, end: 3, color: "primary" as any, priority: 0 },
];
const segments = segmentTextByHighlights("abc", highlights);
const highlighted = segments.find(s => s.highlight);
expect(highlighted?.highlight?.color).toBe("primary");
});
test("preserves highlight priority property", () => {
const highlights: TextHighlight[] = [
{ start: 0, end: 3, color: undefined, priority: 5 },
];
const segments = segmentTextByHighlights("abc", highlights);
const highlighted = segments.find(s => s.highlight);
expect(highlighted?.highlight?.priority).toBe(5);
});
test("preserves dimColor and inverse flags", () => {
const highlights: TextHighlight[] = [
{ start: 0, end: 3, color: undefined, priority: 0, dimColor: true, inverse: true },
];
const segments = segmentTextByHighlights("abc", highlights);
const highlighted = segments.find(s => s.highlight);
expect(highlighted?.highlight?.dimColor).toBe(true);
expect(highlighted?.highlight?.inverse).toBe(true);
});
test("highlights with start === end are skipped", () => {
const highlights: TextHighlight[] = [
{ start: 3, end: 3, color: undefined, priority: 0 },
];
const segments = segmentTextByHighlights("abcdef", highlights);
expect(segments).toHaveLength(1);
expect(segments[0].highlight).toBeUndefined();
});
});

View File

@@ -0,0 +1,76 @@
import { describe, expect, test } from "bun:test";
import {
matchesNegativeKeyword,
matchesKeepGoingKeyword,
} from "../userPromptKeywords";
describe("matchesNegativeKeyword", () => {
test("matches 'wtf'", () => {
expect(matchesNegativeKeyword("wtf is going on")).toBe(true);
});
test("matches 'shit'", () => {
expect(matchesNegativeKeyword("this is shit")).toBe(true);
});
test("matches 'fucking broken'", () => {
expect(matchesNegativeKeyword("this is fucking broken")).toBe(true);
});
test("does not match normal input like 'fix the bug'", () => {
expect(matchesNegativeKeyword("fix the bug")).toBe(false);
});
test("is case-insensitive", () => {
expect(matchesNegativeKeyword("WTF is this")).toBe(true);
expect(matchesNegativeKeyword("This Sucks")).toBe(true);
});
test("matches partial word in sentence", () => {
expect(matchesNegativeKeyword("please help, damn it")).toBe(true);
});
});
describe("matchesKeepGoingKeyword", () => {
test("matches exact 'continue'", () => {
expect(matchesKeepGoingKeyword("continue")).toBe(true);
});
test("matches 'keep going'", () => {
expect(matchesKeepGoingKeyword("keep going")).toBe(true);
});
test("matches 'go on'", () => {
expect(matchesKeepGoingKeyword("go on")).toBe(true);
});
test("does not match 'cont'", () => {
expect(matchesKeepGoingKeyword("cont")).toBe(false);
});
test("does not match empty string", () => {
expect(matchesKeepGoingKeyword("")).toBe(false);
});
test("matches within larger sentence 'please continue'", () => {
// 'continue' must be the entire prompt (lowercased), not a substring
expect(matchesKeepGoingKeyword("please continue")).toBe(false);
});
test("matches 'keep going' in sentence", () => {
expect(matchesKeepGoingKeyword("please keep going")).toBe(true);
});
test("matches 'go on' in sentence", () => {
expect(matchesKeepGoingKeyword("yes, go on")).toBe(true);
});
test("is case-insensitive for 'continue'", () => {
expect(matchesKeepGoingKeyword("Continue")).toBe(true);
expect(matchesKeepGoingKeyword("CONTINUE")).toBe(true);
});
test("is case-insensitive for 'keep going'", () => {
expect(matchesKeepGoingKeyword("Keep Going")).toBe(true);
});
});

View File

@@ -0,0 +1,58 @@
import { describe, expect, test } from "bun:test";
import { withResolvers } from "../withResolvers";
describe("withResolvers", () => {
test("returns object with promise, resolve, reject", () => {
const result = withResolvers<string>();
expect(result).toHaveProperty("promise");
expect(result).toHaveProperty("resolve");
expect(result).toHaveProperty("reject");
expect(typeof result.resolve).toBe("function");
expect(typeof result.reject).toBe("function");
});
test("promise resolves when resolve is called", async () => {
const { promise, resolve } = withResolvers<string>();
resolve("hello");
const result = await promise;
expect(result).toBe("hello");
});
test("promise rejects when reject is called", async () => {
const { promise, reject } = withResolvers<string>();
reject(new Error("fail"));
await expect(promise).rejects.toThrow("fail");
});
test("resolve passes value through", async () => {
const { promise, resolve } = withResolvers<number>();
resolve(42);
expect(await promise).toBe(42);
});
test("reject passes error through", async () => {
const { promise, reject } = withResolvers<void>();
const err = new Error("custom error");
reject(err);
await expect(promise).rejects.toBe(err);
});
test("promise is instanceof Promise", () => {
const { promise } = withResolvers<void>();
expect(promise).toBeInstanceOf(Promise);
});
test("works with generic type parameter", async () => {
const { promise, resolve } = withResolvers<{ name: string }>();
resolve({ name: "test" });
const result = await promise;
expect(result.name).toBe("test");
});
test("resolve/reject can be called asynchronously", async () => {
const { promise, resolve } = withResolvers<number>();
setTimeout(() => resolve(99), 10);
const result = await promise;
expect(result).toBe(99);
});
});

View File

@@ -0,0 +1,84 @@
import { describe, expect, test } from "bun:test";
import {
getXDGStateHome,
getXDGCacheHome,
getXDGDataHome,
getUserBinDir,
} from "../xdg";
describe("getXDGStateHome", () => {
test("returns ~/.local/state by default", () => {
const result = getXDGStateHome({ homedir: "/home/user" });
expect(result).toBe("/home/user/.local/state");
});
test("respects XDG_STATE_HOME env var", () => {
const result = getXDGStateHome({
homedir: "/home/user",
env: { XDG_STATE_HOME: "/custom/state" },
});
expect(result).toBe("/custom/state");
});
test("uses custom homedir from options", () => {
const result = getXDGStateHome({ homedir: "/opt/home" });
expect(result).toBe("/opt/home/.local/state");
});
});
describe("getXDGCacheHome", () => {
test("returns ~/.cache by default", () => {
const result = getXDGCacheHome({ homedir: "/home/user" });
expect(result).toBe("/home/user/.cache");
});
test("respects XDG_CACHE_HOME env var", () => {
const result = getXDGCacheHome({
homedir: "/home/user",
env: { XDG_CACHE_HOME: "/tmp/cache" },
});
expect(result).toBe("/tmp/cache");
});
});
describe("getXDGDataHome", () => {
test("returns ~/.local/share by default", () => {
const result = getXDGDataHome({ homedir: "/home/user" });
expect(result).toBe("/home/user/.local/share");
});
test("respects XDG_DATA_HOME env var", () => {
const result = getXDGDataHome({
homedir: "/home/user",
env: { XDG_DATA_HOME: "/custom/data" },
});
expect(result).toBe("/custom/data");
});
});
describe("getUserBinDir", () => {
test("returns ~/.local/bin", () => {
const result = getUserBinDir({ homedir: "/home/user" });
expect(result).toBe("/home/user/.local/bin");
});
test("uses custom homedir from options", () => {
const result = getUserBinDir({ homedir: "/opt/me" });
expect(result).toBe("/opt/me/.local/bin");
});
});
describe("path construction", () => {
test("all paths end with correct subdirectory", () => {
const home = "/home/test";
expect(getXDGStateHome({ homedir: home })).toMatch(/\.local\/state$/);
expect(getXDGCacheHome({ homedir: home })).toMatch(/\.cache$/);
expect(getXDGDataHome({ homedir: home })).toMatch(/\.local\/share$/);
expect(getUserBinDir({ homedir: home })).toMatch(/\.local\/bin$/);
});
test("respects HOME via homedir override", () => {
const result = getXDGStateHome({ homedir: "/Users/me" });
expect(result).toBe("/Users/me/.local/state");
});
});