test: Phase 5 — 添加 12 个测试文件 (+209 tests, 1177 total)

新增覆盖: effort, tokenBudget, displayTags, taggedId,
controlMessageCompat, MCP normalization/envExpansion,
gitConfigParser, formatBriefTimestamp, hyperlink, windowsPaths, notebook

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
claude-code-best
2026-04-02 10:11:43 +08:00
parent 28e40ddc67
commit 4f323efb61
13 changed files with 1560 additions and 2 deletions

View File

@@ -0,0 +1,139 @@
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
import { expandEnvVarsInString } from "../envExpansion";
describe("expandEnvVarsInString", () => {
// Save and restore env vars touched by tests
const savedEnv: Record<string, string | undefined> = {};
const trackedKeys = [
"TEST_HOME",
"MISSING",
"TEST_A",
"TEST_B",
"TEST_EMPTY",
"TEST_X",
"VAR",
"TEST_FOUND",
];
beforeEach(() => {
for (const key of trackedKeys) {
savedEnv[key] = process.env[key];
}
});
afterEach(() => {
for (const key of trackedKeys) {
if (savedEnv[key] === undefined) {
delete process.env[key];
} else {
process.env[key] = savedEnv[key];
}
}
});
test("expands a single env var that exists", () => {
process.env.TEST_HOME = "/home/user";
const result = expandEnvVarsInString("${TEST_HOME}");
expect(result.expanded).toBe("/home/user");
expect(result.missingVars).toEqual([]);
});
test("returns original placeholder and tracks missing var when not found", () => {
delete process.env.MISSING;
const result = expandEnvVarsInString("${MISSING}");
expect(result.expanded).toBe("${MISSING}");
expect(result.missingVars).toEqual(["MISSING"]);
});
test("uses default value when var is missing and default is provided", () => {
delete process.env.MISSING;
const result = expandEnvVarsInString("${MISSING:-fallback}");
expect(result.expanded).toBe("fallback");
expect(result.missingVars).toEqual([]);
});
test("expands multiple vars", () => {
process.env.TEST_A = "hello";
process.env.TEST_B = "world";
const result = expandEnvVarsInString("${TEST_A}/${TEST_B}");
expect(result.expanded).toBe("hello/world");
expect(result.missingVars).toEqual([]);
});
test("handles mix of found and missing vars", () => {
process.env.TEST_FOUND = "yes";
delete process.env.MISSING;
const result = expandEnvVarsInString("${TEST_FOUND}-${MISSING}");
expect(result.expanded).toBe("yes-${MISSING}");
expect(result.missingVars).toEqual(["MISSING"]);
});
test("returns plain string unchanged with empty missingVars", () => {
const result = expandEnvVarsInString("plain string");
expect(result.expanded).toBe("plain string");
expect(result.missingVars).toEqual([]);
});
test("expands empty env var value", () => {
process.env.TEST_EMPTY = "";
const result = expandEnvVarsInString("${TEST_EMPTY}");
expect(result.expanded).toBe("");
expect(result.missingVars).toEqual([]);
});
test("prefers env var value over default when var exists", () => {
process.env.TEST_X = "real";
const result = expandEnvVarsInString("${TEST_X:-default}");
expect(result.expanded).toBe("real");
expect(result.missingVars).toEqual([]);
});
test("handles default value containing colons", () => {
// split(':-', 2) means only the first :- is the delimiter
delete process.env.TEST_X;
const result = expandEnvVarsInString("${TEST_X:-value:-with:-colons}");
// The default is "value" because split(':-', 2) gives ["TEST_X", "value"]
// Wait -- actually split(':-', 2) on "TEST_X:-value:-with:-colons" gives:
// ["TEST_X", "value"] because limit=2 stops at 2 pieces
expect(result.expanded).toBe("value");
expect(result.missingVars).toEqual([]);
});
test("handles nested-looking syntax as literal (not supported)", () => {
// ${${VAR}} - the regex [^}]+ matches "${VAR" (up to first })
// so varName would be "${VAR" which won't be found in env
delete process.env.VAR;
const result = expandEnvVarsInString("${${VAR}}");
// The regex \$\{([^}]+)\} matches "${${VAR}" with capture "${VAR"
// That env var won't exist, so it stays as "${${VAR}" + remaining "}"
expect(result.missingVars).toEqual(["${VAR"]);
expect(result.expanded).toBe("${${VAR}}");
});
test("handles empty string input", () => {
const result = expandEnvVarsInString("");
expect(result.expanded).toBe("");
expect(result.missingVars).toEqual([]);
});
test("handles var surrounded by text", () => {
process.env.TEST_A = "middle";
const result = expandEnvVarsInString("before-${TEST_A}-after");
expect(result.expanded).toBe("before-middle-after");
expect(result.missingVars).toEqual([]);
});
test("handles default value that is empty string", () => {
delete process.env.MISSING;
const result = expandEnvVarsInString("${MISSING:-}");
expect(result.expanded).toBe("");
expect(result.missingVars).toEqual([]);
});
test("does not expand $VAR without braces", () => {
process.env.TEST_A = "value";
const result = expandEnvVarsInString("$TEST_A");
expect(result.expanded).toBe("$TEST_A");
expect(result.missingVars).toEqual([]);
});
});

View File

