Files
opencode-multi-model/tests/cleanup.test.ts
T

417 lines
11 KiB
TypeScript

import {
afterEach,
beforeEach,
describe,
expect,
mock,
spyOn,
test,
} from "bun:test";
import * as readline from "node:readline";
import { cleanupMultiModel } from "../src/core/cleanup";
const 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("cleanupMultiModel", () => {
let _bunMock: ReturnType<typeof setupMockBun$>;
beforeEach(() => {
_bunMock = setupMockBun$();
executedCommands = [];
mockCommandResponses = {};
});
afterEach(() => {
restoreOriginalBun$();
});
test("no archive tags found - returns success with message", async () => {
mockCommandResponses["git remote"] = {
ok: true,
stdout: "origin",
stderr: "",
};
mockCommandResponses["git tag -l archive/*"] = {
ok: true,
stdout: "",
stderr: "",
};
const result = await cleanupMultiModel({ mode: "cli", force: true });
expect(result.success).toBe(true);
expect(result.localTagsDeleted).toBe(0);
expect(result.remoteTagsDeleted).toBe(0);
expect(result.instructions).toContain("No archive tags found");
});
test("user cancels confirmation - returns success=false", async () => {
mockCommandResponses["git remote"] = {
ok: true,
stdout: "origin",
stderr: "",
};
mockCommandResponses["git tag -l archive/*"] = {
ok: true,
stdout: "archive/test-1\narchive/test-2",
stderr: "",
};
const mockInterface = {
question: (_q: string, cb: (a: string) => void) => cb("n"),
close: () => {},
} as any;
spyOn(readline, "createInterface").mockImplementation(() => mockInterface);
const result = await cleanupMultiModel({ mode: "cli", force: false });
expect(result.success).toBe(false);
expect(result.error).toBe("Cleanup cancelled by user");
expect(result.localTagsDeleted).toBe(0);
expect(result.remoteTagsDeleted).toBe(0);
});
test("force mode skips confirmation", async () => {
mockCommandResponses["git remote"] = {
ok: true,
stdout: "origin",
stderr: "",
};
mockCommandResponses["git tag -l archive/*"] = {
ok: true,
stdout: "archive/test-1",
stderr: "",
};
mockCommandResponses["git tag -d archive/test-1"] = {
ok: true,
stdout: "",
stderr: "",
};
mockCommandResponses["git push origin --delete archive/test-1"] = {
ok: true,
stdout: "",
stderr: "",
};
const result = await cleanupMultiModel({ mode: "cli", force: true });
expect(result.success).toBe(true);
expect(result.localTagsDeleted).toBe(1);
expect(executedCommands).toContain("git tag -d archive/test-1");
});
test("--no-remote flag (remote=false) - only local cleanup, no remote push", async () => {
mockCommandResponses["git remote"] = {
ok: true,
stdout: "origin",
stderr: "",
};
mockCommandResponses["git tag -l archive/*"] = {
ok: true,
stdout: "archive/test-1",
stderr: "",
};
mockCommandResponses["git tag -d archive/test-1"] = {
ok: true,
stdout: "",
stderr: "",
};
const result = await cleanupMultiModel({
mode: "cli",
force: true,
remote: false,
});
expect(result.success).toBe(true);
expect(result.localTagsDeleted).toBe(1);
expect(result.remoteTagsDeleted).toBe(0);
expect(executedCommands).toContain("git tag -d archive/test-1");
expect(executedCommands.some((c) => c.startsWith("git push"))).toBe(false);
});
test("remote deletion failure continues local cleanup", async () => {
mockCommandResponses["git remote"] = {
ok: true,
stdout: "origin",
stderr: "",
};
mockCommandResponses["git tag -l archive/*"] = {
ok: true,
stdout: "archive/test-1\narchive/test-2",
stderr: "",
};
mockCommandResponses["git tag -d archive/test-1"] = {
ok: true,
stdout: "",
stderr: "",
};
mockCommandResponses["git tag -d archive/test-2"] = {
ok: true,
stdout: "",
stderr: "",
};
mockCommandResponses["git push origin --delete archive/test-1"] = {
ok: false,
stdout: "",
stderr: "remote error",
};
mockCommandResponses["git push origin --delete archive/test-2"] = {
ok: true,
stdout: "",
stderr: "",
};
const result = await cleanupMultiModel({ mode: "cli", force: true });
expect(result.success).toBe(true);
expect(result.localTagsDeleted).toBe(2);
expect(result.remoteTagsDeleted).toBe(1);
expect(result.remoteTagErrors.length).toBe(1);
expect(result.remoteTagErrors[0]?.tag).toBe("archive/test-1");
});
test("dynamic remote detection - remote=undefined uses first available remote", async () => {
mockCommandResponses["git remote"] = {
ok: true,
stdout: "upstream\norigin",
stderr: "",
};
mockCommandResponses["git tag -l archive/*"] = {
ok: true,
stdout: "archive/test-1",
stderr: "",
};
mockCommandResponses["git tag -d archive/test-1"] = {
ok: true,
stdout: "",
stderr: "",
};
mockCommandResponses["git push upstream --delete archive/test-1"] = {
ok: true,
stdout: "",
stderr: "",
};
const result = await cleanupMultiModel({
mode: "cli",
force: true,
remote: undefined,
});
expect(result.success).toBe(true);
expect(executedCommands).toContain(
"git push upstream --delete archive/test-1",
);
});
test("remote name validation - uses specified remote if it exists", async () => {
mockCommandResponses["git remote"] = {
ok: true,
stdout: "upstream\norigin",
stderr: "",
};
mockCommandResponses["git tag -l archive/*"] = {
ok: true,
stdout: "archive/test-1",
stderr: "",
};
mockCommandResponses["git tag -d archive/test-1"] = {
ok: true,
stdout: "",
stderr: "",
};
mockCommandResponses["git push upstream --delete archive/test-1"] = {
ok: true,
stdout: "",
stderr: "",
};
const result = await cleanupMultiModel({
mode: "cli",
force: true,
remote: "upstream",
});
expect(result.success).toBe(true);
expect(result.availableRemotes).toEqual(["upstream", "origin"]);
expect(executedCommands).toContain(
"git push upstream --delete archive/test-1",
);
});
test("remote error - returns error if specified remote doesn't exist", async () => {
mockCommandResponses["git remote"] = {
ok: true,
stdout: "origin",
stderr: "",
};
mockCommandResponses["git tag -l archive/*"] = {
ok: true,
stdout: "archive/test-1",
stderr: "",
};
const result = await cleanupMultiModel({
mode: "cli",
force: true,
remote: "nonexistent",
});
expect(result.success).toBe(false);
expect(result.error).toContain("Remote 'nonexistent' not found");
expect(result.error).toContain("Available remotes: origin");
});
test("no remotes configured - only local cleanup", async () => {
mockCommandResponses["git remote"] = {
ok: true,
stdout: "",
stderr: "",
};
mockCommandResponses["git tag -l archive/*"] = {
ok: true,
stdout: "archive/test-1",
stderr: "",
};
mockCommandResponses["git tag -d archive/test-1"] = {
ok: true,
stdout: "",
stderr: "",
};
const result = await cleanupMultiModel({ mode: "cli", force: true });
expect(result.success).toBe(true);
expect(result.availableRemotes).toEqual([]);
expect(result.remoteTagsDeleted).toBe(0);
expect(executedCommands.some((c) => c.startsWith("git push"))).toBe(false);
});
test("user confirms cleanup when not forced - proceeds with deletion", async () => {
mockCommandResponses["git remote"] = {
ok: true,
stdout: "origin",
stderr: "",
};
mockCommandResponses["git tag -l archive/*"] = {
ok: true,
stdout: "archive/test-1",
stderr: "",
};
mockCommandResponses["git tag -d archive/test-1"] = {
ok: true,
stdout: "",
stderr: "",
};
mockCommandResponses["git push origin --delete archive/test-1"] = {
ok: true,
stdout: "",
stderr: "",
};
const mockInterface = {
question: (_q: string, cb: (a: string) => void) => cb("y"),
close: () => {},
} as any;
spyOn(readline, "createInterface").mockImplementation(() => mockInterface);
const result = await cleanupMultiModel({ mode: "cli", force: false });
expect(result.success).toBe(true);
expect(result.localTagsDeleted).toBe(1);
expect(result.remoteTagsDeleted).toBe(1);
expect(executedCommands).toContain("git tag -d archive/test-1");
expect(executedCommands).toContain(
"git push origin --delete archive/test-1",
);
});
test("multiple archive tags deleted", async () => {
mockCommandResponses["git remote"] = {
ok: true,
stdout: "origin",
stderr: "",
};
mockCommandResponses["git tag -l archive/*"] = {
ok: true,
stdout: "archive/test-1\narchive/test-2\narchive/test-3",
stderr: "",
};
mockCommandResponses["git tag -d archive/test-1"] = {
ok: true,
stdout: "",
stderr: "",
};
mockCommandResponses["git tag -d archive/test-2"] = {
ok: true,
stdout: "",
stderr: "",
};
mockCommandResponses["git tag -d archive/test-3"] = {
ok: true,
stdout: "",
stderr: "",
};
mockCommandResponses["git push origin --delete archive/test-1"] = {
ok: true,
stdout: "",
stderr: "",
};
mockCommandResponses["git push origin --delete archive/test-2"] = {
ok: true,
stdout: "",
stderr: "",
};
mockCommandResponses["git push origin --delete archive/test-3"] = {
ok: true,
stdout: "",
stderr: "",
};
const result = await cleanupMultiModel({ mode: "cli", force: true });
expect(result.success).toBe(true);
expect(result.localTagsDeleted).toBe(3);
expect(result.remoteTagsDeleted).toBe(3);
expect(result.localTagNames).toEqual([
"archive/test-1",
"archive/test-2",
"archive/test-3",
]);
});
});