From 5a32691bc22b40a85a038742dd41bd373533b963 Mon Sep 17 00:00:00 2001 From: bendtherules Date: Fri, 13 Mar 2026 17:51:35 +0530 Subject: [PATCH] build: remaining testcases --- .opencode/multi-model.test.ts | 362 +++++++++++++++++- .../1773342290743-silent-harbor.md | 0 .opencode/tools/multi-model.ts | 16 + 3 files changed, 370 insertions(+), 8 deletions(-) rename .opencode/plans/{ => archive}/1773342290743-silent-harbor.md (100%) diff --git a/.opencode/multi-model.test.ts b/.opencode/multi-model.test.ts index 6993907..d04f9e1 100644 --- a/.opencode/multi-model.test.ts +++ b/.opencode/multi-model.test.ts @@ -1,16 +1,19 @@ import { expect, test, mock, spyOn, beforeEach, afterEach, describe } from "bun:test"; import * as fs from "node:fs"; import * as os from "node:os"; -import multiModelTool from "./tools/multi-model"; +import multiModelTool, { __testing_helpers } from "./tools/multi-model"; 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 commandSignature = parts.join(" "); + executedCommands.push(commandSignature); + return { quiet: () => ({ nothrow: async () => { @@ -41,6 +44,7 @@ describe("multi-model tool", () => { 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: "" }, @@ -63,6 +67,7 @@ describe("multi-model tool", () => { ); expect(result).toContain("Error: `sessionName` must not be empty."); + expect(executedCommands).toEqual([]); }); test("fails if no models are provided", async () => { @@ -72,6 +77,7 @@ describe("multi-model tool", () => { ); expect(result).toContain("Error: Model names must not be empty."); + expect(executedCommands).toEqual([]); }); test("fails if duplicate models are provided", async () => { @@ -81,6 +87,7 @@ describe("multi-model tool", () => { ); expect(result).toContain("Duplicate model names are not allowed"); + expect(executedCommands).toEqual([]); }); test("fails if not inside a git repository", async () => { @@ -92,6 +99,7 @@ describe("multi-model tool", () => { ); expect(result).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 () => { @@ -103,6 +111,10 @@ describe("multi-model tool", () => { ); expect(result).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 () => { @@ -114,6 +126,11 @@ describe("multi-model tool", () => { ); expect(result).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 () => { @@ -123,6 +140,12 @@ describe("multi-model tool", () => { ); expect(result).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 () => { @@ -134,6 +157,13 @@ describe("multi-model tool", () => { ); expect(result).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 () => { @@ -143,6 +173,8 @@ describe("multi-model tool", () => { ); expect(result).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-4o"))).toBe(true); + expect(executedCommands.some(c => c.startsWith("tmux send-keys -t test-session:gpt-4o"))).toBe(true); }); test("successfully launches multiple models", async () => { @@ -154,10 +186,13 @@ describe("multi-model tool", () => { ); expect(result).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-4o"))).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-4o"] = { ok: false, stdout: "", stderr: "" }; const result = await multiModelTool.execute( { sessionName: "test session!", models: ["openai/gpt-4o"] }, @@ -165,17 +200,328 @@ describe("multi-model tool", () => { ); expect(result).toContain("Use `tmux attach -t test session!` to join session"); + expect(executedCommands.some(c => c.startsWith("git worktree add -b opencode/test-session/gpt-4o"))).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-4o -c /mock/home/.local/share/opencode/multi-model/test-session/gpt-4o"] = { ok: false, stdout: "", stderr: "tmux error" }; + mockCommandResponses["git worktree remove -f /mock/home/.local/share/opencode/multi-model/test-session/gpt-4o"] = { ok: true, stdout: "", stderr: "" }; + mockCommandResponses["git branch -D opencode/test-session/gpt-4o"] = { ok: true, stdout: "", stderr: "" }; + + const result = await multiModelTool.execute( + { sessionName: "test-session", models: ["openai/gpt-4o"] }, + { metadata: mock() } as any + ); + + expect(result).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-4o", + "git worktree add -b opencode/test-session/gpt-4o /mock/home/.local/share/opencode/multi-model/test-session/gpt-4o", + "tmux new-session -d -s test-session -n gpt-4o -c /mock/home/.local/share/opencode/multi-model/test-session/gpt-4o", + "git worktree remove -f /mock/home/.local/share/opencode/multi-model/test-session/gpt-4o", + "git branch -D opencode/test-session/gpt-4o" + ]); + }); + + test("fails if worktree path exists for first model", async () => { + existsSyncMock.mockImplementation((path: string) => { + return path.includes("test-session/gpt-4o"); + }); + + const result = await multiModelTool.execute( + { sessionName: "test-session", models: ["openai/gpt-4o"] }, + { metadata: mock() } as any + ); + + expect(result).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-4o"] = { ok: true, stdout: "", stderr: "" }; + + const result = await multiModelTool.execute( + { sessionName: "test-session", models: ["openai/gpt-4o"] }, + { metadata: mock() } as any + ); + + expect(result).toContain("Branch 'opencode/test-session/gpt-4o' 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-4o" + ]); + }); + + test("fails if git worktree add fails for first model", async () => { + mockCommandResponses["git worktree add -b opencode/test-session/gpt-4o /mock/home/.local/share/opencode/multi-model/test-session/gpt-4o"] = { ok: false, stdout: "", stderr: "git error" }; + + const result = await multiModelTool.execute( + { sessionName: "test-session", models: ["openai/gpt-4o"] }, + { metadata: mock() } as any + ); + + expect(result).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-4o", + "git worktree add -b opencode/test-session/gpt-4o /mock/home/.local/share/opencode/multi-model/test-session/gpt-4o" + ]); + }); + + test("model name collision creates unique window names", async () => { + mockCommandResponses["opencode models"] = { ok: true, stdout: "openai/gpt-4o\nanthropic/gpt-4o", stderr: "" }; + mockCommandResponses["git show-ref --verify --quiet refs/heads/opencode/test-session/gpt-4o"] = { ok: false, stdout: "", stderr: "" }; + mockCommandResponses["git show-ref --verify --quiet refs/heads/opencode/test-session/gpt-4o-2"] = { ok: false, stdout: "", stderr: "" }; + + const result = await multiModelTool.execute( + { sessionName: "test-session", models: ["openai/gpt-4o", "anthropic/gpt-4o"] }, + { metadata: mock() } as any + ); + + expect(result).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-4o"))).toBe(true); + expect(executedCommands.some(c => c.startsWith("tmux new-window -d -t test-session -n gpt-4o-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 multiModelTool.execute( + { sessionName: "test-session", models: [longModel] }, + { metadata: mock() } as any + ); + + expect(result).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); }); }); -describe("unit functions", () => { - test("shellQuote escapes single quotes", () => { - const value = "test'value"; - const quoted = "'" + value.replace(/'/g, `'"'"'`) + "'"; - expect(quoted).toBe("'test'\"'\"'value'"); +describe("__testing_helpers", () => { + describe("shellQuote", () => { + test("normal string without special characters", () => { + expect(__testing_helpers.shellQuote("test")).toBe("'test'"); + }); + + test("string with spaces", () => { + expect(__testing_helpers.shellQuote("test value")).toBe("'test value'"); + }); + + test("string with single quotes", () => { + expect(__testing_helpers.shellQuote("test'value")).toBe("'test'\"'\"'value'"); + }); + + test("string with multiple single quotes", () => { + expect(__testing_helpers.shellQuote("te's't'v'alue")).toBe("'te'\"'\"'s'\"'\"'t'\"'\"'v'\"'\"'alue'"); + }); + + test("empty string", () => { + expect(__testing_helpers.shellQuote("")).toBe("''"); + }); }); - test("normalizeModels trims and filters empty", () => { - expect([" openai/gpt-4o ", ""].map(m => m.trim()).filter(Boolean)).toEqual(["openai/gpt-4o"]); + describe("normalizeModels", () => { + test("empty array", () => { + expect(__testing_helpers.normalizeModels([])).toEqual([]); + }); + + test("undefined input", () => { + expect(__testing_helpers.normalizeModels(undefined)).toEqual([]); + }); + + test("array with whitespace-only strings", () => { + expect(__testing_helpers.normalizeModels([" ", " "])).toEqual([]); + }); + + test("array with mixed valid models, padded models, and empty strings", () => { + expect(__testing_helpers.normalizeModels([" openai/gpt-4o ", "", " anthropic/claude "])).toEqual(["openai/gpt-4o", "anthropic/claude"]); + }); + }); + + describe("findDuplicates", () => { + test("array with no duplicates", () => { + expect(__testing_helpers.findDuplicates(["a", "b", "c"])).toEqual([]); + }); + + test("array with one duplicate pair", () => { + expect(__testing_helpers.findDuplicates(["a", "b", "a"])).toEqual(["a"]); + }); + + test("array with multiple different duplicates", () => { + expect(__testing_helpers.findDuplicates(["a", "b", "a", "c", "b"])).toEqual(["a", "b"]); + }); + + test("array with a single value repeated more than twice", () => { + expect(__testing_helpers.findDuplicates(["a", "a", "a"])).toEqual(["a"]); + }); + + test("verifying first-repeated-occurrence order", () => { + expect(__testing_helpers.findDuplicates(["x", "a", "b", "a", "x", "b"])).toEqual(["a", "x", "b"]); + }); + }); + + describe("createWindowBaseName", () => { + test("normal model name without slashes", () => { + expect(__testing_helpers.createWindowBaseName("gpt-4o")).toBe("gpt-4o"); + }); + + test("model with slashes", () => { + expect(__testing_helpers.createWindowBaseName("vendor/namespace/model")).toBe("model"); + }); + + test("model exceeding WINDOW_NAME_LIMIT", () => { + const longName = "a".repeat(30); + expect(__testing_helpers.createWindowBaseName(longName)).toBe("aaaaaaaaaaaaaaaaaaaaaaaa"); + }); + + test("model with special characters", () => { + expect(__testing_helpers.createWindowBaseName("OpenAI/GPT_4.0!")).toBe("gpt-4-0"); + }); + + test("model made entirely of special characters", () => { + expect(__testing_helpers.createWindowBaseName("!@#$%^&*()")).toBe("model"); + }); + }); + + describe("createWindowPlans", () => { + test("single model", () => { + expect(__testing_helpers.createWindowPlans(["openai/gpt-4o"])).toEqual([{ model: "openai/gpt-4o", windowName: "gpt-4o" }]); + }); + + test("two models with the same base name", () => { + expect(__testing_helpers.createWindowPlans(["openai/gpt-4o", "anthropic/gpt-4o"])).toEqual([ + { model: "openai/gpt-4o", windowName: "gpt-4o" }, + { model: "anthropic/gpt-4o", windowName: "gpt-4o-2" } + ]); + }); + + test("multiple collisions", () => { + const result = __testing_helpers.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 = __testing_helpers.createWindowPlans([longBase, "b"]); + expect(result[1]?.windowName.length).toBeLessThanOrEqual(24); + }); + }); + + describe("levenshtein", () => { + test("identical strings", () => { + expect(__testing_helpers.levenshtein("test", "test")).toBe(0); + }); + + test("one substitution", () => { + expect(__testing_helpers.levenshtein("test", "tent")).toBe(1); + }); + + test("one insertion", () => { + expect(__testing_helpers.levenshtein("test", "tests")).toBe(1); + }); + + test("one deletion", () => { + expect(__testing_helpers.levenshtein("tests", "test")).toBe(1); + }); + + test("completely different strings", () => { + expect(__testing_helpers.levenshtein("abc", "xyz")).toBe(3); + }); + + test("one empty string", () => { + expect(__testing_helpers.levenshtein("", "test")).toBe(4); + }); + + test("both empty strings", () => { + expect(__testing_helpers.levenshtein("", "")).toBe(0); + }); + }); + + describe("suggestModels", () => { + test("exact match", () => { + const result = __testing_helpers.suggestModels("gpt-4o", ["gpt-4o", "gpt-4o-mini"]); + expect(result[0]).toBe("gpt-4o"); + }); + + test("fuzzy match/typo", () => { + expect(__testing_helpers.suggestModels("gpt5.4", ["gpt-5.4"])).toEqual(["gpt-5.4"]); + }); + + test("fuzzy match with contains boost", () => { + const suggestions = __testing_helpers.suggestModels("gpt4o", ["gpt-4o", "gpt-4o-mini"]); + expect(suggestions[0]).toBe("gpt-4o"); + }); + + test("maximum of 3 suggestions", () => { + const allowlist = ["gpt-4o", "gpt-4o-mini", "gpt-4o-pro", "gpt-4o-ultra"]; + expect(__testing_helpers.suggestModels("gpt4o", allowlist).length).toBe(3); + }); + + test("sorting logic", () => { + const suggestions = __testing_helpers.suggestModels("abc", ["abc", "abd", "abe"]); + expect(suggestions).toEqual(["abc", "abd", "abe"]); + }); + }); + + describe("formatInvalidModelError", () => { + test("single invalid model with available suggestions", () => { + const error = __testing_helpers.formatInvalidModelError(["gpt5.4"], ["gpt-5.4", "gpt-4o"]); + expect(error).toContain("Model name 'gpt5.4' not found"); + expect(error).toContain("Did you mean"); + }); + + test("single invalid model without close matches", () => { + const error = __testing_helpers.formatInvalidModelError(["xyz"], ["abc", "def"]); + expect(error).toContain("Model name 'xyz' not found"); + expect(error).toContain("Did you mean"); + }); + + test("multiple invalid models", () => { + const error = __testing_helpers.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(__testing_helpers.sanitizeName("test-session")).toBe("test-session"); + }); + + test("name with spaces and special chars", () => { + expect(__testing_helpers.sanitizeName("test session!")).toBe("test-session"); + }); + + test("name with consecutive hyphens", () => { + expect(__testing_helpers.sanitizeName("test--session")).toBe("test-session"); + }); + + test("name with leading/trailing hyphens", () => { + expect(__testing_helpers.sanitizeName("-test-session-")).toBe("test-session"); + }); }); }); diff --git a/.opencode/plans/1773342290743-silent-harbor.md b/.opencode/plans/archive/1773342290743-silent-harbor.md similarity index 100% rename from .opencode/plans/1773342290743-silent-harbor.md rename to .opencode/plans/archive/1773342290743-silent-harbor.md diff --git a/.opencode/tools/multi-model.ts b/.opencode/tools/multi-model.ts index b23316f..2a4e015 100644 --- a/.opencode/tools/multi-model.ts +++ b/.opencode/tools/multi-model.ts @@ -482,3 +482,19 @@ export default tool({ ].join("\n"); }, }); + +export const __testing_helpers = { + shellQuote, + normalizeModels, + findDuplicates, + createWindowBaseName, + createWindowPlans, + levenshtein, + suggestModels, + formatInvalidModelError, + sanitizeName, + runCommand, + getWorktreePath, + undoWorktree, + launchModelInWindow, +};