@@ -0,0 +1,59 @@
import { describe, expect, test } from "bun:test";
import { normalizeNameForMCP } from "../normalization";
describe("normalizeNameForMCP", () => {
test("returns simple valid name unchanged", () => {
expect(normalizeNameForMCP("my-server")).toBe("my-server");
});
test("replaces dots with underscores", () => {
expect(normalizeNameForMCP("my.server.name")).toBe("my_server_name");
});
test("replaces spaces with underscores", () => {
expect(normalizeNameForMCP("my server")).toBe("my_server");
});
test("replaces special characters with underscores", () => {
expect(normalizeNameForMCP("server@v2!")).toBe("server_v2_");
});
test("returns already valid name unchanged", () => {
expect(normalizeNameForMCP("valid_name-123")).toBe("valid_name-123");
});
test("returns empty string for empty input", () => {
expect(normalizeNameForMCP("")).toBe("");
});
test("handles claude.ai prefix: collapses consecutive underscores and strips edges", () => {
// "claude.ai My Server" -> replace invalid -> "claude_ai_My_Server"
// starts with "claude.ai " so collapse + strip -> "claude_ai_My_Server"
expect(normalizeNameForMCP("claude.ai My Server")).toBe(
"claude_ai_My_Server"
);
});
test("handles claude.ai prefix with consecutive invalid chars", () => {
// "claude.ai ...test..." -> replace invalid -> "claude_ai____test___"
// collapse consecutive _ -> "claude_ai_test_"
// strip leading/trailing _ -> "claude_ai_test"
expect(normalizeNameForMCP("claude.ai ...test...")).toBe("claude_ai_test");
});
test("non-claude.ai name preserves consecutive underscores", () => {
// "a..b" -> "a__b", no claude.ai prefix so no collapse
expect(normalizeNameForMCP("a..b")).toBe("a__b");
});
test("non-claude.ai name preserves trailing underscores", () => {
expect(normalizeNameForMCP("name!")).toBe("name_");
});
test("handles claude.ai prefix that results in only underscores", () => {
// "claude.ai ..." -> replace invalid -> "claude_ai____"
// collapse -> "claude_ai_"
// strip trailing -> "claude_ai"
expect(normalizeNameForMCP("claude.ai ...")).toBe("claude_ai");
});
});

View File

@@ -0,0 +1,103 @@
import { describe, expect, test } from "bun:test";
import { normalizeControlMessageKeys } from "../controlMessageCompat";
describe("normalizeControlMessageKeys", () => {
// --- basic camelCase to snake_case ---
test("converts requestId to request_id", () => {
const obj = { requestId: "123" };
const result = normalizeControlMessageKeys(obj);
expect(result).toEqual({ request_id: "123" });
expect((result as any).requestId).toBeUndefined();
});
test("leaves request_id unchanged", () => {
const obj = { request_id: "123" };
normalizeControlMessageKeys(obj);
expect(obj).toEqual({ request_id: "123" });
});
// --- both present: snake_case wins ---
test("keeps snake_case when both requestId and request_id exist", () => {
const obj = { requestId: "camel", request_id: "snake" };
const result = normalizeControlMessageKeys(obj) as any;
expect(result.request_id).toBe("snake");
// requestId is NOT deleted when request_id already exists
// because the condition `!('request_id' in record)` prevents the branch
expect(result.requestId).toBe("camel");
});
// --- nested response ---
test("normalizes nested response.requestId", () => {
const obj = { response: { requestId: "456" } };
normalizeControlMessageKeys(obj);
expect((obj as any).response.request_id).toBe("456");
expect((obj as any).response.requestId).toBeUndefined();
});
test("leaves nested response.request_id unchanged", () => {
const obj = { response: { request_id: "789" } };
normalizeControlMessageKeys(obj);
expect((obj as any).response.request_id).toBe("789");
});
test("nested response: snake_case wins when both present", () => {
const obj = {
response: { requestId: "camel", request_id: "snake" },
};
normalizeControlMessageKeys(obj);
expect((obj as any).response.request_id).toBe("snake");
expect((obj as any).response.requestId).toBe("camel");
});
// --- non-object inputs ---
test("returns null as-is", () => {
expect(normalizeControlMessageKeys(null)).toBeNull();
});
test("returns undefined as-is", () => {
expect(normalizeControlMessageKeys(undefined)).toBeUndefined();
});
test("returns string as-is", () => {
expect(normalizeControlMessageKeys("hello")).toBe("hello");
});
test("returns number as-is", () => {
expect(normalizeControlMessageKeys(42)).toBe(42);
});
// --- empty and edge cases ---
test("empty object is unchanged", () => {
const obj = {};
normalizeControlMessageKeys(obj);
expect(obj).toEqual({});
});
test("mutates the original object in place", () => {
const obj = { requestId: "abc", other: "data" };
const result = normalizeControlMessageKeys(obj);
expect(result).toBe(obj); // same reference
expect(obj).toEqual({ request_id: "abc", other: "data" });
});
test("does not affect other keys on the object", () => {
const obj = { requestId: "123", type: "control_request", payload: {} };
normalizeControlMessageKeys(obj);
expect((obj as any).type).toBe("control_request");
expect((obj as any).payload).toEqual({});
expect((obj as any).request_id).toBe("123");
});
test("handles response being null", () => {
const obj = { response: null, requestId: "x" };
normalizeControlMessageKeys(obj);
expect((obj as any).request_id).toBe("x");
expect((obj as any).response).toBeNull();
});
test("handles response being a non-object (string)", () => {
const obj = { response: "not-an-object" };
normalizeControlMessageKeys(obj);
expect((obj as any).response).toBe("not-an-object");
});
});

View File

