import { afterEach, beforeEach, describe, expect, mock, spyOn, test, } from "bun:test"; import * as fs from "node:fs"; import * as os from "node:os"; import { createWindowBaseName, createWindowPlans, findDuplicates, formatInvalidModelError, getBinaryName, getSessionPath, getSettingsPath, getWorktreePath, getWorktreesForSession, launchModelInWindow, levenshtein, loadSettings, normalizeModels, runCommand, sanitizeName, saveSettings, shellQuote, suggestModels, undoWorktree, } from "../src/core/utils"; const originalBun$: typeof Bun.$ = (globalThis as any).Bun?.$; let mockCommandResponses: Record< string, { ok: boolean; stdout: string; stderr: string } > = {}; let executedCommands: string[] = []; function setupMockBun$() { const mockFn = mock((_strings: TemplateStringsArray, ...values: any[]) => { const parts = values[0] as string[]; const commandSignature = parts.join(" "); executedCommands.push(commandSignature); return { quiet: () => ({ nothrow: async () => { const response = mockCommandResponses[commandSignature] ?? { ok: true, stdout: "", stderr: "", }; return { exitCode: response.ok ? 0 : 1, stdout: { toString: () => response.stdout }, stderr: { toString: () => response.stderr }, }; }, }), }; }); (globalThis as any).Bun.$ = mockFn; return mockFn; } function restoreOriginalBun$() { (globalThis as any).Bun.$ = originalBun$; } describe("core utils", () => { let _bunMock: ReturnType; beforeEach(() => { spyOn(os, "homedir").mockReturnValue("/mock/home"); _bunMock = setupMockBun$(); executedCommands = []; mockCommandResponses = {}; }); afterEach(() => { restoreOriginalBun$(); }); describe("shellQuote", () => { test("normal string without special characters", () => { expect(shellQuote("test")).toBe("'test'"); }); test("string with spaces", () => { expect(shellQuote("test value")).toBe("'test value'"); }); test("string with single quotes", () => { expect(shellQuote("test'value")).toBe("'test'\"'\"'value'"); }); test("string with multiple single quotes", () => { expect(shellQuote("te's't'v'alue")).toBe( "'te'\"'\"'s'\"'\"'t'\"'\"'v'\"'\"'alue'", ); }); test("empty string", () => { expect(shellQuote("")).toBe("''"); }); }); describe("normalizeModels", () => { test("empty array", () => { expect(normalizeModels([])).toEqual([]); }); test("undefined input", () => { expect(normalizeModels(undefined)).toEqual([]); }); test("array with whitespace-only strings", () => { expect(normalizeModels([" ", " "])).toEqual([]); }); test("array with mixed valid models, padded models, and empty strings", () => { expect( normalizeModels([" openai/gpt-5.2 ", "", " anthropic/claude "]), ).toEqual(["openai/gpt-5.2", "anthropic/claude"]); }); }); describe("findDuplicates", () => { test("array with no duplicates", () => { expect(findDuplicates(["a", "b", "c"])).toEqual([]); }); test("array with one duplicate pair", () => { expect(findDuplicates(["a", "b", "a"])).toEqual(["a"]); }); test("array with multiple different duplicates", () => { expect(findDuplicates(["a", "b", "a", "c", "b"])).toEqual(["a", "b"]); }); test("array with a single value repeated more than twice", () => { expect(findDuplicates(["a", "a", "a"])).toEqual(["a"]); }); test("verifying first-repeated-occurrence order", () => { expect(findDuplicates(["x", "a", "b", "a", "x", "b"])).toEqual([ "a", "x", "b", ]); }); }); describe("createWindowBaseName", () => { test("normal model name without slashes", () => { expect(createWindowBaseName("gpt-5-2")).toBe("gpt-5-2"); }); test("model with slashes", () => { expect(createWindowBaseName("vendor/namespace/model")).toBe("model"); }); test("model exceeding WINDOW_NAME_LIMIT", () => { const longName = "a".repeat(30); expect(createWindowBaseName(longName)).toBe("aaaaaaaaaaaaaaaaaaaaaaaa"); }); test("model with special characters", () => { expect(createWindowBaseName("OpenAI/GPT_4.0!")).toBe("gpt-4-0"); }); test("model made entirely of special characters", () => { expect(createWindowBaseName("!@#$%^&*()")).toBe("model"); }); }); describe("createWindowPlans", () => { test("single model", () => { expect(createWindowPlans(["openai/gpt-5-2"])).toEqual([ { model: "openai/gpt-5-2", windowName: "gpt-5-2" }, ]); }); test("two models with the same base name", () => { expect( createWindowPlans(["openai/gpt-5-2", "anthropic/gpt-5-2"]), ).toEqual([ { model: "openai/gpt-5-2", windowName: "gpt-5-2" }, { model: "anthropic/gpt-5-2", windowName: "gpt-5-2-2" }, ]); }); test("multiple collisions", () => { const result = createWindowPlans(["a", "b", "c", "a", "b", "a"]); expect(result[0]?.windowName).toBe("a"); expect(result[3]?.windowName).toBe("a-2"); expect(result[5]?.windowName).toBe("a-3"); }); test("truncation to accommodate suffix", () => { const longBase = "a".repeat(23); const result = createWindowPlans([longBase, "b"]); expect(result[1]?.windowName.length).toBeLessThanOrEqual(24); }); }); describe("levenshtein", () => { test("identical strings", () => { expect(levenshtein("test", "test")).toBe(0); }); test("one substitution", () => { expect(levenshtein("test", "tent")).toBe(1); }); test("one insertion", () => { expect(levenshtein("test", "tests")).toBe(1); }); test("one deletion", () => { expect(levenshtein("tests", "test")).toBe(1); }); test("completely different strings", () => { expect(levenshtein("abc", "xyz")).toBe(3); }); test("one empty string", () => { expect(levenshtein("", "test")).toBe(4); }); test("both empty strings", () => { expect(levenshtein("", "")).toBe(0); }); }); describe("suggestModels", () => { test("exact match", () => { const result = suggestModels("gpt-5.2", ["gpt-5.2", "gpt-5.2-mini"]); expect(result[0]).toBe("gpt-5.2"); }); test("fuzzy match/typo", () => { expect(suggestModels("gpt5.4", ["gpt-5.4"])).toEqual(["gpt-5.4"]); }); test("fuzzy match with contains boost", () => { const suggestions = suggestModels("gpt4o", ["gpt-5.2", "gpt-5.2-mini"]); expect(suggestions[0]).toBe("gpt-5.2"); }); test("maximum of 3 suggestions", () => { const allowlist = [ "gpt-5.2", "gpt-5.2-mini", "gpt-5.2-pro", "gpt-5.2-ultra", ]; expect(suggestModels("gpt4o", allowlist).length).toBe(3); }); test("sorting logic", () => { const suggestions = suggestModels("abc", ["abc", "abd", "abe"]); expect(suggestions).toEqual(["abc", "abd", "abe"]); }); }); describe("formatInvalidModelError", () => { test("single invalid model with available suggestions", () => { const error = formatInvalidModelError(["gpt5.4"], ["gpt-5.4", "gpt-5.2"]); expect(error).toContain("Model name 'gpt5.4' not found"); expect(error).toContain("Did you mean"); }); test("single invalid model without close matches", () => { const error = formatInvalidModelError(["xyz"], ["abc", "def"]); expect(error).toContain("Model name 'xyz' not found"); expect(error).toContain("Did you mean"); }); test("multiple invalid models", () => { const error = formatInvalidModelError(["xyz", "abc"], ["def"]); expect(error).toContain("Model names not found"); expect(error).toContain("'xyz'"); expect(error).toContain("'abc'"); }); }); describe("sanitizeName", () => { test("normal alphanumeric name", () => { expect(sanitizeName("test-session")).toBe("test-session"); }); test("name with spaces and special chars", () => { expect(sanitizeName("test session!")).toBe("test-session"); }); test("name with consecutive hyphens", () => { expect(sanitizeName("test--session")).toBe("test-session"); }); test("name with leading/trailing hyphens", () => { expect(sanitizeName("-test-session-")).toBe("test-session"); }); }); describe("runCommand", () => { test("returns ok true for successful command", async () => { mockCommandResponses["echo hello"] = { ok: true, stdout: "hello", stderr: "", }; const result = await runCommand(["echo", "hello"]); expect(result.ok).toBe(true); expect(result.stdout).toBe("hello"); expect(result.stderr).toBe(""); }); test("returns ok false for failed command", async () => { mockCommandResponses.false = { ok: false, stdout: "", stderr: "error", }; const result = await runCommand(["false"]); expect(result.ok).toBe(false); expect(result.stderr).toBe("error"); }); }); describe("getWorktreePath", () => { test("returns correct worktree path", () => { spyOn(os, "homedir").mockReturnValue("/home/user"); const result = getWorktreePath("my-session", "gpt-5.2"); expect(result).toBe( "/home/user/.local/share/opencode/multi-model/my-session/gpt-5.2", ); }); test("handles nested session names", () => { spyOn(os, "homedir").mockReturnValue("/home/user"); const result = getWorktreePath("parent/child", "window"); expect(result).toBe( "/home/user/.local/share/opencode/multi-model/parent/child/window", ); }); }); describe("getSessionPath", () => { test("returns correct session path", () => { spyOn(os, "homedir").mockReturnValue("/home/user"); const result = getSessionPath("my-session"); expect(result).toBe( "/home/user/.local/share/opencode/multi-model/my-session", ); }); }); describe("undoWorktree", () => { test("returns empty array on success", async () => { mockCommandResponses["git worktree remove -f /path/to/worktree"] = { ok: true, stdout: "", stderr: "", }; mockCommandResponses["git branch -D opencode/session/window"] = { ok: true, stdout: "", stderr: "", }; const errors = await undoWorktree( "/path/to/worktree", "opencode/session/window", ); expect(errors).toEqual([]); }); test("returns error when worktree remove fails", async () => { mockCommandResponses["git worktree remove -f /path/to/worktree"] = { ok: false, stdout: "", stderr: "remove failed", }; mockCommandResponses["git branch -D opencode/session/window"] = { ok: true, stdout: "", stderr: "", }; const errors = await undoWorktree( "/path/to/worktree", "opencode/session/window", ); expect(errors.length).toBe(1); expect(errors[0]).toContain("Failed to remove worktree"); }); test("returns error when branch delete fails", async () => { mockCommandResponses["git worktree remove -f /path/to/worktree"] = { ok: true, stdout: "", stderr: "", }; mockCommandResponses["git branch -D opencode/session/window"] = { ok: false, stdout: "", stderr: "branch delete failed", }; const errors = await undoWorktree( "/path/to/worktree", "opencode/session/window", ); expect(errors.length).toBe(1); expect(errors[0]).toContain("Failed to delete branch"); }); test("returns both errors when both operations fail", async () => { mockCommandResponses["git worktree remove -f /path/to/worktree"] = { ok: false, stdout: "", stderr: "remove failed", }; mockCommandResponses["git branch -D opencode/session/window"] = { ok: false, stdout: "", stderr: "branch delete failed", }; const errors = await undoWorktree( "/path/to/worktree", "opencode/session/window", ); expect(errors.length).toBe(2); }); }); describe("launchModelInWindow", () => { test("sends correct tmux command", async () => { mockCommandResponses[ "tmux send-keys -t session:window opencode --model 'openai/gpt-5.2' C-m" ] = { ok: true, stdout: "", stderr: "" }; const result = await launchModelInWindow("session", { model: "openai/gpt-5.2", windowName: "window", }); expect(result.ok).toBe(true); expect(executedCommands).toContain( "tmux send-keys -t session:window opencode --model 'openai/gpt-5.2' C-m", ); }); test("uses custom binary name", async () => { mockCommandResponses[ "tmux send-keys -t session:window kilo --model 'openai/gpt-5.2' C-m" ] = { ok: true, stdout: "", stderr: "" }; const result = await launchModelInWindow( "session", { model: "openai/gpt-5.2", windowName: "window" }, "kilo", ); expect(result.ok).toBe(true); expect(executedCommands).toContain( "tmux send-keys -t session:window kilo --model 'openai/gpt-5.2' C-m", ); }); }); describe("getWorktreesForSession", () => { test("returns session-specific worktrees", async () => { spyOn(os, "homedir").mockReturnValue("/home/user"); mockCommandResponses["git worktree list --porcelain"] = { ok: true, stdout: "worktree /home/user/.local/share/opencode/multi-model/my-session/gpt-5.2\nbranch refs/heads/opencode/my-session/gpt-5.2\n\nworktree /home/user/other-repo\nbranch refs/heads/main", stderr: "", }; const worktrees = await getWorktreesForSession("my-session"); expect(worktrees.length).toBe(1); expect(worktrees[0]?.path).toBe( "/home/user/.local/share/opencode/multi-model/my-session/gpt-5.2", ); expect(worktrees[0]?.branch).toBe("opencode/my-session/gpt-5.2"); }); test("sanitizes session name when filtering worktrees", async () => { spyOn(os, "homedir").mockReturnValue("/home/user"); mockCommandResponses["git worktree list --porcelain"] = { ok: true, stdout: "worktree /home/user/.local/share/opencode/multi-model/my-session/gpt-5.2\nbranch refs/heads/opencode/my-session/gpt-5.2", stderr: "", }; const worktrees = await getWorktreesForSession("my session!"); expect(worktrees.length).toBe(1); expect(worktrees[0]?.path).toBe( "/home/user/.local/share/opencode/multi-model/my-session/gpt-5.2", ); }); test("returns empty array when no matching worktrees", async () => { spyOn(os, "homedir").mockReturnValue("/home/user"); mockCommandResponses["git worktree list --porcelain"] = { ok: true, stdout: "worktree /home/user/other-repo\nbranch refs/heads/main", stderr: "", }; const worktrees = await getWorktreesForSession("my-session"); expect(worktrees).toEqual([]); }); }); describe("getBinaryName", () => { test("returns default 'opencode' when env var not set", () => { delete process.env.OPENCODE_MULTI_MODEL_BINARY; expect(getBinaryName()).toBe("opencode"); }); test("returns env var value when set", () => { process.env.OPENCODE_MULTI_MODEL_BINARY = "kilo"; expect(getBinaryName()).toBe("kilo"); delete process.env.OPENCODE_MULTI_MODEL_BINARY; }); }); describe("getSettingsPath", () => { test("returns correct settings path", () => { spyOn(os, "homedir").mockReturnValue("/home/user"); expect(getSettingsPath()).toBe( "/home/user/.config/opencode-multi-model/settings.json", ); }); }); describe("loadSettings", () => { let readFileMock: ReturnType; beforeEach(() => { spyOn(os, "homedir").mockReturnValue("/home/user"); readFileMock = spyOn(fs.promises, "readFile"); }); test("returns default settings when file does not exist", async () => { readFileMock.mockRejectedValue(new Error("ENOENT")); const result = await loadSettings(); expect(result).toEqual({ lastModels: [] }); }); test("returns default settings when file has invalid JSON", async () => { readFileMock.mockResolvedValue("not valid json"); const result = await loadSettings(); expect(result).toEqual({ lastModels: [] }); }); test("returns default settings when lastModels is not an array", async () => { readFileMock.mockResolvedValue( JSON.stringify({ lastModels: "not array" }), ); const result = await loadSettings(); expect(result).toEqual({ lastModels: [] }); }); test("parses valid settings file", async () => { readFileMock.mockResolvedValue( JSON.stringify({ lastModels: ["model1", "model2"] }), ); const result = await loadSettings(); expect(result).toEqual({ lastModels: ["model1", "model2"] }); }); }); describe("saveSettings", () => { let mkdirMock: ReturnType; let writeFileMock: ReturnType; beforeEach(() => { spyOn(os, "homedir").mockReturnValue("/home/user"); mkdirMock = spyOn(fs.promises, "mkdir"); writeFileMock = spyOn(fs.promises, "writeFile"); }); test("creates directory if it does not exist", async () => { mkdirMock.mockResolvedValue(undefined); writeFileMock.mockResolvedValue(undefined); await saveSettings({ lastModels: ["model1"] }); expect(mkdirMock).toHaveBeenCalledWith( "/home/user/.config/opencode-multi-model", { recursive: true }, ); }); test("writes settings with correct content", async () => { mkdirMock.mockResolvedValue(undefined); writeFileMock.mockResolvedValue(undefined); await saveSettings({ lastModels: ["model1", "model2"] }); expect(writeFileMock).toHaveBeenCalledWith( "/home/user/.config/opencode-multi-model/settings.json", JSON.stringify({ lastModels: ["model1", "model2"] }, null, 2), "utf-8", ); }); }); });