소스 검색

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>
claude-code-best 3 주 전
부모
커밋
4f323efb61

+ 25 - 2
docs/testing-spec.md

@@ -300,7 +300,7 @@ bun test --watch
 
 ## 11. 当前测试覆盖状态
 
-> 更新日期:2026-04-02 | 总计:**968 tests, 52 files, 0 failures**
+> 更新日期:2026-04-02 | 总计:**1177 tests, 64 files, 0 failures**
 
 ### P0 — 核心模块
 
@@ -383,6 +383,23 @@ bun test --watch
 | `src/tools/BashTool/__tests__/destructiveCommandWarning.test.ts` | 22 | getDestructiveCommandWarning (git/rm/database/infrastructure patterns) |
 | `src/tools/BashTool/__tests__/commandSemantics.test.ts` | 11 | interpretCommandResult (grep/diff/test/rg/find exit code semantics) |
 
+### P6 — Phase 5 扩展覆盖
+
+| 测试文件 | 测试数 | 覆盖范围 |
+|----------|--------|----------|
+| `src/utils/__tests__/tokenBudget.test.ts` | 20 | parseTokenBudget, findTokenBudgetPositions, getBudgetContinuationMessage |
+| `src/utils/__tests__/displayTags.test.ts` | 17 | stripDisplayTags, stripDisplayTagsAllowEmpty, stripIdeContextTags |
+| `src/utils/__tests__/taggedId.test.ts` | 10 | toTaggedId (prefix/uniqueness/format) |
+| `src/utils/__tests__/controlMessageCompat.test.ts` | 15 | normalizeControlMessageKeys (snake_case→camelCase 转换) |
+| `src/services/mcp/__tests__/normalization.test.ts` | 11 | normalizeNameForMCP (特殊字符/截断/空字符串/Unicode) |
+| `src/services/mcp/__tests__/envExpansion.test.ts` | 14 | expandEnvVarsInString ($VAR/${VAR}/嵌套/未定义/转义) |
+| `src/utils/git/__tests__/gitConfigParser.test.ts` | 20 | parseConfigString (key=value/section/subsection/多行/注释/引号) |
+| `src/utils/__tests__/formatBriefTimestamp.test.ts` | 10 | formatBriefTimestamp (秒/分/时/天/周/月/年) |
+| `src/utils/__tests__/hyperlink.test.ts` | 10 | createHyperlink (OSC 8 序列/file:///path/fallback) |
+| `src/utils/__tests__/windowsPaths.test.ts` | 20 | windowsPathToPosixPath, posixPathToWindowsPath (驱动器/UNC/相对路径) |
+| `src/utils/__tests__/notebook.test.ts` | 14 | parseCellId, mapNotebookCellsToToolResult (code/markdown/output) |
+| `src/utils/__tests__/effort.test.ts` | 38 | isEffortLevel, parseEffortValue, isValidNumericEffort, convertEffortValueToLevel, getEffortLevelDescription, resolvePickerEffortPersistence |
+
 ### 已知限制
 
 以下模块因 Bun 运行时限制或极重依赖链,暂时无法或不适合测试:
@@ -405,14 +422,20 @@ bun test --watch
 | `src/utils/slowOperations.ts` | tokens.ts, permissions.ts, memoize.ts, PermissionMode.ts |
 | `src/utils/debug.ts` | envValidation.ts, outputLimits.ts |
 | `src/utils/bash/commands.ts` | commandSemantics.ts |
+| `src/utils/thinking.js` | effort.ts |
+| `src/utils/settings/settings.js` | effort.ts |
+| `src/utils/auth.js` | effort.ts |
+| `src/services/analytics/growthbook.js` | effort.ts, tokenBudget.ts |
+| `src/utils/model/modelSupportOverrides.js` | effort.ts |
 
 **关键约束**:`mock.module()` 必须在每个测试文件中内联调用,不能从共享 helper 导入(Bun 在 mock 生效前就解析了 helper 的导入)。
 
 ## 12. 后续测试覆盖计划
 
-> **已完成** — 实际增加 321 tests,从 647 → 968 tests / 52 files
+> **已完成** — Phase 1-4 增加 321 tests (647 → 968),Phase 5 增加 209 tests (968 → 1177)
 >
 > Phase 1-4 全部完成,详见上方 P3-P5 表格。
+> Phase 5 新增 12 个测试文件覆盖:effort、tokenBudget、displayTags、taggedId、controlMessageCompat、MCP normalization/envExpansion、gitConfigParser、formatBriefTimestamp、hyperlink、windowsPaths、notebook,详见 P6 表格。
 > 实际调整:Phase 3 中 `context.ts` 因极重依赖链(bootstrap/state + claudemd + git 等)且 `getGitStatus` 在 test 环境直接返回 null,替换为 `envValidation.ts`(更实用);Phase 4 中 GlobTool 纯函数不足,替换为 `commandSemantics.ts` + `destructiveCommandWarning.ts`。
 
 ### 不纳入计划的模块

+ 139 - 0
src/services/mcp/__tests__/envExpansion.test.ts

@@ -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([]);
+  });
+});

+ 59 - 0
src/services/mcp/__tests__/normalization.test.ts

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

+ 103 - 0
src/utils/__tests__/controlMessageCompat.test.ts

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

+ 134 - 0
src/utils/__tests__/displayTags.test.ts

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

+ 255 - 0
src/utils/__tests__/effort.test.ts

@@ -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();
+  });
+});

+ 76 - 0
src/utils/__tests__/formatBriefTimestamp.test.ts

@@ -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(":");
+  });
+});

+ 99 - 0
src/utils/__tests__/hyperlink.test.ts

@@ -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}`);
+  });
+});

+ 162 - 0
src/utils/__tests__/notebook.test.ts

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

+ 104 - 0
src/utils/__tests__/taggedId.test.ts

@@ -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);
+    }
+  });
+});

+ 150 - 0
src/utils/__tests__/tokenBudget.test.ts

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

+ 116 - 0
src/utils/__tests__/windowsPaths.test.ts

@@ -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);
+  });
+});

+ 138 - 0
src/utils/git/__tests__/gitConfigParser.test.ts

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