@@ -0,0 +1,134 @@
import { describe, expect, test } from "bun:test";
import {
stripDisplayTags,
stripDisplayTagsAllowEmpty,
stripIdeContextTags,
} from "../displayTags";
describe("stripDisplayTags", () => {
test("strips a single system tag and returns remaining text", () => {
expect(
stripDisplayTags("<system-reminder>secret stuff</system-reminder>text")
).toBe("text");
});
test("strips multiple tags and preserves text between them", () => {
const input =
"<hook-output>data</hook-output>hello <task-info>info</task-info>world";
expect(stripDisplayTags(input)).toBe("hello world");
});
test("preserves uppercase JSX component names", () => {
expect(stripDisplayTags("fix the <Button> layout")).toBe(
"fix the <Button> layout"
);
});
test("preserves angle brackets in prose (when x < y)", () => {
expect(stripDisplayTags("when x < y")).toBe("when x < y");
});
test("preserves DOCTYPE declarations", () => {
expect(stripDisplayTags("<!DOCTYPE html>")).toBe("<!DOCTYPE html>");
});
test("returns original text when stripping would result in empty", () => {
const input = "<system-reminder>all tags</system-reminder>";
expect(stripDisplayTags(input)).toBe(input);
});
test("strips tags with attributes", () => {
expect(
stripDisplayTags('<context type="ide">data</context>hello')
).toBe("hello");
});
test("handles multi-line tag content", () => {
const input = "<info>\nline1\nline2\n</info>remaining";
expect(stripDisplayTags(input)).toBe("remaining");
});
test("returns trimmed result", () => {
expect(
stripDisplayTags(" <tag>content</tag> hello ")
).toBe("hello");
});
test("handles empty string input", () => {
// Empty string is falsy, so stripDisplayTags returns original
expect(stripDisplayTags("")).toBe("");
});
test("handles whitespace-only input", () => {
// After trim, result is empty string which is falsy, returns original
expect(stripDisplayTags(" ")).toBe(" ");
});
});
describe("stripDisplayTagsAllowEmpty", () => {
test("returns empty string when all content is tags", () => {
expect(
stripDisplayTagsAllowEmpty("<system-reminder>stuff</system-reminder>")
).toBe("");
});
test("strips tags and returns remaining text", () => {
expect(
stripDisplayTagsAllowEmpty("<tag>content</tag>hello")
).toBe("hello");
});
test("returns empty string for empty input", () => {
expect(stripDisplayTagsAllowEmpty("")).toBe("");
});
test("returns empty string for whitespace-only content after strip", () => {
expect(
stripDisplayTagsAllowEmpty("<tag>content</tag> ")
).toBe("");
});
});
describe("stripIdeContextTags", () => {
test("strips ide_opened_file tags", () => {
expect(
stripIdeContextTags(
"<ide_opened_file>path/to/file.ts</ide_opened_file>hello"
)
).toBe("hello");
});
test("strips ide_selection tags", () => {
expect(
stripIdeContextTags("<ide_selection>selected code</ide_selection>world")
).toBe("world");
});
test("strips ide tags with attributes", () => {
expect(
stripIdeContextTags(
'<ide_opened_file path="foo.ts">content</ide_opened_file>text'
)
).toBe("text");
});
test("preserves other lowercase tags", () => {
expect(
stripIdeContextTags("<system-reminder>data</system-reminder>hello")
).toBe("<system-reminder>data</system-reminder>hello");
});
test("preserves user-typed HTML like <code>", () => {
expect(stripIdeContextTags("use <code>foo</code> here")).toBe(
"use <code>foo</code> here"
);
});
test("strips only IDE tags while preserving other tags and text", () => {
const input =
"<ide_opened_file>f.ts</ide_opened_file><system-reminder>x</system-reminder>text";
expect(stripIdeContextTags(input)).toBe(
"<system-reminder>x</system-reminder>text"
);
});
});

View File

