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
+160 -49
View File
@@ -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");