mirror of
https://github.com/bendtherules/opencode-multi-model.git
synced 2026-08-18 13:42:21 +00:00
build: add biome formatter and linter
This commit is contained in:
+302
-75
@@ -1,10 +1,21 @@
|
||||
import { expect, test, mock, spyOn, beforeEach, afterEach, describe } from "bun:test";
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
mock,
|
||||
spyOn,
|
||||
test,
|
||||
} from "bun:test";
|
||||
import * as os from "node:os";
|
||||
import * as readline from "node:readline";
|
||||
import { closeMultiModel } from "../src/core/close";
|
||||
|
||||
let originalBun$: typeof Bun.$ = (globalThis as any).Bun?.$;
|
||||
let mockCommandResponses: Record<string, { ok: boolean; stdout: string; stderr: string }> = {};
|
||||
const originalBun$: typeof Bun.$ = (globalThis as any).Bun?.$;
|
||||
let mockCommandResponses: Record<
|
||||
string,
|
||||
{ ok: boolean; stdout: string; stderr: string }
|
||||
> = {};
|
||||
let executedCommands: string[] = [];
|
||||
|
||||
function setupMockBun$() {
|
||||
@@ -12,7 +23,11 @@ function setupMockBun$() {
|
||||
const parts = values[0] as string[];
|
||||
const commandSignature = parts.join(" ");
|
||||
executedCommands.push(commandSignature);
|
||||
const response = mockCommandResponses[commandSignature] ?? { ok: true, stdout: "", stderr: "" };
|
||||
const response = mockCommandResponses[commandSignature] ?? {
|
||||
ok: true,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
};
|
||||
return {
|
||||
quiet: () => ({
|
||||
nothrow: async () => ({
|
||||
@@ -46,12 +61,28 @@ describe("closeMultiModel", () => {
|
||||
});
|
||||
|
||||
test("kills existing session without cleanup", async () => {
|
||||
mockCommandResponses["tmux has-session -t test-session"] = { ok: true, stdout: "", stderr: "" };
|
||||
mockCommandResponses["tmux kill-session -t test-session"] = { ok: true, stdout: "", stderr: "" };
|
||||
mockCommandResponses["tmux has-session -t test-session"] = {
|
||||
ok: true,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
};
|
||||
mockCommandResponses["tmux kill-session -t test-session"] = {
|
||||
ok: true,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
};
|
||||
// No worktrees
|
||||
mockCommandResponses["git worktree list --porcelain"] = { ok: true, stdout: "", stderr: "" };
|
||||
mockCommandResponses["git worktree list --porcelain"] = {
|
||||
ok: true,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
};
|
||||
|
||||
const result = await closeMultiModel({ sessionName: "test-session", cleanupWorktrees: false, force: false });
|
||||
const result = await closeMultiModel({
|
||||
sessionName: "test-session",
|
||||
cleanupWorktrees: false,
|
||||
force: false,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.cleanupPerformed).toBe(false);
|
||||
expect(result.worktreesRemoved).toEqual([]);
|
||||
@@ -60,37 +91,82 @@ describe("closeMultiModel", () => {
|
||||
});
|
||||
|
||||
test("no session exists, no cleanup", async () => {
|
||||
mockCommandResponses["tmux has-session -t test-session"] = { ok: false, stdout: "", stderr: "" };
|
||||
mockCommandResponses["git worktree list --porcelain"] = { ok: true, stdout: "", stderr: "" };
|
||||
mockCommandResponses["tmux has-session -t test-session"] = {
|
||||
ok: false,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
};
|
||||
mockCommandResponses["git worktree list --porcelain"] = {
|
||||
ok: true,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
};
|
||||
|
||||
const result = await closeMultiModel({ sessionName: "test-session", cleanupWorktrees: false, force: false });
|
||||
const result = await closeMultiModel({
|
||||
sessionName: "test-session",
|
||||
cleanupWorktrees: false,
|
||||
force: false,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.cleanupPerformed).toBe(false);
|
||||
expect(result.worktreesRemoved).toEqual([]);
|
||||
expect(executedCommands).toContain("tmux has-session -t test-session");
|
||||
// No kill-session should be issued
|
||||
expect(executedCommands.some(c => c.startsWith("tmux kill-session"))).toBe(false);
|
||||
expect(
|
||||
executedCommands.some((c) => c.startsWith("tmux kill-session")),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("cleanup works with worktrees and force, all succeeds", async () => {
|
||||
mockCommandResponses["tmux has-session -t test-session"] = { ok: false, stdout: "", stderr: "" };
|
||||
const worktreePath = "/home/user/.local/share/opencode/multi-model/test-session/model";
|
||||
mockCommandResponses["tmux has-session -t test-session"] = {
|
||||
ok: false,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
};
|
||||
const worktreePath =
|
||||
"/home/user/.local/share/opencode/multi-model/test-session/model";
|
||||
const worktreeListOutput = `worktree ${worktreePath}\nbranch refs/heads/opencode/test-session/model`;
|
||||
mockCommandResponses["git worktree list --porcelain"] = { ok: true, stdout: worktreeListOutput, stderr: "" };
|
||||
mockCommandResponses[`git worktree remove -f ${worktreePath}`] = { ok: true, stdout: "", stderr: "" };
|
||||
mockCommandResponses["git branch -D opencode/test-session/model"] = { ok: true, stdout: "", stderr: "" };
|
||||
mockCommandResponses["git worktree list --porcelain"] = {
|
||||
ok: true,
|
||||
stdout: worktreeListOutput,
|
||||
stderr: "",
|
||||
};
|
||||
mockCommandResponses[`git worktree remove -f ${worktreePath}`] = {
|
||||
ok: true,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
};
|
||||
mockCommandResponses["git branch -D opencode/test-session/model"] = {
|
||||
ok: true,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
};
|
||||
// Tag command will default to ok:true via fallback
|
||||
mockCommandResponses["rm -rf /home/user/.local/share/opencode/multi-model/test-session"] = { ok: true, stdout: "", stderr: "" };
|
||||
mockCommandResponses[
|
||||
"rm -rf /home/user/.local/share/opencode/multi-model/test-session"
|
||||
] = { ok: true, stdout: "", stderr: "" };
|
||||
|
||||
// Freeze timestamp for deterministic tag name
|
||||
const fixedDate = new Date("2023-01-01T00:00:00.000Z");
|
||||
const OriginalDate = Date;
|
||||
// @ts-ignore
|
||||
global.Date = class extends OriginalDate { constructor() { super(); return fixedDate; } toISOString() { return "2023-01-01T00:00:00.000Z"; } } as any;
|
||||
// @ts-expect-error
|
||||
global.Date = class extends OriginalDate {
|
||||
constructor() {
|
||||
super();
|
||||
return fixedDate;
|
||||
}
|
||||
toISOString() {
|
||||
return "2023-01-01T00:00:00.000Z";
|
||||
}
|
||||
} as any;
|
||||
|
||||
const result = await closeMultiModel({ sessionName: "test-session", cleanupWorktrees: true, force: true });
|
||||
const result = await closeMultiModel({
|
||||
sessionName: "test-session",
|
||||
cleanupWorktrees: true,
|
||||
force: true,
|
||||
});
|
||||
// Restore Date
|
||||
// @ts-ignore
|
||||
// @ts-expect-error
|
||||
global.Date = OriginalDate;
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
@@ -98,17 +174,30 @@ describe("closeMultiModel", () => {
|
||||
expect(result.worktreesRemoved).toEqual([worktreePath]);
|
||||
expect(result.branchesDeleted).toEqual(["opencode/test-session/model"]);
|
||||
expect(result.tagsCreated?.length).toBe(1);
|
||||
|
||||
|
||||
// Ensure removal commands were executed
|
||||
expect(executedCommands).toContain(`git worktree remove -f ${worktreePath}`);
|
||||
expect(executedCommands).toContain(`git branch -D opencode/test-session/model`);
|
||||
expect(executedCommands).toContain(
|
||||
`git worktree remove -f ${worktreePath}`,
|
||||
);
|
||||
expect(executedCommands).toContain(
|
||||
`git branch -D opencode/test-session/model`,
|
||||
);
|
||||
});
|
||||
|
||||
test("user cancels cleanup when not forced", async () => {
|
||||
mockCommandResponses["tmux has-session -t test-session"] = { ok: false, stdout: "", stderr: "" };
|
||||
const worktreePath = "/home/user/.local/share/opencode/multi-model/test-session/model";
|
||||
mockCommandResponses["tmux has-session -t test-session"] = {
|
||||
ok: false,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
};
|
||||
const worktreePath =
|
||||
"/home/user/.local/share/opencode/multi-model/test-session/model";
|
||||
const worktreeListOutput = `worktree ${worktreePath}\nbranch refs/heads/opencode/test-session/model`;
|
||||
mockCommandResponses["git worktree list --porcelain"] = { ok: true, stdout: worktreeListOutput, stderr: "" };
|
||||
mockCommandResponses["git worktree list --porcelain"] = {
|
||||
ok: true,
|
||||
stdout: worktreeListOutput,
|
||||
stderr: "",
|
||||
};
|
||||
|
||||
// Stub readline to return "n"
|
||||
const mockInterface = {
|
||||
@@ -117,16 +206,29 @@ describe("closeMultiModel", () => {
|
||||
} as any;
|
||||
spyOn(readline, "createInterface").mockImplementation(() => mockInterface);
|
||||
|
||||
const result = await closeMultiModel({ sessionName: "test-session", cleanupWorktrees: true, force: false });
|
||||
const result = await closeMultiModel({
|
||||
sessionName: "test-session",
|
||||
cleanupWorktrees: true,
|
||||
force: false,
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toBe("Cleanup cancelled by user");
|
||||
});
|
||||
|
||||
test("user confirms cleanup when not forced", async () => {
|
||||
mockCommandResponses["tmux has-session -t test-session"] = { ok: false, stdout: "", stderr: "" };
|
||||
const worktreePath = "/home/user/.local/share/opencode/multi-model/test-session/model";
|
||||
mockCommandResponses["tmux has-session -t test-session"] = {
|
||||
ok: false,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
};
|
||||
const worktreePath =
|
||||
"/home/user/.local/share/opencode/multi-model/test-session/model";
|
||||
const worktreeListOutput = `worktree ${worktreePath}\nbranch refs/heads/opencode/test-session/model`;
|
||||
mockCommandResponses["git worktree list --porcelain"] = { ok: true, stdout: worktreeListOutput, stderr: "" };
|
||||
mockCommandResponses["git worktree list --porcelain"] = {
|
||||
ok: true,
|
||||
stdout: worktreeListOutput,
|
||||
stderr: "",
|
||||
};
|
||||
|
||||
// Stub readline to simulate user confirming cleanup (answers "y")
|
||||
const mockInterface = {
|
||||
@@ -138,13 +240,25 @@ describe("closeMultiModel", () => {
|
||||
// Freeze timestamp for deterministic tag name
|
||||
const fixedDate = new Date("2023-01-01T00:00:00.000Z");
|
||||
const OriginalDate = Date;
|
||||
// @ts-ignore
|
||||
global.Date = class extends OriginalDate { constructor() { super(); return fixedDate; } toISOString() { return "2023-01-01T00:00:00.000Z"; } } as any;
|
||||
// @ts-expect-error
|
||||
global.Date = class extends OriginalDate {
|
||||
constructor() {
|
||||
super();
|
||||
return fixedDate;
|
||||
}
|
||||
toISOString() {
|
||||
return "2023-01-01T00:00:00.000Z";
|
||||
}
|
||||
} as any;
|
||||
|
||||
const result = await closeMultiModel({ sessionName: "test-session", cleanupWorktrees: true, force: false });
|
||||
const result = await closeMultiModel({
|
||||
sessionName: "test-session",
|
||||
cleanupWorktrees: true,
|
||||
force: false,
|
||||
});
|
||||
|
||||
// Restore Date after invocation
|
||||
// @ts-ignore
|
||||
// @ts-expect-error
|
||||
global.Date = OriginalDate;
|
||||
|
||||
// Verify result indicates successful cleanup
|
||||
@@ -153,38 +267,74 @@ describe("closeMultiModel", () => {
|
||||
expect(result.worktreesRemoved).toEqual([worktreePath]);
|
||||
expect(result.branchesDeleted).toEqual(["opencode/test-session/model"]);
|
||||
expect(result.tagsCreated?.length).toBe(1);
|
||||
|
||||
|
||||
// Verify the expected git commands were executed
|
||||
expect(executedCommands).toContain(`git worktree remove -f ${worktreePath}`);
|
||||
expect(executedCommands).toContain(`git branch -D opencode/test-session/model`);
|
||||
expect(executedCommands).toContain(
|
||||
`git worktree remove -f ${worktreePath}`,
|
||||
);
|
||||
expect(executedCommands).toContain(
|
||||
`git branch -D opencode/test-session/model`,
|
||||
);
|
||||
const expectedTagCmd = `git tag archive/opencode/test-session/model-2023-01-01T00-00-00-000Z opencode/test-session/model`;
|
||||
expect(executedCommands).toContain(expectedTagCmd);
|
||||
});
|
||||
|
||||
test("warning when tag creation fails", async () => {
|
||||
mockCommandResponses["tmux has-session -t test-session"] = { ok: false, stdout: "", stderr: "" };
|
||||
const worktreePath = "/home/user/.local/share/opencode/multi-model/test-session/model";
|
||||
mockCommandResponses["tmux has-session -t test-session"] = {
|
||||
ok: false,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
};
|
||||
const worktreePath =
|
||||
"/home/user/.local/share/opencode/multi-model/test-session/model";
|
||||
const worktreeListOutput = `worktree ${worktreePath}\nbranch refs/heads/opencode/test-session/model`;
|
||||
mockCommandResponses["git worktree list --porcelain"] = { ok: true, stdout: worktreeListOutput, stderr: "" };
|
||||
mockCommandResponses[`git worktree remove -f ${worktreePath}`] = { ok: true, stdout: "", stderr: "" };
|
||||
mockCommandResponses["git branch -D opencode/test-session/model"] = { ok: true, stdout: "", stderr: "" };
|
||||
mockCommandResponses["git worktree list --porcelain"] = {
|
||||
ok: true,
|
||||
stdout: worktreeListOutput,
|
||||
stderr: "",
|
||||
};
|
||||
mockCommandResponses[`git worktree remove -f ${worktreePath}`] = {
|
||||
ok: true,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
};
|
||||
mockCommandResponses["git branch -D opencode/test-session/model"] = {
|
||||
ok: true,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
};
|
||||
// Tag command will fail
|
||||
const fixedDate = new Date("2023-01-01T00:00:00.000Z");
|
||||
const OriginalDate = Date;
|
||||
// @ts-ignore
|
||||
global.Date = class extends OriginalDate { constructor() { super(); return fixedDate; } toISOString() { return "2023-01-01T00:00:00.000Z"; } } as any;
|
||||
// @ts-expect-error
|
||||
global.Date = class extends OriginalDate {
|
||||
constructor() {
|
||||
super();
|
||||
return fixedDate;
|
||||
}
|
||||
toISOString() {
|
||||
return "2023-01-01T00:00:00.000Z";
|
||||
}
|
||||
} as any;
|
||||
const tagCommand = `git tag archive/opencode/test-session/model-2023-01-01T00-00-00-000Z opencode/test-session/model`;
|
||||
mockCommandResponses[tagCommand] = { ok: false, stdout: "", stderr: "tag error" };
|
||||
mockCommandResponses[tagCommand] = {
|
||||
ok: false,
|
||||
stdout: "",
|
||||
stderr: "tag error",
|
||||
};
|
||||
|
||||
const result = await closeMultiModel({ sessionName: "test-session", cleanupWorktrees: true, force: true });
|
||||
const result = await closeMultiModel({
|
||||
sessionName: "test-session",
|
||||
cleanupWorktrees: true,
|
||||
force: true,
|
||||
});
|
||||
// Restore Date
|
||||
// @ts-ignore
|
||||
// @ts-expect-error
|
||||
global.Date = OriginalDate;
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
// Expect the warning message to be captured in instructions
|
||||
expect(result.instructions.includes('Failed to create tag')).toBe(true);
|
||||
expect(result.instructions.includes("Failed to create tag")).toBe(true);
|
||||
expect(result.tagsCreated).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -192,23 +342,53 @@ describe("closeMultiModel", () => {
|
||||
const unsanitized = "test session!";
|
||||
const sanitized = "test-session"; // result of sanitizeName
|
||||
// tmux lookup uses unsanitized name
|
||||
mockCommandResponses[`tmux has-session -t ${unsanitized}`] = { ok: false, stdout: "", stderr: "" };
|
||||
mockCommandResponses[`tmux has-session -t ${unsanitized}`] = {
|
||||
ok: false,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
};
|
||||
const worktreePath = `/home/user/.local/share/opencode/multi-model/${sanitized}/model`;
|
||||
const worktreeListOutput = `worktree ${worktreePath}\nbranch refs/heads/opencode/${sanitized}/model`;
|
||||
mockCommandResponses["git worktree list --porcelain"] = { ok: true, stdout: worktreeListOutput, stderr: "" };
|
||||
mockCommandResponses[`git worktree remove -f ${worktreePath}`] = { ok: true, stdout: "", stderr: "" };
|
||||
mockCommandResponses[`git branch -D opencode/${sanitized}/model`] = { ok: true, stdout: "", stderr: "" };
|
||||
mockCommandResponses[`rm -rf /home/user/.local/share/opencode/multi-model/${sanitized}`] = { ok: true, stdout: "", stderr: "" };
|
||||
mockCommandResponses["git worktree list --porcelain"] = {
|
||||
ok: true,
|
||||
stdout: worktreeListOutput,
|
||||
stderr: "",
|
||||
};
|
||||
mockCommandResponses[`git worktree remove -f ${worktreePath}`] = {
|
||||
ok: true,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
};
|
||||
mockCommandResponses[`git branch -D opencode/${sanitized}/model`] = {
|
||||
ok: true,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
};
|
||||
mockCommandResponses[
|
||||
`rm -rf /home/user/.local/share/opencode/multi-model/${sanitized}`
|
||||
] = { ok: true, stdout: "", stderr: "" };
|
||||
|
||||
// Freeze timestamp for deterministic tag name
|
||||
const fixedDate = new Date("2023-01-01T00:00:00.000Z");
|
||||
const OriginalDate = Date;
|
||||
// @ts-ignore
|
||||
global.Date = class extends OriginalDate { constructor() { super(); return fixedDate; } toISOString() { return "2023-01-01T00:00:00.000Z"; } } as any;
|
||||
// @ts-expect-error
|
||||
global.Date = class extends OriginalDate {
|
||||
constructor() {
|
||||
super();
|
||||
return fixedDate;
|
||||
}
|
||||
toISOString() {
|
||||
return "2023-01-01T00:00:00.000Z";
|
||||
}
|
||||
} as any;
|
||||
|
||||
const result = await closeMultiModel({ sessionName: unsanitized, cleanupWorktrees: true, force: true });
|
||||
const result = await closeMultiModel({
|
||||
sessionName: unsanitized,
|
||||
cleanupWorktrees: true,
|
||||
force: true,
|
||||
});
|
||||
// Restore Date
|
||||
// @ts-ignore
|
||||
// @ts-expect-error
|
||||
global.Date = OriginalDate;
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
@@ -217,37 +397,83 @@ describe("closeMultiModel", () => {
|
||||
expect(result.branchesDeleted).toEqual([`opencode/${sanitized}/model`]);
|
||||
expect(result.tagsCreated?.length).toBe(1);
|
||||
// Verify that the commands used the sanitized paths for worktree removal and branch deletion
|
||||
expect(executedCommands).toContain(`git worktree remove -f ${worktreePath}`);
|
||||
expect(executedCommands).toContain(`git branch -D opencode/${sanitized}/model`);
|
||||
expect(executedCommands).toContain(
|
||||
`git worktree remove -f ${worktreePath}`,
|
||||
);
|
||||
expect(executedCommands).toContain(
|
||||
`git branch -D opencode/${sanitized}/model`,
|
||||
);
|
||||
});
|
||||
|
||||
test("tmux commands use sessionName, not safeSessionName", async () => {
|
||||
const unsanitized = "my session!";
|
||||
const sanitized = "my-session";
|
||||
mockCommandResponses[`tmux has-session -t ${unsanitized}`] = { ok: true, stdout: "", stderr: "" };
|
||||
mockCommandResponses[`tmux kill-session -t ${unsanitized}`] = { ok: true, stdout: "", stderr: "" };
|
||||
mockCommandResponses[`tmux has-session -t ${unsanitized}`] = {
|
||||
ok: true,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
};
|
||||
mockCommandResponses[`tmux kill-session -t ${unsanitized}`] = {
|
||||
ok: true,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
};
|
||||
// No worktrees
|
||||
mockCommandResponses["git worktree list --porcelain"] = { ok: true, stdout: "", stderr: "" };
|
||||
mockCommandResponses["git worktree list --porcelain"] = {
|
||||
ok: true,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
};
|
||||
|
||||
const result = await closeMultiModel({ sessionName: unsanitized, cleanupWorktrees: false, force: false });
|
||||
const result = await closeMultiModel({
|
||||
sessionName: unsanitized,
|
||||
cleanupWorktrees: false,
|
||||
force: false,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
// Verify tmux commands used unsanitized name
|
||||
expect(executedCommands).toContain(`tmux has-session -t ${unsanitized}`);
|
||||
expect(executedCommands).toContain(`tmux kill-session -t ${unsanitized}`);
|
||||
// Ensure sanitized name not used in tmux commands
|
||||
expect(executedCommands.some(c => c.includes(`-t ${sanitized}`))).toBe(false);
|
||||
expect(executedCommands.some((c) => c.includes(`-t ${sanitized}`))).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("skip archive tags when flag disabled", async () => {
|
||||
mockCommandResponses["tmux has-session -t test-session"] = { ok: false, stdout: "", stderr: "" };
|
||||
const worktreePath = "/home/user/.local/share/opencode/multi-model/test-session/model";
|
||||
mockCommandResponses["tmux has-session -t test-session"] = {
|
||||
ok: false,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
};
|
||||
const worktreePath =
|
||||
"/home/user/.local/share/opencode/multi-model/test-session/model";
|
||||
const worktreeListOutput = `worktree ${worktreePath}\nbranch refs/heads/opencode/test-session/model`;
|
||||
mockCommandResponses["git worktree list --porcelain"] = { ok: true, stdout: worktreeListOutput, stderr: "" };
|
||||
mockCommandResponses[`git worktree remove -f ${worktreePath}`] = { ok: true, stdout: "", stderr: "" };
|
||||
mockCommandResponses["git branch -D opencode/test-session/model"] = { ok: true, stdout: "", stderr: "" };
|
||||
mockCommandResponses["rm -rf /home/user/.local/share/opencode/multi-model/test-session"] = { ok: true, stdout: "", stderr: "" };
|
||||
mockCommandResponses["git worktree list --porcelain"] = {
|
||||
ok: true,
|
||||
stdout: worktreeListOutput,
|
||||
stderr: "",
|
||||
};
|
||||
mockCommandResponses[`git worktree remove -f ${worktreePath}`] = {
|
||||
ok: true,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
};
|
||||
mockCommandResponses["git branch -D opencode/test-session/model"] = {
|
||||
ok: true,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
};
|
||||
mockCommandResponses[
|
||||
"rm -rf /home/user/.local/share/opencode/multi-model/test-session"
|
||||
] = { ok: true, stdout: "", stderr: "" };
|
||||
|
||||
const result = await closeMultiModel({ sessionName: "test-session", cleanupWorktrees: true, force: true, createArchiveTags: false });
|
||||
const result = await closeMultiModel({
|
||||
sessionName: "test-session",
|
||||
cleanupWorktrees: true,
|
||||
force: true,
|
||||
createArchiveTags: false,
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.cleanupPerformed).toBe(true);
|
||||
@@ -255,7 +481,8 @@ describe("closeMultiModel", () => {
|
||||
expect(result.branchesDeleted).toEqual(["opencode/test-session/model"]);
|
||||
expect(result.tagsCreated?.length ?? 0).toBe(0);
|
||||
// Ensure no tag command was executed
|
||||
expect(executedCommands.some(cmd => cmd.startsWith("git tag"))).toBe(false);
|
||||
expect(executedCommands.some((cmd) => cmd.startsWith("git tag"))).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
import { expect, test, mock, beforeEach, afterEach, describe, spyOn } from "bun:test";
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
mock,
|
||||
spyOn,
|
||||
test,
|
||||
} 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<string, { ok: boolean; stdout: string; stderr: string }> = {};
|
||||
const originalBun$: typeof Bun.$ = (globalThis as any).Bun?.$;
|
||||
let mockCommandResponses: Record<
|
||||
string,
|
||||
{ ok: boolean; stdout: string; stderr: string }
|
||||
> = {};
|
||||
let executedCommands: string[] = [];
|
||||
|
||||
function setupMockBun$() {
|
||||
@@ -15,7 +26,11 @@ function setupMockBun$() {
|
||||
if (cmd === "git worktree list --porcelain") {
|
||||
throw new Error("unexpected error");
|
||||
}
|
||||
const response = mockCommandResponses[cmd] ?? { ok: true, stdout: "", stderr: "" };
|
||||
const response = mockCommandResponses[cmd] ?? {
|
||||
ok: true,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
};
|
||||
return {
|
||||
quiet: () => ({
|
||||
nothrow: async () => ({
|
||||
@@ -46,7 +61,11 @@ describe("closeMultiModel unexpected error handling", () => {
|
||||
});
|
||||
|
||||
test("catches thrown error and returns failure", async () => {
|
||||
const result = await closeMultiModel({ sessionName: "any-session", cleanupWorktrees: false, force: false });
|
||||
const result = await closeMultiModel({
|
||||
sessionName: "any-session",
|
||||
cleanupWorktrees: false,
|
||||
force: false,
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain("unexpected error");
|
||||
});
|
||||
|
||||
+228
-66
@@ -1,28 +1,39 @@
|
||||
import { expect, test, mock, spyOn, beforeEach, afterEach, describe } from "bun:test";
|
||||
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";
|
||||
import {
|
||||
shellQuote,
|
||||
normalizeModels,
|
||||
findDuplicates,
|
||||
createWindowBaseName,
|
||||
createWindowPlans,
|
||||
levenshtein,
|
||||
suggestModels,
|
||||
findDuplicates,
|
||||
formatInvalidModelError,
|
||||
sanitizeName,
|
||||
runCommand,
|
||||
getWorktreePath,
|
||||
getSessionPath,
|
||||
undoWorktree,
|
||||
launchModelInWindow,
|
||||
getWorktreesForSession,
|
||||
getBinaryName,
|
||||
getSessionPath,
|
||||
getWorktreePath,
|
||||
getWorktreesForSession,
|
||||
launchModelInWindow,
|
||||
levenshtein,
|
||||
normalizeModels,
|
||||
runCommand,
|
||||
sanitizeName,
|
||||
shellQuote,
|
||||
suggestModels,
|
||||
undoWorktree,
|
||||
} from "../src/core/utils";
|
||||
|
||||
let originalBun$: typeof Bun.$ = (globalThis as any).Bun?.$;
|
||||
let mockCommandResponses: Record<string, { ok: boolean; stdout: string; stderr: string }> = {};
|
||||
const originalBun$: typeof Bun.$ = (globalThis as any).Bun?.$;
|
||||
let mockCommandResponses: Record<
|
||||
string,
|
||||
{ ok: boolean; stdout: string; stderr: string }
|
||||
> = {};
|
||||
let executedCommands: string[] = [];
|
||||
|
||||
function setupMockBun$() {
|
||||
@@ -35,7 +46,11 @@ function setupMockBun$() {
|
||||
return {
|
||||
quiet: () => ({
|
||||
nothrow: async () => {
|
||||
const response = mockCommandResponses[commandSignature] ?? { ok: true, stdout: "", stderr: "" };
|
||||
const response = mockCommandResponses[commandSignature] ?? {
|
||||
ok: true,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
};
|
||||
return {
|
||||
exitCode: response.ok ? 0 : 1,
|
||||
stdout: { toString: () => response.stdout },
|
||||
@@ -65,12 +80,29 @@ describe("multi-model launch", () => {
|
||||
executedCommands = [];
|
||||
|
||||
mockCommandResponses = {
|
||||
"git rev-parse --is-inside-work-tree": { ok: true, stdout: "true", stderr: "" },
|
||||
"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: "" },
|
||||
"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: "" },
|
||||
};
|
||||
});
|
||||
|
||||
@@ -112,7 +144,11 @@ describe("multi-model launch", () => {
|
||||
});
|
||||
|
||||
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" };
|
||||
mockCommandResponses["git rev-parse --is-inside-work-tree"] = {
|
||||
ok: false,
|
||||
stdout: "",
|
||||
stderr: "not a git repo",
|
||||
};
|
||||
|
||||
const result = await openMultiModel({
|
||||
sessionName: "test-session",
|
||||
@@ -120,12 +156,18 @@ describe("multi-model launch", () => {
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain("Error: `multi-model` requires a git repository.");
|
||||
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: "" };
|
||||
mockCommandResponses["command -v tmux"] = {
|
||||
ok: false,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
};
|
||||
|
||||
const result = await openMultiModel({
|
||||
sessionName: "test-session",
|
||||
@@ -133,7 +175,9 @@ describe("multi-model launch", () => {
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain("Error: `tmux` is not installed or not on `PATH`.");
|
||||
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",
|
||||
@@ -141,7 +185,11 @@ describe("multi-model launch", () => {
|
||||
});
|
||||
|
||||
test("fails if opencode is not installed", async () => {
|
||||
mockCommandResponses["command -v opencode"] = { ok: false, stdout: "", stderr: "" };
|
||||
mockCommandResponses["command -v opencode"] = {
|
||||
ok: false,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
};
|
||||
|
||||
const result = await openMultiModel({
|
||||
sessionName: "test-session",
|
||||
@@ -149,7 +197,9 @@ describe("multi-model launch", () => {
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain("Error: `opencode` is not installed or not on `PATH`.");
|
||||
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",
|
||||
@@ -164,7 +214,9 @@ describe("multi-model launch", () => {
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain("Error: Model name 'invalid/model' not found");
|
||||
expect(result.error).toContain(
|
||||
"Error: Model name 'invalid/model' not found",
|
||||
);
|
||||
expect(executedCommands).toEqual([
|
||||
"git rev-parse --is-inside-work-tree",
|
||||
"command -v tmux",
|
||||
@@ -174,7 +226,11 @@ describe("multi-model launch", () => {
|
||||
});
|
||||
|
||||
test("fails if session already exists", async () => {
|
||||
mockCommandResponses["tmux has-session -t test-session"] = { ok: true, stdout: "", stderr: "" };
|
||||
mockCommandResponses["tmux has-session -t test-session"] = {
|
||||
ok: true,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
};
|
||||
|
||||
const result = await openMultiModel({
|
||||
sessionName: "test-session",
|
||||
@@ -199,13 +255,25 @@ describe("multi-model launch", () => {
|
||||
});
|
||||
|
||||
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);
|
||||
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: "" };
|
||||
mockCommandResponses[
|
||||
"git show-ref --verify --quiet refs/heads/opencode/test-session/claude-3-5-sonnet"
|
||||
] = { ok: false, stdout: "", stderr: "" };
|
||||
|
||||
const result = await openMultiModel({
|
||||
sessionName: "test-session",
|
||||
@@ -213,14 +281,30 @@ describe("multi-model launch", () => {
|
||||
});
|
||||
|
||||
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);
|
||||
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: "" };
|
||||
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({
|
||||
sessionName: "test session!",
|
||||
@@ -228,18 +312,36 @@ describe("multi-model launch", () => {
|
||||
});
|
||||
|
||||
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);
|
||||
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: "" };
|
||||
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({
|
||||
sessionName: unsanitized,
|
||||
@@ -248,19 +350,45 @@ describe("multi-model launch", () => {
|
||||
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
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: "" };
|
||||
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({
|
||||
sessionName: "test-session",
|
||||
@@ -305,7 +433,9 @@ describe("multi-model launch", () => {
|
||||
});
|
||||
|
||||
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: "" };
|
||||
mockCommandResponses[
|
||||
"git show-ref --verify --quiet refs/heads/opencode/test-session/gpt-5-2"
|
||||
] = { ok: true, stdout: "", stderr: "" };
|
||||
|
||||
const result = await openMultiModel({
|
||||
sessionName: "test-session",
|
||||
@@ -313,7 +443,9 @@ describe("multi-model launch", () => {
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain("Branch 'opencode/test-session/gpt-5-2' already exists");
|
||||
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",
|
||||
@@ -325,7 +457,9 @@ describe("multi-model launch", () => {
|
||||
});
|
||||
|
||||
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" };
|
||||
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({
|
||||
sessionName: "test-session",
|
||||
@@ -346,9 +480,17 @@ describe("multi-model launch", () => {
|
||||
});
|
||||
|
||||
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: "" };
|
||||
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({
|
||||
sessionName: "test-session",
|
||||
@@ -356,15 +498,31 @@ describe("multi-model launch", () => {
|
||||
});
|
||||
|
||||
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);
|
||||
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: "" };
|
||||
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({
|
||||
sessionName: "test-session",
|
||||
@@ -372,8 +530,12 @@ describe("multi-model launch", () => {
|
||||
});
|
||||
|
||||
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(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);
|
||||
});
|
||||
});
|
||||
|
||||
+470
-167
@@ -1,10 +1,21 @@
|
||||
import { expect, test, mock, beforeEach, afterEach, describe, spyOn } from "bun:test";
|
||||
import * as os from "node:os";
|
||||
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";
|
||||
|
||||
let originalBun$: typeof Bun.$ = (globalThis as any).Bun?.$;
|
||||
let mockCommandResponses: Record<string, { ok: boolean; stdout: string; stderr: string }> = {};
|
||||
const originalBun$: typeof Bun.$ = (globalThis as any).Bun?.$;
|
||||
let mockCommandResponses: Record<
|
||||
string,
|
||||
{ ok: boolean; stdout: string; stderr: string }
|
||||
> = {};
|
||||
let executedCommands: string[] = [];
|
||||
|
||||
function setupMockBun$() {
|
||||
@@ -12,7 +23,11 @@ function setupMockBun$() {
|
||||
const parts = values[0] as string[];
|
||||
const cmd = parts.join(" ");
|
||||
executedCommands.push(cmd);
|
||||
const response = mockCommandResponses[cmd] ?? { ok: true, stdout: "", stderr: "" };
|
||||
const response = mockCommandResponses[cmd] ?? {
|
||||
ok: true,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
};
|
||||
return {
|
||||
quiet: () => ({
|
||||
nothrow: async () => ({
|
||||
@@ -44,193 +59,481 @@ describe("openMultiModel error paths", () => {
|
||||
|
||||
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" };
|
||||
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" });
|
||||
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: "" };
|
||||
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" });
|
||||
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: "" };
|
||||
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" });
|
||||
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 = `/mock/home/.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 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 = `/mock/home/.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 = `/mock/home/.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 = `/mock/home/.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",
|
||||
});
|
||||
|
||||
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 = `/mock/home/.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 = `/mock/home/.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");
|
||||
});
|
||||
|
||||
|
||||
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 = `/mock/home/.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 = `/mock/home/.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",
|
||||
});
|
||||
|
||||
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 = `/mock/home/.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 = `/mock/home/.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");
|
||||
});
|
||||
|
||||
|
||||
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 = `/mock/home/.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 = `/mock/home/.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");
|
||||
});
|
||||
|
||||
test("second model worktree path already exists", async () => {
|
||||
const session = "sess-wtpath";
|
||||
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 = `/mock/home/.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 path exists
|
||||
const secondBranch = `opencode/${session}/gpt-5-2-2`;
|
||||
const secondPath = `/mock/home/.local/share/opencode/multi-model/${session}/gpt-5-2-2`;
|
||||
mockCommandResponses[`git show-ref --verify --quiet refs/heads/${secondBranch}`] = { ok: false, stdout: "", stderr: "" };
|
||||
// mock fs existsSync to return true for secondPath
|
||||
spyOn(fs, "existsSync").mockImplementation((p) => p === secondPath);
|
||||
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 (Worktree path already exists at");
|
||||
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 = `/mock/home/.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 = `/mock/home/.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");
|
||||
});
|
||||
|
||||
test("second model worktree path already exists", async () => {
|
||||
const session = "sess-wtpath";
|
||||
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 = `/mock/home/.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 path exists
|
||||
const secondBranch = `opencode/${session}/gpt-5-2-2`;
|
||||
const secondPath = `/mock/home/.local/share/opencode/multi-model/${session}/gpt-5-2-2`;
|
||||
mockCommandResponses[
|
||||
`git show-ref --verify --quiet refs/heads/${secondBranch}`
|
||||
] = { ok: false, stdout: "", stderr: "" };
|
||||
// mock fs existsSync to return true for secondPath
|
||||
spyOn(fs, "existsSync").mockImplementation((p) => p === secondPath);
|
||||
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 (Worktree path already exists at",
|
||||
);
|
||||
expect(result.windows).toContain("gpt-5-2");
|
||||
expect(result.windows).toContain("gpt-5-2-2");
|
||||
});
|
||||
});
|
||||
|
||||
+160
-49
@@ -1,27 +1,38 @@
|
||||
import { expect, test, mock, spyOn, beforeEach, afterEach, describe } from "bun:test";
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
mock,
|
||||
spyOn,
|
||||
test,
|
||||
} from "bun:test";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import {
|
||||
shellQuote,
|
||||
normalizeModels,
|
||||
findDuplicates,
|
||||
createWindowBaseName,
|
||||
createWindowPlans,
|
||||
levenshtein,
|
||||
suggestModels,
|
||||
findDuplicates,
|
||||
formatInvalidModelError,
|
||||
sanitizeName,
|
||||
runCommand,
|
||||
getWorktreePath,
|
||||
getSessionPath,
|
||||
undoWorktree,
|
||||
launchModelInWindow,
|
||||
getWorktreesForSession,
|
||||
getBinaryName,
|
||||
getSessionPath,
|
||||
getWorktreePath,
|
||||
getWorktreesForSession,
|
||||
launchModelInWindow,
|
||||
levenshtein,
|
||||
normalizeModels,
|
||||
runCommand,
|
||||
sanitizeName,
|
||||
shellQuote,
|
||||
suggestModels,
|
||||
undoWorktree,
|
||||
} from "../src/core/utils";
|
||||
|
||||
let originalBun$: typeof Bun.$ = (globalThis as any).Bun?.$;
|
||||
let mockCommandResponses: Record<string, { ok: boolean; stdout: string; stderr: string }> = {};
|
||||
const originalBun$: typeof Bun.$ = (globalThis as any).Bun?.$;
|
||||
let mockCommandResponses: Record<
|
||||
string,
|
||||
{ ok: boolean; stdout: string; stderr: string }
|
||||
> = {};
|
||||
let executedCommands: string[] = [];
|
||||
|
||||
function setupMockBun$() {
|
||||
@@ -32,7 +43,11 @@ function setupMockBun$() {
|
||||
return {
|
||||
quiet: () => ({
|
||||
nothrow: async () => {
|
||||
const response = mockCommandResponses[commandSignature] ?? { ok: true, stdout: "", stderr: "" };
|
||||
const response = mockCommandResponses[commandSignature] ?? {
|
||||
ok: true,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
};
|
||||
return {
|
||||
exitCode: response.ok ? 0 : 1,
|
||||
stdout: { toString: () => response.stdout },
|
||||
@@ -75,7 +90,9 @@ describe("core utils", () => {
|
||||
expect(shellQuote("test'value")).toBe("'test'\"'\"'value'");
|
||||
});
|
||||
test("string with multiple single quotes", () => {
|
||||
expect(shellQuote("te's't'v'alue")).toBe("'te'\"'\"'s'\"'\"'t'\"'\"'v'\"'\"'alue'");
|
||||
expect(shellQuote("te's't'v'alue")).toBe(
|
||||
"'te'\"'\"'s'\"'\"'t'\"'\"'v'\"'\"'alue'",
|
||||
);
|
||||
});
|
||||
test("empty string", () => {
|
||||
expect(shellQuote("")).toBe("''");
|
||||
@@ -93,7 +110,9 @@ describe("core utils", () => {
|
||||
expect(normalizeModels([" ", " "])).toEqual([]);
|
||||
});
|
||||
test("array with mixed valid models, padded models, and empty strings", () => {
|
||||
expect(normalizeModels([" openai/gpt-5.2 ", "", " anthropic/claude "])).toEqual(["openai/gpt-5.2", "anthropic/claude"]);
|
||||
expect(
|
||||
normalizeModels([" openai/gpt-5.2 ", "", " anthropic/claude "]),
|
||||
).toEqual(["openai/gpt-5.2", "anthropic/claude"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -111,7 +130,11 @@ describe("core utils", () => {
|
||||
expect(findDuplicates(["a", "a", "a"])).toEqual(["a"]);
|
||||
});
|
||||
test("verifying first-repeated-occurrence order", () => {
|
||||
expect(findDuplicates(["x", "a", "b", "a", "x", "b"])).toEqual(["a", "x", "b"]);
|
||||
expect(findDuplicates(["x", "a", "b", "a", "x", "b"])).toEqual([
|
||||
"a",
|
||||
"x",
|
||||
"b",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -136,10 +159,14 @@ describe("core utils", () => {
|
||||
|
||||
describe("createWindowPlans", () => {
|
||||
test("single model", () => {
|
||||
expect(createWindowPlans(["openai/gpt-5-2"])).toEqual([{ model: "openai/gpt-5-2", windowName: "gpt-5-2" }]);
|
||||
expect(createWindowPlans(["openai/gpt-5-2"])).toEqual([
|
||||
{ model: "openai/gpt-5-2", windowName: "gpt-5-2" },
|
||||
]);
|
||||
});
|
||||
test("two models with the same base name", () => {
|
||||
expect(createWindowPlans(["openai/gpt-5-2", "anthropic/gpt-5-2"])).toEqual([
|
||||
expect(
|
||||
createWindowPlans(["openai/gpt-5-2", "anthropic/gpt-5-2"]),
|
||||
).toEqual([
|
||||
{ model: "openai/gpt-5-2", windowName: "gpt-5-2" },
|
||||
{ model: "anthropic/gpt-5-2", windowName: "gpt-5-2-2" },
|
||||
]);
|
||||
@@ -194,7 +221,12 @@ describe("core utils", () => {
|
||||
expect(suggestions[0]).toBe("gpt-5.2");
|
||||
});
|
||||
test("maximum of 3 suggestions", () => {
|
||||
const allowlist = ["gpt-5.2", "gpt-5.2-mini", "gpt-5.2-pro", "gpt-5.2-ultra"];
|
||||
const allowlist = [
|
||||
"gpt-5.2",
|
||||
"gpt-5.2-mini",
|
||||
"gpt-5.2-pro",
|
||||
"gpt-5.2-ultra",
|
||||
];
|
||||
expect(suggestModels("gpt4o", allowlist).length).toBe(3);
|
||||
});
|
||||
test("sorting logic", () => {
|
||||
@@ -239,14 +271,22 @@ describe("core utils", () => {
|
||||
|
||||
describe("runCommand", () => {
|
||||
test("returns ok true for successful command", async () => {
|
||||
mockCommandResponses["echo hello"] = { ok: true, stdout: "hello", stderr: "" };
|
||||
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" };
|
||||
mockCommandResponses["false"] = {
|
||||
ok: false,
|
||||
stdout: "",
|
||||
stderr: "error",
|
||||
};
|
||||
const result = await runCommand(["false"]);
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.stderr).toBe("error");
|
||||
@@ -257,12 +297,16 @@ describe("core utils", () => {
|
||||
test("returns correct worktree path", () => {
|
||||
spyOn(os, "homedir").mockReturnValue("/home/user");
|
||||
const result = getWorktreePath("my-session", "gpt-5.2");
|
||||
expect(result).toBe("/home/user/.local/share/opencode/multi-model/my-session/gpt-5.2");
|
||||
expect(result).toBe(
|
||||
"/home/user/.local/share/opencode/multi-model/my-session/gpt-5.2",
|
||||
);
|
||||
});
|
||||
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");
|
||||
expect(result).toBe(
|
||||
"/home/user/.local/share/opencode/multi-model/parent/child/window",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -270,57 +314,118 @@ describe("core utils", () => {
|
||||
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");
|
||||
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: "" };
|
||||
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");
|
||||
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: "" };
|
||||
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");
|
||||
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" };
|
||||
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");
|
||||
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" };
|
||||
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");
|
||||
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-5.2' C-m"] = { ok: true, stdout: "", stderr: "" };
|
||||
mockCommandResponses[
|
||||
"tmux send-keys -t session:window opencode --model 'openai/gpt-5.2' C-m"
|
||||
] = { ok: true, stdout: "", stderr: "" };
|
||||
|
||||
const result = await launchModelInWindow("session", { model: "openai/gpt-5.2", windowName: "window" });
|
||||
const result = await launchModelInWindow("session", {
|
||||
model: "openai/gpt-5.2",
|
||||
windowName: "window",
|
||||
});
|
||||
expect(result.ok).toBe(true);
|
||||
expect(executedCommands).toContain("tmux send-keys -t session:window opencode --model 'openai/gpt-5.2' C-m");
|
||||
expect(executedCommands).toContain(
|
||||
"tmux send-keys -t session:window opencode --model 'openai/gpt-5.2' C-m",
|
||||
);
|
||||
});
|
||||
test("uses custom binary name", async () => {
|
||||
mockCommandResponses["tmux send-keys -t session:window kilo --model 'openai/gpt-5.2' C-m"] = { ok: true, stdout: "", stderr: "" };
|
||||
mockCommandResponses[
|
||||
"tmux send-keys -t session:window kilo --model 'openai/gpt-5.2' C-m"
|
||||
] = { ok: true, stdout: "", stderr: "" };
|
||||
|
||||
const result = await launchModelInWindow("session", { model: "openai/gpt-5.2", windowName: "window" }, "kilo");
|
||||
const result = await launchModelInWindow(
|
||||
"session",
|
||||
{ model: "openai/gpt-5.2", windowName: "window" },
|
||||
"kilo",
|
||||
);
|
||||
expect(result.ok).toBe(true);
|
||||
expect(executedCommands).toContain("tmux send-keys -t session:window kilo --model 'openai/gpt-5.2' C-m");
|
||||
expect(executedCommands).toContain(
|
||||
"tmux send-keys -t session:window kilo --model 'openai/gpt-5.2' C-m",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -329,26 +434,32 @@ describe("core utils", () => {
|
||||
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-5.2\nbranch refs/heads/opencode/my-session/gpt-5.2\n\nworktree /home/user/other-repo\nbranch refs/heads/main",
|
||||
stdout:
|
||||
"worktree /home/user/.local/share/opencode/multi-model/my-session/gpt-5.2\nbranch refs/heads/opencode/my-session/gpt-5.2\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-5.2");
|
||||
expect(worktrees[0]?.path).toBe(
|
||||
"/home/user/.local/share/opencode/multi-model/my-session/gpt-5.2",
|
||||
);
|
||||
expect(worktrees[0]?.branch).toBe("opencode/my-session/gpt-5.2");
|
||||
});
|
||||
test("sanitizes session name when filtering 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-5.2\nbranch refs/heads/opencode/my-session/gpt-5.2",
|
||||
stdout:
|
||||
"worktree /home/user/.local/share/opencode/multi-model/my-session/gpt-5.2\nbranch refs/heads/opencode/my-session/gpt-5.2",
|
||||
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-5.2");
|
||||
expect(worktrees[0]?.path).toBe(
|
||||
"/home/user/.local/share/opencode/multi-model/my-session/gpt-5.2",
|
||||
);
|
||||
});
|
||||
test("returns empty array when no matching worktrees", async () => {
|
||||
spyOn(os, "homedir").mockReturnValue("/home/user");
|
||||
|
||||
Reference in New Issue
Block a user