@@ -0,0 +1,255 @@
import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test";
// Mock heavy dependencies to avoid import chain issues
mock.module("src/utils/thinking.js", () => ({
isUltrathinkEnabled: () => false,
}));
mock.module("src/utils/settings/settings.js", () => ({
getInitialSettings: () => ({}),
}));
mock.module("src/utils/auth.js", () => ({
isProSubscriber: () => false,
isMaxSubscriber: () => false,
isTeamSubscriber: () => false,
}));
mock.module("src/services/analytics/growthbook.js", () => ({
getFeatureValue_CACHED_MAY_BE_STALE: () => null,
}));
mock.module("src/utils/model/modelSupportOverrides.js", () => ({
get3PModelCapabilityOverride: () => undefined,
}));
const {
isEffortLevel,
parseEffortValue,
isValidNumericEffort,
convertEffortValueToLevel,
getEffortLevelDescription,
resolvePickerEffortPersistence,
EFFORT_LEVELS,
} = await import("src/utils/effort.js");
// ─── EFFORT_LEVELS constant ────────────────────────────────────────────
describe("EFFORT_LEVELS", () => {
test("contains the four canonical levels", () => {
expect(EFFORT_LEVELS).toEqual(["low", "medium", "high", "max"]);
});
});
// ─── isEffortLevel ─────────────────────────────────────────────────────
describe("isEffortLevel", () => {
test("returns true for 'low'", () => {
expect(isEffortLevel("low")).toBe(true);
});
test("returns true for 'medium'", () => {
expect(isEffortLevel("medium")).toBe(true);
});
test("returns true for 'high'", () => {
expect(isEffortLevel("high")).toBe(true);
});
test("returns true for 'max'", () => {
expect(isEffortLevel("max")).toBe(true);
});
test("returns false for 'invalid'", () => {
expect(isEffortLevel("invalid")).toBe(false);
});
test("returns false for empty string", () => {
expect(isEffortLevel("")).toBe(false);
});
});
// ─── parseEffortValue ──────────────────────────────────────────────────
describe("parseEffortValue", () => {
test("returns undefined for undefined", () => {
expect(parseEffortValue(undefined)).toBeUndefined();
});
test("returns undefined for null", () => {
expect(parseEffortValue(null)).toBeUndefined();
});
test("returns undefined for empty string", () => {
expect(parseEffortValue("")).toBeUndefined();
});
test("returns number for integer input", () => {
expect(parseEffortValue(42)).toBe(42);
});
test("returns string for valid effort level string", () => {
expect(parseEffortValue("low")).toBe("low");
expect(parseEffortValue("medium")).toBe("medium");
expect(parseEffortValue("high")).toBe("high");
expect(parseEffortValue("max")).toBe("max");
});
test("parses numeric string to number", () => {
expect(parseEffortValue("42")).toBe(42);
});
test("returns undefined for invalid string", () => {
expect(parseEffortValue("invalid")).toBeUndefined();
});
test("non-integer number falls through to string parsing (parseInt truncates)", () => {
// 3.14 fails isValidNumericEffort, then String(3.14) -> "3.14" -> parseInt = 3
expect(parseEffortValue(3.14)).toBe(3);
});
test("handles case-insensitive effort level strings", () => {
expect(parseEffortValue("LOW")).toBe("low");
expect(parseEffortValue("HIGH")).toBe("high");
});
});
// ─── isValidNumericEffort ──────────────────────────────────────────────
describe("isValidNumericEffort", () => {
test("returns true for integer", () => {
expect(isValidNumericEffort(50)).toBe(true);
});
test("returns true for zero", () => {
expect(isValidNumericEffort(0)).toBe(true);
});
test("returns true for negative integer", () => {
expect(isValidNumericEffort(-1)).toBe(true);
});
test("returns false for float", () => {
expect(isValidNumericEffort(3.14)).toBe(false);
});
test("returns false for NaN", () => {
expect(isValidNumericEffort(NaN)).toBe(false);
});
test("returns false for Infinity", () => {
expect(isValidNumericEffort(Infinity)).toBe(false);
});
});
// ─── convertEffortValueToLevel ─────────────────────────────────────────
describe("convertEffortValueToLevel", () => {
test("returns valid effort level string as-is", () => {
expect(convertEffortValueToLevel("low")).toBe("low");
expect(convertEffortValueToLevel("medium")).toBe("medium");
expect(convertEffortValueToLevel("high")).toBe("high");
expect(convertEffortValueToLevel("max")).toBe("max");
});
test("returns 'high' for unknown string", () => {
expect(convertEffortValueToLevel("unknown" as any)).toBe("high");
});
test("non-ant numeric value returns 'high'", () => {
const saved = process.env.USER_TYPE;
delete process.env.USER_TYPE;
expect(convertEffortValueToLevel(50)).toBe("high");
expect(convertEffortValueToLevel(100)).toBe("high");
process.env.USER_TYPE = saved;
});
describe("ant numeric mapping", () => {
let savedUserType: string | undefined;
beforeEach(() => {
savedUserType = process.env.USER_TYPE;
process.env.USER_TYPE = "ant";
});
afterEach(() => {
if (savedUserType === undefined) {
delete process.env.USER_TYPE;
} else {
process.env.USER_TYPE = savedUserType;
}
});
test("value <= 50 maps to 'low'", () => {
expect(convertEffortValueToLevel(50)).toBe("low");
expect(convertEffortValueToLevel(0)).toBe("low");
expect(convertEffortValueToLevel(-10)).toBe("low");
});
test("value 51-85 maps to 'medium'", () => {
expect(convertEffortValueToLevel(51)).toBe("medium");
expect(convertEffortValueToLevel(85)).toBe("medium");
});
test("value 86-100 maps to 'high'", () => {
expect(convertEffortValueToLevel(86)).toBe("high");
expect(convertEffortValueToLevel(100)).toBe("high");
});
test("value > 100 maps to 'max'", () => {
expect(convertEffortValueToLevel(101)).toBe("max");
expect(convertEffortValueToLevel(200)).toBe("max");
});
});
});
// ─── getEffortLevelDescription ─────────────────────────────────────────
describe("getEffortLevelDescription", () => {
test("returns description for 'low'", () => {
const desc = getEffortLevelDescription("low");
expect(desc).toContain("Quick");
});
test("returns description for 'medium'", () => {
const desc = getEffortLevelDescription("medium");
expect(desc).toContain("Balanced");
});
test("returns description for 'high'", () => {
const desc = getEffortLevelDescription("high");
expect(desc).toContain("Comprehensive");
});
test("returns description for 'max'", () => {
const desc = getEffortLevelDescription("max");
expect(desc).toContain("Maximum");
});
});
// ─── resolvePickerEffortPersistence ────────────────────────────────────
describe("resolvePickerEffortPersistence", () => {
test("returns undefined when picked matches model default and no prior persistence", () => {
const result = resolvePickerEffortPersistence("high", "high", undefined, false);
expect(result).toBeUndefined();
});
test("returns picked when it differs from model default", () => {
const result = resolvePickerEffortPersistence("low", "high", undefined, false);
expect(result).toBe("low");
});
test("returns picked when priorPersisted is set (even if same as default)", () => {
const result = resolvePickerEffortPersistence("high", "high", "high", false);
expect(result).toBe("high");
});
test("returns picked when toggledInPicker is true (even if same as default)", () => {
const result = resolvePickerEffortPersistence("high", "high", undefined, true);
expect(result).toBe("high");
});
test("returns undefined picked value when no explicit and matches default", () => {
const result = resolvePickerEffortPersistence(undefined, "high" as any, undefined, false);
expect(result).toBeUndefined();
});
});

View File

@@ -0,0 +1,76 @@
import { describe, expect, test } from "bun:test";
import { formatBriefTimestamp } from "../formatBriefTimestamp";
describe("formatBriefTimestamp", () => {
// Fixed "now" for deterministic tests: 2026-04-02T14:00:00Z (Thursday)
const now = new Date("2026-04-02T14:00:00Z");
test("same day timestamp returns time only (contains colon)", () => {
const result = formatBriefTimestamp("2026-04-02T10:30:00Z", now);
expect(result).toContain(":");
// Should NOT contain a weekday name since it's the same day
expect(result).not.toMatch(
/Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday/
);
});
test("yesterday returns weekday and time", () => {
// 2026-04-01 is Wednesday
const result = formatBriefTimestamp("2026-04-01T16:15:00Z", now);
expect(result).toContain("Wednesday");
expect(result).toContain(":");
});
test("3 days ago returns weekday and time", () => {
// 2026-03-30 is Monday
const result = formatBriefTimestamp("2026-03-30T09:00:00Z", now);
expect(result).toContain("Monday");
expect(result).toContain(":");
});
test("6 days ago returns weekday and time (still within 6-day window)", () => {
// 2026-03-27 is Friday
const result = formatBriefTimestamp("2026-03-27T12:00:00Z", now);
expect(result).toContain("Friday");
expect(result).toContain(":");
});
test("7+ days ago returns weekday, month, day, and time", () => {
// 2026-03-20 is Friday, 13 days ago
const result = formatBriefTimestamp("2026-03-20T14:30:00Z", now);
expect(result).toContain("Friday");
expect(result).toContain(":");
// Should contain month abbreviation (Mar)
expect(result).toMatch(/Mar/);
});
test("much older date returns full format with month", () => {
const result = formatBriefTimestamp("2025-12-25T08:00:00Z", now);
expect(result).toContain(":");
expect(result).toMatch(/Dec/);
});
test("invalid ISO string returns empty string", () => {
expect(formatBriefTimestamp("not-a-date", now)).toBe("");
});
test("empty string returns empty string", () => {
expect(formatBriefTimestamp("", now)).toBe("");
});
test("same day early morning returns time format", () => {
const result = formatBriefTimestamp("2026-04-02T01:05:00Z", now);
expect(result).toContain(":");
// Should be time-only format
expect(result.length).toBeLessThan(20);
});
test("uses current time as default when now is not provided", () => {
// Just verify it returns a non-empty string for a recent timestamp
const recent = new Date();
recent.setMinutes(recent.getMinutes() - 5);
const result = formatBriefTimestamp(recent.toISOString());
expect(result).not.toBe("");
expect(result).toContain(":");
});
});

