build: add biome formatter and linter

This commit is contained in:
2026-03-23 08:17:27 +05:30
parent 068db04355
commit 7a164c705c
21 changed files with 1620 additions and 510 deletions
+302 -75
View File
@@ -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,
);
});
});