diff --git a/tests/multi-model.test.ts b/tests/multi-model.test.ts index 75c062b..959674e 100644 --- a/tests/multi-model.test.ts +++ b/tests/multi-model.test.ts @@ -14,8 +14,11 @@ import { sanitizeName, runCommand, getWorktreePath, + getSessionPath, undoWorktree, launchModelInWindow, + getWorktreesForSession, + getBinaryName, } from "../src/core/utils"; let originalBun$: typeof Bun.$ = (globalThis as any).Bun?.$; @@ -351,6 +354,19 @@ describe("multi-model launch", () => { }); 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'"); @@ -556,4 +572,138 @@ describe("core utils", () => { 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-4o"); + expect(result).toBe("/home/user/.local/share/opencode/multi-model/my-session/gpt-4o"); + }); + + 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-4o' C-m"] = { ok: true, stdout: "", stderr: "" }; + + const result = await launchModelInWindow("session", { model: "openai/gpt-4o", windowName: "window" }); + expect(result.ok).toBe(true); + expect(executedCommands).toContain("tmux send-keys -t session:window opencode --model 'openai/gpt-4o' C-m"); + }); + + test("uses custom binary name", async () => { + mockCommandResponses["tmux send-keys -t session:window kilo --model 'openai/gpt-4o' C-m"] = { ok: true, stdout: "", stderr: "" }; + + const result = await launchModelInWindow("session", { model: "openai/gpt-4o", windowName: "window" }, "kilo"); + expect(result.ok).toBe(true); + expect(executedCommands).toContain("tmux send-keys -t session:window kilo --model 'openai/gpt-4o' 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-4o\nbranch refs/heads/opencode/my-session/gpt-4o\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-4o"); + expect(worktrees[0]?.branch).toBe("opencode/my-session/gpt-4o"); + }); + + 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; + }); + }); });