View File

@@ -0,0 +1,99 @@
import { describe, expect, test } from "bun:test";
import { createHyperlink, OSC8_START, OSC8_END } from "../hyperlink";
// ─── OSC8 constants ────────────────────────────────────────────────────
describe("OSC8 constants", () => {
test("OSC8_START is the correct escape sequence", () => {
expect(OSC8_START).toBe("\x1b]8;;");
});
test("OSC8_END is the BEL character", () => {
expect(OSC8_END).toBe("\x07");
});
});
// ─── createHyperlink ───────────────────────────────────────────────────
describe("createHyperlink", () => {
test("supported + no content: wraps URL in OSC 8 with URL as display text", () => {
const url = "https://example.com";
const result = createHyperlink(url, undefined, { supportsHyperlinks: true });
expect(result).toContain(OSC8_START);
expect(result).toContain(OSC8_END);
// Structure: OSC8_START + url + OSC8_END + coloredText + OSC8_START + OSC8_END
expect(result).toStartWith(`${OSC8_START}${url}${OSC8_END}`);
expect(result).toEndWith(`${OSC8_START}${OSC8_END}`);
});
test("supported + content: shows content as link text", () => {
const url = "https://example.com";
const content = "click here";
const result = createHyperlink(url, content, { supportsHyperlinks: true });
expect(result).toStartWith(`${OSC8_START}${url}${OSC8_END}`);
expect(result).toContain("click here");
expect(result).toEndWith(`${OSC8_START}${OSC8_END}`);
});
test("not supported: returns plain URL regardless of content", () => {
const url = "https://example.com";
const result = createHyperlink(url, "some content", {
supportsHyperlinks: false,
});
expect(result).toBe(url);
});
test("not supported + no content: returns plain URL", () => {
const url = "https://example.com/path?q=1";
const result = createHyperlink(url, undefined, {
supportsHyperlinks: false,
});
expect(result).toBe(url);
});
test("URL with special characters works when supported", () => {
const url = "https://example.com/path?a=1&b=2#section";
const result = createHyperlink(url, undefined, { supportsHyperlinks: true });
expect(result).toStartWith(`${OSC8_START}${url}${OSC8_END}`);
expect(result).toEndWith(`${OSC8_START}${OSC8_END}`);
});
test("URL with special characters works when not supported", () => {
const url = "https://example.com/path?a=1&b=2#section";
const result = createHyperlink(url, undefined, {
supportsHyperlinks: false,
});
expect(result).toBe(url);
});
test("supported link text contains the display content", () => {
const result = createHyperlink("https://example.com", "text", {
supportsHyperlinks: true,
});
// The colored text portion is between the two OSC8 sequences
const inner = result.slice(
`${OSC8_START}https://example.com${OSC8_END}`.length,
result.length - `${OSC8_START}${OSC8_END}`.length
);
// chalk.blue may or may not emit ANSI depending on environment,
// but the display text must always be present
expect(inner).toContain("text");
});
test("empty content string is treated as display text when supported", () => {
const url = "https://example.com";
const result = createHyperlink(url, "", { supportsHyperlinks: true });
// Empty string is falsy, so displayText falls back to url
// Actually: content ?? url — "" is not null/undefined, so "" is used
expect(result).toStartWith(`${OSC8_START}${url}${OSC8_END}`);
expect(result).toEndWith(`${OSC8_START}${OSC8_END}`);
});
});

View File

@@ -0,0 +1,162 @@
import { describe, expect, test } from "bun:test";
import { parseCellId, mapNotebookCellsToToolResult } from "../notebook";
// ─── parseCellId ───────────────────────────────────────────────────────
describe("parseCellId", () => {
test("parses cell-0 to 0", () => {
expect(parseCellId("cell-0")).toBe(0);
});
test("parses cell-5 to 5", () => {
expect(parseCellId("cell-5")).toBe(5);
});
test("parses cell-100 to 100", () => {
expect(parseCellId("cell-100")).toBe(100);
});
test("returns undefined for cell- (no number)", () => {
expect(parseCellId("cell-")).toBeUndefined();
});
test("returns undefined for cell-abc (non-numeric)", () => {
expect(parseCellId("cell-abc")).toBeUndefined();
});
test("returns undefined for other-format", () => {
expect(parseCellId("other-format")).toBeUndefined();
});
test("returns undefined for empty string", () => {
expect(parseCellId("")).toBeUndefined();
});
test("returns undefined for prefix-only match like cell-0-extra", () => {
// regex is /^cell-(\d+)$/ so trailing text should fail
expect(parseCellId("cell-0-extra")).toBeUndefined();
});
});
// ─── mapNotebookCellsToToolResult ──────────────────────────────────────
describe("mapNotebookCellsToToolResult", () => {
test("returns tool result with correct tool_use_id", () => {
const data = [
{
cellType: "code",
source: 'print("hello")',
cell_id: "cell-0",
language: "python",
},
];
const result = mapNotebookCellsToToolResult(data, "tool-123");
expect(result.tool_use_id).toBe("tool-123");
expect(result.type).toBe("tool_result");
});
test("content array contains text blocks for cell content", () => {
const data = [
{
cellType: "code",
source: 'x = 1',
cell_id: "cell-0",
language: "python",
},
];
const result = mapNotebookCellsToToolResult(data, "tool-1");
expect(result.content).toBeInstanceOf(Array);
expect(result.content!.length).toBeGreaterThanOrEqual(1);
const firstBlock = result.content![0] as { type: string; text: string };
expect(firstBlock.type).toBe("text");
expect(firstBlock.text).toContain("cell-0");
expect(firstBlock.text).toContain("x = 1");
});
test("merges adjacent text blocks from multiple cells", () => {
const data = [
{
cellType: "code",
source: "a = 1",
cell_id: "cell-0",
language: "python",
},
{
cellType: "code",
source: "b = 2",
cell_id: "cell-1",
language: "python",
},
];
const result = mapNotebookCellsToToolResult(data, "tool-2");
// Two adjacent text blocks should be merged into one
const textBlocks = result.content!.filter(
(b: any) => b.type === "text"
);
expect(textBlocks).toHaveLength(1);
});
test("preserves image blocks without merging", () => {
const data = [
{
cellType: "code",
source: "plot()",
cell_id: "cell-0",
language: "python",
outputs: [
{
output_type: "display_data",
text: "",
image: {
image_data: "iVBORw0KGgo=",
media_type: "image/png" as const,
},
},
],
},
{
cellType: "code",
source: "print(1)",
cell_id: "cell-1",
language: "python",
},
];
const result = mapNotebookCellsToToolResult(data, "tool-3");
const types = result.content!.map((b: any) => b.type);
expect(types).toContain("image");
});
test("markdown cell includes cell_type metadata", () => {
const data = [
{
cellType: "markdown",
source: "# Title",
cell_id: "cell-0",
},
];
const result = mapNotebookCellsToToolResult(data, "tool-4");
const textBlock = result.content![0] as { type: string; text: string };
expect(textBlock.text).toContain("<cell_type>markdown</cell_type>");
});
test("non-python code cell includes language metadata", () => {
const data = [
{
cellType: "code",
source: "val x = 1",
cell_id: "cell-0",
language: "scala",
},
];
const result = mapNotebookCellsToToolResult(data, "tool-5");
const textBlock = result.content![0] as { type: string; text: string };
expect(textBlock.text).toContain("<language>scala</language>");
});
});

