import { afterEach, beforeEach, describe, expect, mock, spyOn, test, } from "bun:test"; import * as fs from "node:fs"; import * as os from "node:os"; import { openMultiModel } from "../src/core/open"; 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("multi-model launch", () => { let existsSyncMock: ReturnType; let _bunMock: ReturnType; beforeEach(() => { spyOn(os, "homedir").mockReturnValue("/mock/home"); existsSyncMock = spyOn(fs, "existsSync").mockReturnValue(false); _bunMock = setupMockBun$(); executedCommands = []; mockCommandResponses = { "git rev-parse --is-inside-work-tree": { ok: true, stdout: "true", stderr: "", }, "command -v tmux": { ok: true, stdout: "/usr/bin/tmux", stderr: "" }, "command -v opencode": { ok: true, stdout: "/usr/bin/opencode", stderr: "", }, "opencode models": { ok: true, stdout: "openai/gpt-5.2\nanthropic/claude-3-5-sonnet", stderr: "", }, "tmux has-session -t test-session": { ok: false, stdout: "", stderr: "session not found", }, "git show-ref --verify --quiet refs/heads/opencode/test-session/gpt-5-2": { ok: false, stdout: "", stderr: "" }, }; }); afterEach(() => { restoreOriginalBun$(); }); test("fails if session name is empty", async () => { const result = await openMultiModel({ mode: "cli", sessionName: "", models: ["openai/gpt-5.2"], }); expect(result.success).toBe(false); expect(result.error).toContain("Error: `sessionName` must not be empty."); expect(executedCommands).toEqual([]); }); test("fails if no models are provided", async () => { const result = await openMultiModel({ mode: "cli", sessionName: "test-session", models: [], }); expect(result.success).toBe(false); expect(result.error).toContain("Error: Model names must not be empty."); expect(executedCommands).toEqual([]); }); test("fails if duplicate models are provided", async () => { const result = await openMultiModel({ mode: "cli", sessionName: "test-session", models: ["openai/gpt-5.2", "openai/gpt-5.2"], }); expect(result.success).toBe(false); expect(result.error).toContain("Duplicate model names are not allowed"); expect(executedCommands).toEqual([]); }); test("fails if not inside a git repository", async () => { mockCommandResponses["git rev-parse --is-inside-work-tree"] = { ok: false, stdout: "", stderr: "not a git repo", }; const result = await openMultiModel({ mode: "cli", sessionName: "test-session", models: ["openai/gpt-5.2"], }); expect(result.success).toBe(false); expect(result.error).toContain( "Error: `multi-model` requires a git repository.", ); expect(executedCommands).toEqual(["git rev-parse --is-inside-work-tree"]); }); test("fails if tmux is not installed", async () => { mockCommandResponses["command -v tmux"] = { ok: false, stdout: "", stderr: "", }; const result = await openMultiModel({ mode: "cli", sessionName: "test-session", models: ["openai/gpt-5.2"], }); expect(result.success).toBe(false); expect(result.error).toContain( "Error: `tmux` is not installed or not on `PATH`.", ); expect(executedCommands).toEqual([ "git rev-parse --is-inside-work-tree", "command -v tmux", ]); }); test("fails if opencode is not installed", async () => { mockCommandResponses["command -v opencode"] = { ok: false, stdout: "", stderr: "", }; const result = await openMultiModel({ mode: "cli", sessionName: "test-session", models: ["openai/gpt-5.2"], }); expect(result.success).toBe(false); expect(result.error).toContain( "Error: `opencode` is not installed or not on `PATH`.", ); expect(executedCommands).toEqual([ "git rev-parse --is-inside-work-tree", "command -v tmux", "command -v opencode", ]); }); test("fails if model name is not in allowlist", async () => { const result = await openMultiModel({ mode: "cli", sessionName: "test-session", models: ["invalid/model"], }); expect(result.success).toBe(false); expect(result.error).toContain( "Error: Model name 'invalid/model' not found", ); expect(executedCommands).toEqual([ "git rev-parse --is-inside-work-tree", "command -v tmux", "command -v opencode", "opencode models", ]); }); test("fails if session already exists", async () => { mockCommandResponses["tmux has-session -t test-session"] = { ok: true, stdout: "", stderr: "", }; const result = await openMultiModel({ mode: "cli", sessionName: "test-session", models: ["openai/gpt-5.2"], }); expect(result.success).toBe(false); expect(result.error).toContain("Error: Session already exists"); expect(executedCommands).toEqual([ "git rev-parse --is-inside-work-tree", "command -v tmux", "command -v opencode", "opencode models", "tmux has-session -t test-session", ]); }); test("successfully plans and executes tmux launch with single model", async () => { const result = await openMultiModel({ mode: "cli", sessionName: "test-session", models: ["openai/gpt-5.2"], }); expect(result.success).toBe(true); expect(result.instructions).toContain( "Use `tmux attach -t test-session` to join session", ); expect( executedCommands.some((c) => c.startsWith("tmux new-session -d -s test-session -n gpt-5-2"), ), ).toBe(true); expect( executedCommands.some((c) => c.startsWith("tmux send-keys -t test-session:gpt-5-2"), ), ).toBe(true); }); test("successfully launches multiple models", async () => { mockCommandResponses[ "git show-ref --verify --quiet refs/heads/opencode/test-session/claude-3-5-sonnet" ] = { ok: false, stdout: "", stderr: "" }; const result = await openMultiModel({ mode: "cli", sessionName: "test-session", models: ["openai/gpt-5.2", "anthropic/claude-3-5-sonnet"], }); expect(result.success).toBe(true); expect(result.instructions).toContain( "Use `tmux attach -t test-session` to join session", ); expect( executedCommands.some((c) => c.startsWith("tmux new-session -d -s test-session -n gpt-5-2"), ), ).toBe(true); expect( executedCommands.some((c) => c.startsWith("tmux new-window -d -t test-session -n claude-3-5-sonnet"), ), ).toBe(true); }); test("sanitizes session name", async () => { mockCommandResponses["tmux has-session -t test session!"] = { ok: false, stdout: "", stderr: "", }; mockCommandResponses[ "git show-ref --verify --quiet refs/heads/opencode/test-session/gpt-5-2" ] = { ok: false, stdout: "", stderr: "" }; const result = await openMultiModel({ mode: "cli", sessionName: "test session!", models: ["openai/gpt-5.2"], }); expect(result.success).toBe(true); expect(result.instructions).toContain( "Use `tmux attach -t test session!` to join session", ); expect( executedCommands.some((c) => c.startsWith("git worktree add -b opencode/test-session/gpt-5-2"), ), ).toBe(true); }); test("uses sessionName for tmux commands, not safeSessionName", async () => { const unsanitized = "my awesome session!!"; const sanitized = "my-awesome-session"; mockCommandResponses[`tmux has-session -t ${unsanitized}`] = { ok: false, stdout: "", stderr: "", }; mockCommandResponses[ `git show-ref --verify --quiet refs/heads/opencode/${sanitized}/gpt-5-2` ] = { ok: false, stdout: "", stderr: "" }; mockCommandResponses[ `git worktree add -b opencode/${sanitized}/gpt-5-2 /mock/home/.local/share/opencode/multi-model/${sanitized}/gpt-5-2` ] = { ok: true, stdout: "", stderr: "" }; mockCommandResponses[ `tmux new-session -d -s ${unsanitized} -n gpt-5-2 -c /mock/home/.local/share/opencode/multi-model/${sanitized}/gpt-5-2` ] = { ok: true, stdout: "", stderr: "" }; mockCommandResponses[ `tmux send-keys -t ${unsanitized}:gpt-5-2 opencode --model 'openai/gpt-5.2' C-m` ] = { ok: true, stdout: "", stderr: "" }; const result = await openMultiModel({ mode: "cli", sessionName: unsanitized, models: ["openai/gpt-5.2"], }); expect(result.success).toBe(true); // tmux commands should use unsanitized name expect( executedCommands.some((c) => c.startsWith(`tmux has-session -t ${unsanitized}`), ), ).toBe(true); expect( executedCommands.some((c) => c.startsWith(`tmux new-session -d -s ${unsanitized}`), ), ).toBe(true); expect( executedCommands.some((c) => c.startsWith(`tmux send-keys -t ${unsanitized}:`), ), ).toBe(true); // sanitized name should NOT appear in tmux commands expect(executedCommands.some((c) => c.includes(`-t ${sanitized}`))).toBe( false, ); // git branches and paths should use sanitized name expect( executedCommands.some((c) => c.startsWith(`git worktree add -b opencode/${sanitized}/gpt-5-2`), ), ).toBe(true); }); test("fails if tmux new-session fails for the first model and triggers undo", async () => { mockCommandResponses[ "tmux new-session -d -s test-session -n gpt-5-2 -c /mock/home/.local/share/opencode/multi-model/test-session/gpt-5-2" ] = { ok: false, stdout: "", stderr: "tmux error" }; mockCommandResponses[ "git worktree remove -f /mock/home/.local/share/opencode/multi-model/test-session/gpt-5-2" ] = { ok: true, stdout: "", stderr: "" }; mockCommandResponses["git branch -D opencode/test-session/gpt-5-2"] = { ok: true, stdout: "", stderr: "", }; const result = await openMultiModel({ mode: "cli", sessionName: "test-session", models: ["openai/gpt-5.2"], }); expect(result.success).toBe(false); expect(result.error).toContain("Failed to create tmux session"); expect(executedCommands).toEqual([ "git rev-parse --is-inside-work-tree", "command -v tmux", "command -v opencode", "opencode models", "tmux has-session -t test-session", "git show-ref --verify --quiet refs/heads/opencode/test-session/gpt-5-2", "git worktree add -b opencode/test-session/gpt-5-2 /mock/home/.local/share/opencode/multi-model/test-session/gpt-5-2", "tmux new-session -d -s test-session -n gpt-5-2 -c /mock/home/.local/share/opencode/multi-model/test-session/gpt-5-2", "git worktree remove -f /mock/home/.local/share/opencode/multi-model/test-session/gpt-5-2", "git branch -D opencode/test-session/gpt-5-2", ]); }); test("fails if worktree path exists for first model", async () => { existsSyncMock.mockImplementation((path: string) => { return path.includes("test-session/gpt-5-2"); }); const result = await openMultiModel({ mode: "cli", sessionName: "test-session", models: ["openai/gpt-5.2"], }); expect(result.success).toBe(false); expect(result.error).toContain("Worktree path already exists"); expect(executedCommands).toEqual([ "git rev-parse --is-inside-work-tree", "command -v tmux", "command -v opencode", "opencode models", "tmux has-session -t test-session", ]); }); test("fails if git branch exists for first model", async () => { mockCommandResponses[ "git show-ref --verify --quiet refs/heads/opencode/test-session/gpt-5-2" ] = { ok: true, stdout: "", stderr: "" }; const result = await openMultiModel({ mode: "cli", sessionName: "test-session", models: ["openai/gpt-5.2"], }); expect(result.success).toBe(false); expect(result.error).toContain( "Branch 'opencode/test-session/gpt-5-2' already exists", ); expect(executedCommands).toEqual([ "git rev-parse --is-inside-work-tree", "command -v tmux", "command -v opencode", "opencode models", "tmux has-session -t test-session", "git show-ref --verify --quiet refs/heads/opencode/test-session/gpt-5-2", ]); }); test("fails if git worktree add fails for first model", async () => { mockCommandResponses[ "git worktree add -b opencode/test-session/gpt-5-2 /mock/home/.local/share/opencode/multi-model/test-session/gpt-5-2" ] = { ok: false, stdout: "", stderr: "git error" }; const result = await openMultiModel({ mode: "cli", sessionName: "test-session", models: ["openai/gpt-5.2"], }); expect(result.success).toBe(false); expect(result.error).toContain("Failed to create worktree"); expect(executedCommands).toEqual([ "git rev-parse --is-inside-work-tree", "command -v tmux", "command -v opencode", "opencode models", "tmux has-session -t test-session", "git show-ref --verify --quiet refs/heads/opencode/test-session/gpt-5-2", "git worktree add -b opencode/test-session/gpt-5-2 /mock/home/.local/share/opencode/multi-model/test-session/gpt-5-2", ]); }); test("model name collision creates unique window names", async () => { mockCommandResponses["opencode models"] = { ok: true, stdout: "openai/gpt-5.2\nanthropic/gpt-5.2", stderr: "", }; mockCommandResponses[ "git show-ref --verify --quiet refs/heads/opencode/test-session/gpt-5-2" ] = { ok: false, stdout: "", stderr: "" }; mockCommandResponses[ "git show-ref --verify --quiet refs/heads/opencode/test-session/gpt-5-2-2" ] = { ok: false, stdout: "", stderr: "" }; const result = await openMultiModel({ mode: "cli", sessionName: "test-session", models: ["openai/gpt-5.2", "anthropic/gpt-5.2"], }); expect(result.success).toBe(true); expect(result.instructions).toContain( "Use `tmux attach -t test-session` to join session", ); expect( executedCommands.some((c) => c.startsWith("tmux new-session -d -s test-session -n gpt-5-2"), ), ).toBe(true); expect( executedCommands.some((c) => c.startsWith("tmux new-window -d -t test-session -n gpt-5-2-2"), ), ).toBe(true); }); test("long model name is truncated in window name", async () => { const longModel = "verylongmodelfrontexampleprovider"; mockCommandResponses["opencode models"] = { ok: true, stdout: longModel, stderr: "", }; mockCommandResponses[ `git show-ref --verify --quiet refs/heads/opencode/test-session/${longModel.slice(0, 24)}` ] = { ok: false, stdout: "", stderr: "" }; const result = await openMultiModel({ mode: "cli", sessionName: "test-session", models: [longModel], }); expect(result.success).toBe(true); expect(result.instructions).toContain( "Use `tmux attach -t test-session` to join session", ); const windowName = executedCommands .find((c) => c.includes("tmux new-session")) ?.match(/-n (\S+)/)?.[1]; expect(windowName?.length).toBeLessThanOrEqual(24); }); }); // ---------- // Core utils tests remain in a separate file.