mirror of
https://github.com/bendtherules/opencode-multi-model.git
synced 2026-08-18 21:52:11 +00:00
224 lines
11 KiB
TypeScript
224 lines
11 KiB
TypeScript
import { expect, test, mock, spyOn, beforeEach, afterEach, describe } 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 }> = {};
|
|
let executedCommands: string[] = [];
|
|
|
|
function setupMockBun$() {
|
|
const mockFn = mock((strings: TemplateStringsArray, ...values: any[]) => {
|
|
const parts = values[0] as string[];
|
|
const commandSignature = parts.join(" ");
|
|
executedCommands.push(commandSignature);
|
|
const response = mockCommandResponses[commandSignature] ?? { ok: true, stdout: "", stderr: "" };
|
|
return {
|
|
quiet: () => ({
|
|
nothrow: async () => ({
|
|
exitCode: response.ok ? 0 : 1,
|
|
stdout: { toString: () => response.stdout },
|
|
stderr: { toString: () => response.stderr },
|
|
}),
|
|
}),
|
|
};
|
|
});
|
|
(globalThis as any).Bun.$ = mockFn;
|
|
return mockFn;
|
|
}
|
|
|
|
function restoreOriginalBun$() {
|
|
(globalThis as any).Bun.$ = originalBun$;
|
|
}
|
|
|
|
describe("closeMultiModel", () => {
|
|
let bunMock: ReturnType<typeof setupMockBun$>;
|
|
|
|
beforeEach(() => {
|
|
spyOn(os, "homedir").mockReturnValue("/home/user");
|
|
bunMock = setupMockBun$();
|
|
executedCommands = [];
|
|
mockCommandResponses = {};
|
|
});
|
|
|
|
afterEach(() => {
|
|
restoreOriginalBun$();
|
|
});
|
|
|
|
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: "" };
|
|
// No worktrees
|
|
mockCommandResponses["git worktree list --porcelain"] = { ok: true, stdout: "", stderr: "" };
|
|
|
|
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");
|
|
expect(executedCommands).toContain("tmux kill-session -t test-session");
|
|
});
|
|
|
|
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: "" };
|
|
|
|
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);
|
|
});
|
|
|
|
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";
|
|
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: "" };
|
|
// 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: "" };
|
|
|
|
// 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;
|
|
|
|
const result = await closeMultiModel({ sessionName: "test-session", cleanupWorktrees: true, force: true });
|
|
// Restore Date
|
|
// @ts-ignore
|
|
global.Date = OriginalDate;
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.cleanupPerformed).toBe(true);
|
|
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`);
|
|
});
|
|
|
|
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";
|
|
const worktreeListOutput = `worktree ${worktreePath}\nbranch refs/heads/opencode/test-session/model`;
|
|
mockCommandResponses["git worktree list --porcelain"] = { ok: true, stdout: worktreeListOutput, stderr: "" };
|
|
|
|
// Stub readline to return "n"
|
|
const mockInterface = {
|
|
question: (q: string, cb: (a: string) => void) => cb("n"),
|
|
close: () => {},
|
|
} as any;
|
|
spyOn(readline, "createInterface").mockImplementation(() => mockInterface);
|
|
|
|
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";
|
|
const worktreeListOutput = `worktree ${worktreePath}\nbranch refs/heads/opencode/test-session/model`;
|
|
mockCommandResponses["git worktree list --porcelain"] = { ok: true, stdout: worktreeListOutput, stderr: "" };
|
|
|
|
// Stub readline to simulate user confirming cleanup (answers "y")
|
|
const mockInterface = {
|
|
question: (q: string, cb: (a: string) => void) => cb("y"),
|
|
close: () => {},
|
|
} as any;
|
|
spyOn(readline, "createInterface").mockImplementation(() => mockInterface);
|
|
|
|
// 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;
|
|
|
|
const result = await closeMultiModel({ sessionName: "test-session", cleanupWorktrees: true, force: false });
|
|
|
|
// Restore Date after invocation
|
|
// @ts-ignore
|
|
global.Date = OriginalDate;
|
|
|
|
// Verify result indicates successful cleanup
|
|
expect(result.success).toBe(true);
|
|
expect(result.cleanupPerformed).toBe(true);
|
|
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`);
|
|
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";
|
|
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: "" };
|
|
// 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;
|
|
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" };
|
|
|
|
const result = await closeMultiModel({ sessionName: "test-session", cleanupWorktrees: true, force: true });
|
|
// Restore Date
|
|
// @ts-ignore
|
|
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.tagsCreated).toEqual([]);
|
|
});
|
|
|
|
test("cleanup works with unsanitized session name", async () => {
|
|
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: "" };
|
|
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: "" };
|
|
|
|
// 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;
|
|
|
|
const result = await closeMultiModel({ sessionName: unsanitized, cleanupWorktrees: true, force: true });
|
|
// Restore Date
|
|
// @ts-ignore
|
|
global.Date = OriginalDate;
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.cleanupPerformed).toBe(true);
|
|
expect(result.worktreesRemoved).toEqual([worktreePath]);
|
|
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`);
|
|
});
|
|
});
|