View File

@@ -0,0 +1,104 @@
import { describe, expect, test } from "bun:test";
import { toTaggedId } from "../taggedId";
const BASE_58_CHARS =
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
describe("toTaggedId", () => {
test("zero UUID produces all base58 '1's (first char)", () => {
const result = toTaggedId(
"user",
"00000000-0000-0000-0000-000000000000"
);
// base58 of 0 is all '1's (the first base58 character)
expect(result).toBe("user_01" + "1".repeat(22));
});
test("format is tag_01 + 22 base58 chars", () => {
const result = toTaggedId(
"user",
"550e8400-e29b-41d4-a716-446655440000"
);
expect(result).toMatch(
new RegExp(`^user_01[${BASE_58_CHARS.replace(/[-]/g, "\\-")}]{22}$`)
);
});
test("output starts with the provided tag", () => {
const result = toTaggedId("org", "550e8400-e29b-41d4-a716-446655440000");
expect(result.startsWith("org_01")).toBe(true);
});
test("UUID with hyphens equals UUID without hyphens", () => {
const withHyphens = toTaggedId(
"user",
"550e8400-e29b-41d4-a716-446655440000"
);
const withoutHyphens = toTaggedId(
"user",
"550e8400e29b41d4a716446655440000"
);
expect(withHyphens).toBe(withoutHyphens);
});
test("different tags produce different prefixes", () => {
const uuid = "550e8400-e29b-41d4-a716-446655440000";
const userResult = toTaggedId("user", uuid);
const orgResult = toTaggedId("org", uuid);
const msgResult = toTaggedId("msg", uuid);
// They share the same base58 suffix but different prefixes
expect(userResult.slice(userResult.indexOf("_01") + 3)).toBe(
orgResult.slice(orgResult.indexOf("_01") + 3)
);
expect(userResult).not.toBe(orgResult);
expect(orgResult).not.toBe(msgResult);
});
test("different UUIDs produce different encoded parts", () => {
const result1 = toTaggedId(
"user",
"550e8400-e29b-41d4-a716-446655440000"
);
const result2 = toTaggedId(
"user",
"661f9500-f3ac-52e5-b827-557766550111"
);
expect(result1).not.toBe(result2);
});
test("encoded part is always exactly 22 characters", () => {
const uuids = [
"00000000-0000-0000-0000-000000000000",
"ffffffff-ffff-ffff-ffff-ffffffffffff",
"550e8400-e29b-41d4-a716-446655440000",
"00000000-0000-0000-0000-000000000001",
];
for (const uuid of uuids) {
const result = toTaggedId("test", uuid);
const encoded = result.slice("test_01".length);
expect(encoded).toHaveLength(22);
}
});
test("throws on invalid UUID (too short)", () => {
expect(() => toTaggedId("user", "abcdef")).toThrow("Invalid UUID hex length");
});
test("throws on invalid UUID (too long)", () => {
expect(() =>
toTaggedId("user", "550e8400e29b41d4a716446655440000ff")
).toThrow("Invalid UUID hex length");
});
test("max UUID (all f's) produces valid base58 output", () => {
const result = toTaggedId(
"user",
"ffffffff-ffff-ffff-ffff-ffffffffffff"
);
expect(result.startsWith("user_01")).toBe(true);
const encoded = result.slice("user_01".length);
for (const ch of encoded) {
expect(BASE_58_CHARS).toContain(ch);
}
});
});

View File

