diff --git a/tests/close_error.test.ts b/tests/close_error.test.ts new file mode 100644 index 0000000..7601e85 --- /dev/null +++ b/tests/close_error.test.ts @@ -0,0 +1,53 @@ +import { expect, test, mock, beforeEach, afterEach, describe, spyOn } from "bun:test"; +import * as os from "node:os"; +import { closeMultiModel } from "../src/core/close"; + +let originalBun$: typeof Bun.$ = (globalThis as any).Bun?.$; +let mockCommandResponses: Record = {}; +let executedCommands: string[] = []; + +function setupMockBun$() { + const mockFn = mock((strings: TemplateStringsArray, ...values: any[]) => { + const parts = values[0] as string[]; + const cmd = parts.join(" "); + executedCommands.push(cmd); + // Simulate throwing error for a specific command + if (cmd === "git worktree list --porcelain") { + throw new Error("unexpected error"); + } + const response = mockCommandResponses[cmd] ?? { ok: true, stdout: "", stderr: "" }; + return { + quiet: () => ({ + nothrow: async () => ({ + 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("closeMultiModel unexpected error handling", () => { + beforeEach(() => { + spyOn(os, "homedir").mockReturnValue("/home/user"); + mockCommandResponses = {}; + executedCommands = []; + setupMockBun$(); + }); + afterEach(() => { + restoreOriginalBun$(); + }); + + test("catches thrown error and returns failure", async () => { + const result = await closeMultiModel({ sessionName: "any-session", cleanupWorktrees: false, force: false }); + expect(result.success).toBe(false); + expect(result.error).toContain("unexpected error"); + }); +}); diff --git a/tests/open_error.test.ts b/tests/open_error.test.ts new file mode 100644 index 0000000..89b6c7a --- /dev/null +++ b/tests/open_error.test.ts @@ -0,0 +1,201 @@ +import { expect, test, mock, beforeEach, afterEach, describe, spyOn } from "bun:test"; +import * as os from "node:os"; +import { openMultiModel } from "../src/core/open"; + +let originalBun$: typeof Bun.$ = (globalThis as any).Bun?.$; +let mockCommandResponses: Record = {}; +let executedCommands: string[] = []; + +function setupMockBun$() { + const mockFn = mock((strings: TemplateStringsArray, ...values: any[]) => { + const parts = values[0] as string[]; + const cmd = parts.join(" "); + executedCommands.push(cmd); + const response = mockCommandResponses[cmd] ?? { ok: true, stdout: "", stderr: "" }; + return { + quiet: () => ({ + nothrow: async () => ({ + 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("openMultiModel error paths", () => { + beforeEach(() => { + spyOn(os, "homedir").mockReturnValue("/mock/home"); + mockCommandResponses = {}; + executedCommands = []; + setupMockBun$(); + }); + afterEach(() => { + restoreOriginalBun$(); + }); + + test("fails when model list command fails", async () => { + const session = "sess"; + mockCommandResponses["git rev-parse --is-inside-work-tree"] = { ok: true, stdout: "true", stderr: "" }; + mockCommandResponses["command -v tmux"] = { ok: true, stdout: "tmux", stderr: "" }; + mockCommandResponses["command -v opencode"] = { ok: true, stdout: "opencode", stderr: "" }; + mockCommandResponses["opencode models"] = { ok: false, stdout: "", stderr: "model error" }; + + const result = await openMultiModel({ sessionName: session, models: ["openai/gpt-5.2"], binaryName: "opencode", mode: "cli" }); + expect(result.success).toBe(false); + expect(result.error).toContain("Failed to load valid models"); + }); + + test("fails when duplicate models provided", async () => { + const session = "dup"; + mockCommandResponses["git rev-parse --is-inside-work-tree"] = { ok: true, stdout: "true", stderr: "" }; + mockCommandResponses["command -v tmux"] = { ok: true, stdout: "tmux", stderr: "" }; + mockCommandResponses["command -v opencode"] = { ok: true, stdout: "opencode", stderr: "" }; + mockCommandResponses["opencode models"] = { ok: true, stdout: "openai/gpt-5.2", stderr: "" }; + + const result = await openMultiModel({ sessionName: session, models: ["openai/gpt-5.2", "openai/gpt-5.2"], binaryName: "opencode", mode: "cli" }); + expect(result.success).toBe(false); + expect(result.error).toContain("Duplicate model names are not allowed"); + }); + + test("fails when session already exists", async () => { + const session = "existing"; + mockCommandResponses["git rev-parse --is-inside-work-tree"] = { ok: true, stdout: "true", stderr: "" }; + mockCommandResponses["command -v tmux"] = { ok: true, stdout: "tmux", stderr: "" }; + mockCommandResponses["command -v opencode"] = { ok: true, stdout: "opencode", stderr: "" }; + mockCommandResponses["opencode models"] = { ok: true, stdout: "openai/gpt-5.2", stderr: "" }; + mockCommandResponses[`tmux has-session -t ${session}`] = { ok: true, stdout: "", stderr: "" }; + + const result = await openMultiModel({ sessionName: session, models: ["openai/gpt-5.2"], binaryName: "opencode", mode: "cli" }); + expect(result.success).toBe(false); + expect(result.error).toContain("Session already exists"); + }); + + test("second model fails because branch already exists", async () => { + const session = "sess-add"; + const binary = "opencode"; + const models = ["openai/gpt-5.2", "anthropic/gpt-5-2"]; + // common mocks + mockCommandResponses["git rev-parse --is-inside-work-tree"] = { ok: true, stdout: "true", stderr: "" }; + mockCommandResponses["command -v tmux"] = { ok: true, stdout: "tmux", stderr: "" }; + mockCommandResponses["command -v opencode"] = { ok: true, stdout: "opencode", stderr: "" }; + mockCommandResponses["opencode models"] = { ok: true, stdout: models.join("\n"), stderr: "" }; + mockCommandResponses[`tmux has-session -t ${session}`] = { ok: false, stdout: "", stderr: "" }; + // first model success + const firstBranch = `opencode/${session}/gpt-5-2`; + const firstPath = `/home/user/.local/share/opencode/multi-model/${session}/gpt-5-2`; + mockCommandResponses[`git show-ref --verify --quiet refs/heads/${firstBranch}`] = { ok: false, stdout: "", stderr: "" }; + mockCommandResponses[`git worktree add -b ${firstBranch} ${firstPath}`] = { ok: true, stdout: "", stderr: "" }; + mockCommandResponses[`tmux new-session -d -s ${session} -n gpt-5-2 -c ${firstPath}`] = { ok: true, stdout: "", stderr: "" }; + mockCommandResponses[`tmux send-keys -t ${session}:gpt-5-2 ${binary} --model 'openai/gpt-5.2' C-m`] = { ok: true, stdout: "", stderr: "" }; + // second model branch exists + const secondBranch = `opencode/${session}/gpt-5-2-2`; + mockCommandResponses[`git show-ref --verify --quiet refs/heads/${secondBranch}`] = { ok: true, stdout: "", stderr: "" }; + const result = await openMultiModel({ sessionName: session, models, binaryName: binary, mode: "cli" }); + expect(result.success).toBe(true); + expect(result.instructions).toContain("Failed: anthropic/gpt-5-2 (Branch"); + expect(result.windows).toContain("gpt-5-2"); + expect(result.windows).toContain("gpt-5-2-2"); + }); + + test("second model worktree creation fails", async () => { + const session = "sess-wt"; + const binary = "opencode"; + const models = ["openai/gpt-5.2", "anthropic/gpt-5-2"]; + mockCommandResponses["git rev-parse --is-inside-work-tree"] = { ok: true, stdout: "true", stderr: "" }; + mockCommandResponses["command -v tmux"] = { ok: true, stdout: "tmux", stderr: "" }; + mockCommandResponses["command -v opencode"] = { ok: true, stdout: "opencode", stderr: "" }; + mockCommandResponses["opencode models"] = { ok: true, stdout: models.join("\n"), stderr: "" }; + mockCommandResponses[`tmux has-session -t ${session}`] = { ok: false, stdout: "", stderr: "" }; + // first model success + const firstBranch = `opencode/${session}/gpt-5-2`; + const firstPath = `/home/user/.local/share/opencode/multi-model/${session}/gpt-5-2`; + mockCommandResponses[`git show-ref --verify --quiet refs/heads/${firstBranch}`] = { ok: false, stdout: "", stderr: "" }; + mockCommandResponses[`git worktree add -b ${firstBranch} ${firstPath}`] = { ok: true, stdout: "", stderr: "" }; + mockCommandResponses[`tmux new-session -d -s ${session} -n gpt-5-2 -c ${firstPath}`] = { ok: true, stdout: "", stderr: "" }; + mockCommandResponses[`tmux send-keys -t ${session}:gpt-5-2 ${binary} --model 'openai/gpt-5.2' C-m`] = { ok: true, stdout: "", stderr: "" }; + // second model fails worktree + const secondBranch = `opencode/${session}/gpt-5-2-2`; + const secondPath = `/home/user/.local/share/opencode/multi-model/${session}/gpt-5-2-2`; + mockCommandResponses[`git show-ref --verify --quiet refs/heads/${secondBranch}`] = { ok: false, stdout: "", stderr: "" }; + mockCommandResponses[`git worktree add -b ${secondBranch} ${secondPath}`] = { ok: false, stdout: "", stderr: "wt error" }; + mockCommandResponses[`git worktree remove -f ${secondPath}`] = { ok: true, stdout: "", stderr: "" }; + mockCommandResponses[`git branch -D ${secondBranch}`] = { ok: true, stdout: "", stderr: "" }; + const result = await openMultiModel({ sessionName: session, models, binaryName: binary, mode: "cli" }); + expect(result.success).toBe(true); + expect(result.instructions).toContain("Error: Created tmux session"); expect(result.instructions).toContain("Failed: anthropic/gpt-5-2"); + expect(result.windows).toContain("gpt-5-2"); + expect(result.windows).toContain("gpt-5-2-2"); + }); + + test("second model window creation fails", async () => { + const session = "sess-win"; + const binary = "opencode"; + const models = ["openai/gpt-5.2", "anthropic/gpt-5-2"]; + mockCommandResponses["git rev-parse --is-inside-work-tree"] = { ok: true, stdout: "true", stderr: "" }; + mockCommandResponses["command -v tmux"] = { ok: true, stdout: "tmux", stderr: "" }; + mockCommandResponses["command -v opencode"] = { ok: true, stdout: "opencode", stderr: "" }; + mockCommandResponses["opencode models"] = { ok: true, stdout: models.join("\n"), stderr: "" }; + mockCommandResponses[`tmux has-session -t ${session}`] = { ok: false, stdout: "", stderr: "" }; + // first model success + const firstBranch = `opencode/${session}/gpt-5-2`; + const firstPath = `/home/user/.local/share/opencode/multi-model/${session}/gpt-5-2`; + mockCommandResponses[`git show-ref --verify --quiet refs/heads/${firstBranch}`] = { ok: false, stdout: "", stderr: "" }; + mockCommandResponses[`git worktree add -b ${firstBranch} ${firstPath}`] = { ok: true, stdout: "", stderr: "" }; + mockCommandResponses[`tmux new-session -d -s ${session} -n gpt-5-2 -c ${firstPath}`] = { ok: true, stdout: "", stderr: "" }; + mockCommandResponses[`tmux send-keys -t ${session}:gpt-5-2 ${binary} --model 'openai/gpt-5.2' C-m`] = { ok: true, stdout: "", stderr: "" }; + // second model window failure + const secondBranch = `opencode/${session}/gpt-5-2-2`; + const secondPath = `/home/user/.local/share/opencode/multi-model/${session}/gpt-5-2-2`; + mockCommandResponses[`git show-ref --verify --quiet refs/heads/${secondBranch}`] = { ok: false, stdout: "", stderr: "" }; + mockCommandResponses[`git worktree add -b ${secondBranch} ${secondPath}`] = { ok: true, stdout: "", stderr: "" }; + mockCommandResponses[`tmux new-window -d -t ${session} -n gpt-5-2-2 -c ${secondPath}`] = { ok: false, stdout: "", stderr: "win err" }; + mockCommandResponses[`git worktree remove -f ${secondPath}`] = { ok: true, stdout: "", stderr: "" }; + mockCommandResponses[`git branch -D ${secondBranch}`] = { ok: true, stdout: "", stderr: "" }; + const result = await openMultiModel({ sessionName: session, models, binaryName: binary, mode: "cli" }); + expect(result.success).toBe(true); + expect(result.instructions).toContain("Error: Created tmux session"); expect(result.instructions).toContain("Failed: anthropic/gpt-5-2"); + expect(result.windows).toContain("gpt-5-2"); + expect(result.windows).toContain("gpt-5-2-2"); + }); + + test("second model launch command fails", async () => { + const session = "sess-launch"; + const binary = "opencode"; + const models = ["openai/gpt-5.2", "anthropic/gpt-5-2"]; + mockCommandResponses["git rev-parse --is-inside-work-tree"] = { ok: true, stdout: "true", stderr: "" }; + mockCommandResponses["command -v tmux"] = { ok: true, stdout: "tmux", stderr: "" }; + mockCommandResponses["command -v opencode"] = { ok: true, stdout: "opencode", stderr: "" }; + mockCommandResponses["opencode models"] = { ok: true, stdout: models.join("\n"), stderr: "" }; + mockCommandResponses[`tmux has-session -t ${session}`] = { ok: false, stdout: "", stderr: "" }; + // first model success + const firstBranch = `opencode/${session}/gpt-5-2`; + const firstPath = `/home/user/.local/share/opencode/multi-model/${session}/gpt-5-2`; + mockCommandResponses[`git show-ref --verify --quiet refs/heads/${firstBranch}`] = { ok: false, stdout: "", stderr: "" }; + mockCommandResponses[`git worktree add -b ${firstBranch} ${firstPath}`] = { ok: true, stdout: "", stderr: "" }; + mockCommandResponses[`tmux new-session -d -s ${session} -n gpt-5-2 -c ${firstPath}`] = { ok: true, stdout: "", stderr: "" }; + mockCommandResponses[`tmux send-keys -t ${session}:gpt-5-2 ${binary} --model 'openai/gpt-5.2' C-m`] = { ok: true, stdout: "", stderr: "" }; + // second model launch fails + const secondBranch = `opencode/${session}/gpt-5-2-2`; + const secondPath = `/home/user/.local/share/opencode/multi-model/${session}/gpt-5-2-2`; + mockCommandResponses[`git show-ref --verify --quiet refs/heads/${secondBranch}`] = { ok: false, stdout: "", stderr: "" }; + mockCommandResponses[`git worktree add -b ${secondBranch} ${secondPath}`] = { ok: true, stdout: "", stderr: "" }; + mockCommandResponses[`tmux new-window -d -t ${session} -n gpt-5-2-2 -c ${secondPath}`] = { ok: true, stdout: "", stderr: "" }; + mockCommandResponses[`tmux send-keys -t ${session}:gpt-5-2-2 ${binary} --model 'anthropic/gpt-5-2' C-m`] = { ok: false, stdout: "", stderr: "launch err" }; + mockCommandResponses[`git worktree remove -f ${secondPath}`] = { ok: true, stdout: "", stderr: "" }; + mockCommandResponses[`git branch -D ${secondBranch}`] = { ok: true, stdout: "", stderr: "" }; + const result = await openMultiModel({ sessionName: session, models, binaryName: binary, mode: "cli" }); + expect(result.success).toBe(true); + expect(result.instructions).toContain("Failed: anthropic/gpt-5-2 (launch err"); + expect(result.windows).toContain("gpt-5-2"); + expect(result.windows).toContain("gpt-5-2-2"); + }); + + });