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; 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({ 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({ 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({ 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({ 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({ 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({ 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({ 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({ 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({ 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({ 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({ 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", ]); }); });