@@ -0,0 +1,150 @@
import { describe, expect, test } from "bun:test";
import {
parseTokenBudget,
findTokenBudgetPositions,
getBudgetContinuationMessage,
} from "../tokenBudget";
describe("parseTokenBudget", () => {
// --- shorthand at start ---
test("parses +500k at start", () => {
expect(parseTokenBudget("+500k")).toBe(500_000);
});
test("parses +2.5M at start", () => {
expect(parseTokenBudget("+2.5M")).toBe(2_500_000);
});
test("parses +1b at start", () => {
expect(parseTokenBudget("+1b")).toBe(1_000_000_000);
});
test("parses shorthand with leading whitespace", () => {
expect(parseTokenBudget(" +500k")).toBe(500_000);
});
// --- shorthand at end ---
test("parses +1.5m at end of sentence", () => {
expect(parseTokenBudget("do this +1.5m")).toBe(1_500_000);
});
test("parses shorthand at end with trailing period", () => {
expect(parseTokenBudget("please continue +100k.")).toBe(100_000);
});
test("parses shorthand at end with trailing whitespace", () => {
expect(parseTokenBudget("keep going +250k ")).toBe(250_000);
});
// --- verbose ---
test("parses 'use 2M tokens'", () => {
expect(parseTokenBudget("use 2M tokens")).toBe(2_000_000);
});
test("parses 'spend 500k tokens'", () => {
expect(parseTokenBudget("spend 500k tokens")).toBe(500_000);
});
test("parses verbose with singular 'token'", () => {
expect(parseTokenBudget("use 1k token")).toBe(1_000);
});
test("parses verbose embedded in sentence", () => {
expect(parseTokenBudget("please use 3.5m tokens for this task")).toBe(
3_500_000
);
});
// --- no match (returns null) ---
test("returns null for plain text", () => {
expect(parseTokenBudget("hello world")).toBeNull();
});
test("returns null for bare number without +", () => {
expect(parseTokenBudget("500k")).toBeNull();
});
test("returns null for number without suffix", () => {
expect(parseTokenBudget("+500")).toBeNull();
});
test("returns null for empty string", () => {
expect(parseTokenBudget("")).toBeNull();
});
// --- case insensitivity ---
test("is case insensitive for suffix", () => {
expect(parseTokenBudget("+500K")).toBe(500_000);
expect(parseTokenBudget("+2m")).toBe(2_000_000);
expect(parseTokenBudget("+1B")).toBe(1_000_000_000);
});
// --- priority: start shorthand wins over end/verbose ---
test("start shorthand takes priority over verbose in same text", () => {
expect(parseTokenBudget("+100k use 2M tokens")).toBe(100_000);
});
});
describe("findTokenBudgetPositions", () => {
test("returns single position for +500k at start", () => {
const positions = findTokenBudgetPositions("+500k");
expect(positions).toHaveLength(1);
expect(positions[0]!.start).toBe(0);
expect(positions[0]!.end).toBe(5);
});
test("returns position for shorthand at end", () => {
const text = "do this +100k";
const positions = findTokenBudgetPositions(text);
expect(positions).toHaveLength(1);
expect(positions[0]!.start).toBe(8);
expect(text.slice(positions[0]!.start, positions[0]!.end)).toBe("+100k");
});
test("returns position for verbose match", () => {
const text = "please use 2M tokens here";
const positions = findTokenBudgetPositions(text);
expect(positions).toHaveLength(1);
expect(text.slice(positions[0]!.start, positions[0]!.end)).toBe(
"use 2M tokens"
);
});
test("returns multiple positions for combined shorthand + verbose", () => {
const text = "use 2M tokens and then +500k";
const positions = findTokenBudgetPositions(text);
expect(positions.length).toBeGreaterThanOrEqual(2);
});
test("returns empty array for no match", () => {
expect(findTokenBudgetPositions("hello world")).toEqual([]);
});
test("does not double-count when +500k matches both start and end", () => {
const positions = findTokenBudgetPositions("+500k");
expect(positions).toHaveLength(1);
});
});
describe("getBudgetContinuationMessage", () => {
test("formats a continuation message with correct values", () => {
const msg = getBudgetContinuationMessage(50, 250_000, 500_000);
expect(msg).toContain("50%");
expect(msg).toContain("250,000");
expect(msg).toContain("500,000");
expect(msg).toContain("Keep working");
expect(msg).toContain("do not summarize");
});
test("formats zero values", () => {
const msg = getBudgetContinuationMessage(0, 0, 100_000);
expect(msg).toContain("0%");
expect(msg).toContain("0 / 100,000");
});
test("formats large numbers with commas", () => {
const msg = getBudgetContinuationMessage(75, 7_500_000, 10_000_000);
expect(msg).toContain("7,500,000");
expect(msg).toContain("10,000,000");
});
});

View File

@@ -0,0 +1,116 @@
import { describe, expect, test } from "bun:test";
import {
windowsPathToPosixPath,
posixPathToWindowsPath,
} from "../windowsPaths";
// ─── windowsPathToPosixPath ────────────────────────────────────────────
describe("windowsPathToPosixPath", () => {
test("converts drive letter path to posix", () => {
expect(windowsPathToPosixPath("C:\\Users\\foo")).toBe("/c/Users/foo");
});
test("lowercases the drive letter", () => {
expect(windowsPathToPosixPath("D:\\Work\\project")).toBe(
"/d/Work/project"
);
});
test("handles lowercase drive letter input", () => {
expect(windowsPathToPosixPath("e:\\data")).toBe("/e/data");
});
test("converts UNC path", () => {
expect(windowsPathToPosixPath("\\\\server\\share\\dir")).toBe(
"//server/share/dir"
);
});
test("converts root drive path", () => {
expect(windowsPathToPosixPath("D:\\")).toBe("/d/");
});
test("converts relative path by flipping backslashes", () => {
expect(windowsPathToPosixPath("src\\main.ts")).toBe("src/main.ts");
});
test("handles forward slashes in windows drive path", () => {
// The regex matches both / and \\ after drive letter
expect(windowsPathToPosixPath("C:/Users/foo")).toBe("/c/Users/foo");
});
test("already-posix relative path passes through", () => {
expect(windowsPathToPosixPath("src/main.ts")).toBe("src/main.ts");
});
test("handles deeply nested path", () => {
expect(
windowsPathToPosixPath("C:\\Users\\me\\Documents\\project\\src\\index.ts")
).toBe("/c/Users/me/Documents/project/src/index.ts");
});
});
// ─── posixPathToWindowsPath ────────────────────────────────────────────
describe("posixPathToWindowsPath", () => {
test("converts MSYS2/Git Bash drive path to windows", () => {
expect(posixPathToWindowsPath("/c/Users/foo")).toBe("C:\\Users\\foo");
});
test("uppercases the drive letter", () => {
expect(posixPathToWindowsPath("/d/Work/project")).toBe(
"D:\\Work\\project"
);
});
test("converts cygdrive path", () => {
expect(posixPathToWindowsPath("/cygdrive/d/work")).toBe("D:\\work");
});
test("converts cygdrive root path", () => {
expect(posixPathToWindowsPath("/cygdrive/c/")).toBe("C:\\");
});
test("converts UNC posix path to windows UNC", () => {
expect(posixPathToWindowsPath("//server/share/dir")).toBe(
"\\\\server\\share\\dir"
);
});
test("converts root drive posix path", () => {
expect(posixPathToWindowsPath("/d/")).toBe("D:\\");
});
test("converts bare drive mount (no trailing slash)", () => {
// /d matches the regex ^\/([A-Za-z])(\/|$) where $2 is empty
expect(posixPathToWindowsPath("/d")).toBe("D:\\");
});
test("converts relative path by flipping forward slashes", () => {
expect(posixPathToWindowsPath("src/main.ts")).toBe("src\\main.ts");
});
test("handles already-windows relative path", () => {
// No leading / or //, just flips / to backslash
expect(posixPathToWindowsPath("foo\\bar")).toBe("foo\\bar");
});
});
// ─── round-trip conversions ────────────────────────────────────────────
describe("round-trip conversions", () => {
test("drive path round-trips windows -> posix -> windows", () => {
const original = "C:\\Users\\foo\\bar";
const posix = windowsPathToPosixPath(original);
const back = posixPathToWindowsPath(posix);
expect(back).toBe(original);
});
test("drive path round-trips posix -> windows -> posix", () => {
const original = "/c/Users/foo/bar";
const win = posixPathToWindowsPath(original);
const back = windowsPathToPosixPath(win);
expect(back).toBe(original);
});
});

