mirror of
https://github.com/bendtherules/opencode-multi-model.git
synced 2026-08-18 13:42:21 +00:00
182 lines
6.3 KiB
TypeScript
182 lines
6.3 KiB
TypeScript
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";
|
|
|
|
let originalBun$: typeof Bun.$ = (globalThis as any).Bun?.$;
|
|
let mockCommandResponses: Record<string, { ok: boolean; stdout: string; stderr: string }> = {};
|
|
|
|
function setupMockBun$() {
|
|
const mockFn = mock((strings: TemplateStringsArray, ...values: any[]) => {
|
|
const parts = values[0] as string[];
|
|
const commandSignature = parts.join(" ");
|
|
|
|
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 tool", () => {
|
|
let existsSyncMock: ReturnType<typeof spyOn>;
|
|
let bunMock: ReturnType<typeof setupMockBun$>;
|
|
|
|
beforeEach(() => {
|
|
spyOn(os, "homedir").mockReturnValue("/mock/home");
|
|
existsSyncMock = spyOn(fs, "existsSync").mockReturnValue(false);
|
|
bunMock = setupMockBun$();
|
|
|
|
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-4o\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-4o": { ok: false, stdout: "", stderr: "" },
|
|
};
|
|
});
|
|
|
|
afterEach(() => {
|
|
restoreOriginalBun$();
|
|
});
|
|
|
|
test("fails if session name is empty", async () => {
|
|
const result = await multiModelTool.execute(
|
|
{ sessionName: "", models: ["openai/gpt-4o"] },
|
|
{ metadata: mock() } as any
|
|
);
|
|
|
|
expect(result).toContain("Error: `sessionName` must not be empty.");
|
|
});
|
|
|
|
test("fails if no models are provided", async () => {
|
|
const result = await multiModelTool.execute(
|
|
{ sessionName: "test-session", models: [] },
|
|
{ metadata: mock() } as any
|
|
);
|
|
|
|
expect(result).toContain("Error: Model names must not be empty.");
|
|
});
|
|
|
|
test("fails if duplicate models are provided", async () => {
|
|
const result = await multiModelTool.execute(
|
|
{ sessionName: "test-session", models: ["openai/gpt-4o", "openai/gpt-4o"] },
|
|
{ metadata: mock() } as any
|
|
);
|
|
|
|
expect(result).toContain("Duplicate model names are not allowed");
|
|
});
|
|
|
|
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 multiModelTool.execute(
|
|
{ sessionName: "test-session", models: ["openai/gpt-4o"] },
|
|
{ metadata: mock() } as any
|
|
);
|
|
|
|
expect(result).toContain("Error: `multi-model` requires a git repository.");
|
|
});
|
|
|
|
test("fails if tmux is not installed", async () => {
|
|
mockCommandResponses["command -v tmux"] = { ok: false, stdout: "", stderr: "" };
|
|
|
|
const result = await multiModelTool.execute(
|
|
{ sessionName: "test-session", models: ["openai/gpt-4o"] },
|
|
{ metadata: mock() } as any
|
|
);
|
|
|
|
expect(result).toContain("Error: `tmux` is not installed or not on `PATH`.");
|
|
});
|
|
|
|
test("fails if opencode is not installed", async () => {
|
|
mockCommandResponses["command -v opencode"] = { ok: false, stdout: "", stderr: "" };
|
|
|
|
const result = await multiModelTool.execute(
|
|
{ sessionName: "test-session", models: ["openai/gpt-4o"] },
|
|
{ metadata: mock() } as any
|
|
);
|
|
|
|
expect(result).toContain("Error: `opencode` is not installed or not on `PATH`.");
|
|
});
|
|
|
|
test("fails if model name is not in allowlist", async () => {
|
|
const result = await multiModelTool.execute(
|
|
{ sessionName: "test-session", models: ["invalid/model"] },
|
|
{ metadata: mock() } as any
|
|
);
|
|
|
|
expect(result).toContain("Error: Model name 'invalid/model' not found");
|
|
});
|
|
|
|
test("fails if session already exists", async () => {
|
|
mockCommandResponses["tmux has-session -t test-session"] = { ok: true, stdout: "", stderr: "" };
|
|
|
|
const result = await multiModelTool.execute(
|
|
{ sessionName: "test-session", models: ["openai/gpt-4o"] },
|
|
{ metadata: mock() } as any
|
|
);
|
|
|
|
expect(result).toContain("Error: Session already exists");
|
|
});
|
|
|
|
test("successfully plans and executes tmux launch with single model", async () => {
|
|
const result = await multiModelTool.execute(
|
|
{ sessionName: "test-session", models: ["openai/gpt-4o"] },
|
|
{ metadata: mock() } as any
|
|
);
|
|
|
|
expect(result).toContain("Use `tmux attach -t test-session` to join session");
|
|
});
|
|
|
|
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 multiModelTool.execute(
|
|
{ sessionName: "test-session", models: ["openai/gpt-4o", "anthropic/claude-3-5-sonnet"] },
|
|
{ metadata: mock() } as any
|
|
);
|
|
|
|
expect(result).toContain("Use `tmux attach -t test-session` to join session");
|
|
});
|
|
|
|
test("sanitizes session name", async () => {
|
|
mockCommandResponses["tmux has-session -t test session!"] = { ok: false, stdout: "", stderr: "" };
|
|
|
|
const result = await multiModelTool.execute(
|
|
{ sessionName: "test session!", models: ["openai/gpt-4o"] },
|
|
{ metadata: mock() } as any
|
|
);
|
|
|
|
expect(result).toContain("Use `tmux attach -t test session!` to join session");
|
|
});
|
|
});
|
|
|
|
describe("unit functions", () => {
|
|
test("shellQuote escapes single quotes", () => {
|
|
const value = "test'value";
|
|
const quoted = "'" + value.replace(/'/g, `'"'"'`) + "'";
|
|
expect(quoted).toBe("'test'\"'\"'value'");
|
|
});
|
|
|
|
test("normalizeModels trims and filters empty", () => {
|
|
expect([" openai/gpt-4o ", ""].map(m => m.trim()).filter(Boolean)).toEqual(["openai/gpt-4o"]);
|
|
});
|
|
});
|