View File

@@ -0,0 +1,138 @@
import { describe, expect, test } from "bun:test";
import { parseConfigString } from "../gitConfigParser";
describe("parseConfigString", () => {
test("parses simple remote url", () => {
const config = '[remote "origin"]\n\turl = https://github.com/user/repo.git';
expect(parseConfigString(config, "remote", "origin", "url")).toBe(
"https://github.com/user/repo.git"
);
});
test("section matching is case-insensitive", () => {
const config = '[REMOTE "origin"]\n\turl = https://example.com';
expect(parseConfigString(config, "remote", "origin", "url")).toBe(
"https://example.com"
);
});
test("subsection matching is case-sensitive", () => {
const config = '[remote "Origin"]\n\turl = https://example.com';
expect(parseConfigString(config, "remote", "origin", "url")).toBeNull();
});
test("subsection matching is case-sensitive (positive)", () => {
const config = '[remote "Origin"]\n\turl = https://example.com';
expect(parseConfigString(config, "remote", "Origin", "url")).toBe(
"https://example.com"
);
});
test("key matching is case-insensitive", () => {
const config = '[remote "origin"]\n\tURL = https://example.com';
expect(parseConfigString(config, "remote", "origin", "url")).toBe(
"https://example.com"
);
});
test("parses quoted value with spaces", () => {
const config = '[user]\n\tname = "John Doe"';
expect(parseConfigString(config, "user", null, "name")).toBe("John Doe");
});
test("handles escape sequence \\n inside quotes", () => {
const config = '[user]\n\tname = "line1\\nline2"';
expect(parseConfigString(config, "user", null, "name")).toBe(
"line1\nline2"
);
});
test("handles escape sequence \\t inside quotes", () => {
const config = '[user]\n\tname = "col1\\tcol2"';
expect(parseConfigString(config, "user", null, "name")).toBe(
"col1\tcol2"
);
});
test("handles escape sequence \\\\ inside quotes", () => {
const config = '[user]\n\tname = "back\\\\slash"';
expect(parseConfigString(config, "user", null, "name")).toBe("back\\slash");
});
test("handles escape sequence \\\" inside quotes", () => {
const config = '[user]\n\tname = "say \\"hello\\""';
expect(parseConfigString(config, "user", null, "name")).toBe('say "hello"');
});
test("strips inline comment with #", () => {
const config = '[remote "origin"]\n\turl = https://example.com # comment';
expect(parseConfigString(config, "remote", "origin", "url")).toBe(
"https://example.com"
);
});
test("strips inline comment with ;", () => {
const config = '[remote "origin"]\n\turl = https://example.com ; comment';
expect(parseConfigString(config, "remote", "origin", "url")).toBe(
"https://example.com"
);
});
test("finds value in correct section among multiple sections", () => {
const config = [
'[remote "origin"]',
"\turl = https://origin.example.com",
'[remote "upstream"]',
"\turl = https://upstream.example.com",
].join("\n");
expect(parseConfigString(config, "remote", "upstream", "url")).toBe(
"https://upstream.example.com"
);
});
test("returns null for missing key", () => {
const config = '[remote "origin"]\n\turl = https://example.com';
expect(
parseConfigString(config, "remote", "origin", "pushurl")
).toBeNull();
});
test("returns null for missing section", () => {
const config = '[remote "origin"]\n\turl = https://example.com';
expect(parseConfigString(config, "branch", "main", "merge")).toBeNull();
});
test("returns null for boolean key (no = sign)", () => {
const config = "[core]\n\tbare";
expect(parseConfigString(config, "core", null, "bare")).toBeNull();
});
test("returns null for empty config string", () => {
expect(parseConfigString("", "remote", "origin", "url")).toBeNull();
});
test("handles section without subsection", () => {
const config = "[core]\n\trepositoryformatversion = 0";
expect(
parseConfigString(config, "core", null, "repositoryformatversion")
).toBe("0");
});
test("does not match section without subsection when subsection is requested", () => {
const config = "[core]\n\tbare = false";
// Looking for [core "something"] but config has [core]
expect(parseConfigString(config, "core", "something", "bare")).toBeNull();
});
test("skips comment-only lines", () => {
const config = [
"# This is a comment",
"; This is also a comment",
'[remote "origin"]',
"\turl = https://example.com",
].join("\n");
expect(parseConfigString(config, "remote", "origin", "url")).toBe(
"https://example.com"
);
});
});