From 7e201486f15575d587890dbe1fd637a5f552da91 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 9 Feb 2026 11:00:24 -0800 Subject: [PATCH] resource management --- apps/desktop/src/git.test.ts | 257 ++++++++++++++++++----------------- apps/desktop/tsconfig.json | 2 +- 2 files changed, 131 insertions(+), 128 deletions(-) diff --git a/apps/desktop/src/git.test.ts b/apps/desktop/src/git.test.ts index 37f3e8e09eb2a9ba81415057b24095573d773f1a..ed55f02c0f68f7de10b9f1da8b71fc3e89aaaf17 100644 --- a/apps/desktop/src/git.test.ts +++ b/apps/desktop/src/git.test.ts @@ -2,7 +2,7 @@ import { existsSync } from "node:fs"; import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { checkoutGitBranch, @@ -29,6 +29,17 @@ async function git(cwd: string, command: string): Promise { return result.stdout.trim(); } +/** Create a disposable temp directory that cleans up automatically. */ +async function makeTmpDir() { + const dir = await mkdtemp(path.join(tmpdir(), "git-test-")); + return { + path: dir, + [Symbol.asyncDispose]: async () => { + await rm(dir, { recursive: true, force: true }); + }, + }; +} + /** Create a repo with an initial commit so branches work. */ async function initRepoWithCommit(cwd: string): Promise { await initGitRepo({ cwd }); @@ -42,27 +53,19 @@ async function initRepoWithCommit(cwd: string): Promise { // ── Tests ── describe("git integration", () => { - let tmpDir: string; - - beforeEach(async () => { - tmpDir = await mkdtemp(path.join(tmpdir(), "git-test-")); - }); - - afterEach(async () => { - await rm(tmpDir, { recursive: true, force: true }); - }); - // ── initGitRepo ── describe("initGitRepo", () => { it("creates a valid git repo", async () => { - await initGitRepo({ cwd: tmpDir }); - expect(existsSync(path.join(tmpDir, ".git"))).toBe(true); + await using tmp = await makeTmpDir(); + await initGitRepo({ cwd: tmp.path }); + expect(existsSync(path.join(tmp.path, ".git"))).toBe(true); }); it("listGitBranches reports isRepo: true after init + commit", async () => { - await initRepoWithCommit(tmpDir); - const result = await listGitBranches({ cwd: tmpDir }); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + const result = await listGitBranches({ cwd: tmp.path }); expect(result.isRepo).toBe(true); expect(result.branches.length).toBeGreaterThanOrEqual(1); }); @@ -72,42 +75,47 @@ describe("git integration", () => { describe("listGitBranches", () => { it("returns isRepo: false for non-git directory", async () => { - const result = await listGitBranches({ cwd: tmpDir }); + await using tmp = await makeTmpDir(); + const result = await listGitBranches({ cwd: tmp.path }); expect(result.isRepo).toBe(false); expect(result.branches).toEqual([]); }); it("returns the current branch with current: true", async () => { - await initRepoWithCommit(tmpDir); - const result = await listGitBranches({ cwd: tmpDir }); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + const result = await listGitBranches({ cwd: tmp.path }); const current = result.branches.find((b) => b.current); expect(current).toBeDefined(); expect(current!.current).toBe(true); }); it("sorts current branch first", async () => { - await initRepoWithCommit(tmpDir); - await createGitBranch({ cwd: tmpDir, branch: "aaa-first-alpha" }); - await createGitBranch({ cwd: tmpDir, branch: "zzz-last" }); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "aaa-first-alpha" }); + await createGitBranch({ cwd: tmp.path, branch: "zzz-last" }); - const result = await listGitBranches({ cwd: tmpDir }); + const result = await listGitBranches({ cwd: tmp.path }); expect(result.branches[0]!.current).toBe(true); }); it("lists multiple branches after creating them", async () => { - await initRepoWithCommit(tmpDir); - await createGitBranch({ cwd: tmpDir, branch: "feature-a" }); - await createGitBranch({ cwd: tmpDir, branch: "feature-b" }); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "feature-a" }); + await createGitBranch({ cwd: tmp.path, branch: "feature-b" }); - const result = await listGitBranches({ cwd: tmpDir }); + const result = await listGitBranches({ cwd: tmp.path }); const names = result.branches.map((b) => b.name); expect(names).toContain("feature-a"); expect(names).toContain("feature-b"); }); it("isDefault is false when no remote exists", async () => { - await initRepoWithCommit(tmpDir); - const result = await listGitBranches({ cwd: tmpDir }); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + const result = await listGitBranches({ cwd: tmp.path }); expect(result.branches.every((b) => b.isDefault === false)).toBe(true); }); }); @@ -116,57 +124,50 @@ describe("git integration", () => { describe("checkoutGitBranch", () => { it("checks out an existing branch", async () => { - await initRepoWithCommit(tmpDir); - await createGitBranch({ cwd: tmpDir, branch: "feature" }); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "feature" }); - await checkoutGitBranch({ cwd: tmpDir, branch: "feature" }); + await checkoutGitBranch({ cwd: tmp.path, branch: "feature" }); - const result = await listGitBranches({ cwd: tmpDir }); + const result = await listGitBranches({ cwd: tmp.path }); const current = result.branches.find((b) => b.current); expect(current!.name).toBe("feature"); }); it("throws when branch does not exist", async () => { - await initRepoWithCommit(tmpDir); - await expect( - checkoutGitBranch({ cwd: tmpDir, branch: "nonexistent" }), - ).rejects.toThrow(); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await expect(checkoutGitBranch({ cwd: tmp.path, branch: "nonexistent" })).rejects.toThrow(); }); it("throws when checkout would overwrite uncommitted changes", async () => { - await initRepoWithCommit(tmpDir); - await createGitBranch({ cwd: tmpDir, branch: "other" }); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "other" }); // Create a conflicting change: modify README on current branch - await writeFile(path.join(tmpDir, "README.md"), "modified\n"); - await git(tmpDir, "add README.md"); - - // On the other branch, README has original content — checkout should conflict - // Actually git checkout with staged changes can sometimes work, so let's - // create a real conflict: commit different content on other branch first. - // Simpler: write to a tracked file without committing, then checkout a branch - // where that file differs. + await writeFile(path.join(tmp.path, "README.md"), "modified\n"); + await git(tmp.path, "add README.md"); // First, checkout other branch cleanly - await git(tmpDir, "stash"); - await checkoutGitBranch({ cwd: tmpDir, branch: "other" }); - await writeFile(path.join(tmpDir, "README.md"), "other content\n"); - await git(tmpDir, "add ."); - await git(tmpDir, "commit -m 'other change'"); + await git(tmp.path, "stash"); + await checkoutGitBranch({ cwd: tmp.path, branch: "other" }); + await writeFile(path.join(tmp.path, "README.md"), "other content\n"); + await git(tmp.path, "add ."); + await git(tmp.path, "commit -m 'other change'"); // Go back to default branch - const defaultBranch = ( - await listGitBranches({ cwd: tmpDir }) - ).branches.find((b) => !b.current)!.name; - await checkoutGitBranch({ cwd: tmpDir, branch: defaultBranch }); + const defaultBranch = (await listGitBranches({ cwd: tmp.path })).branches.find( + (b) => !b.current, + )!.name; + await checkoutGitBranch({ cwd: tmp.path, branch: defaultBranch }); // Make uncommitted changes to the same file - await writeFile(path.join(tmpDir, "README.md"), "conflicting local\n"); + await writeFile(path.join(tmp.path, "README.md"), "conflicting local\n"); // Checkout should fail due to uncommitted changes - await expect( - checkoutGitBranch({ cwd: tmpDir, branch: "other" }), - ).rejects.toThrow(); + await expect(checkoutGitBranch({ cwd: tmp.path, branch: "other" })).rejects.toThrow(); }); }); @@ -174,19 +175,19 @@ describe("git integration", () => { describe("createGitBranch", () => { it("creates a new branch visible in listGitBranches", async () => { - await initRepoWithCommit(tmpDir); - await createGitBranch({ cwd: tmpDir, branch: "new-feature" }); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "new-feature" }); - const result = await listGitBranches({ cwd: tmpDir }); + const result = await listGitBranches({ cwd: tmp.path }); expect(result.branches.some((b) => b.name === "new-feature")).toBe(true); }); it("throws when branch already exists", async () => { - await initRepoWithCommit(tmpDir); - await createGitBranch({ cwd: tmpDir, branch: "dupe" }); - await expect( - createGitBranch({ cwd: tmpDir, branch: "dupe" }), - ).rejects.toThrow(); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "dupe" }); + await expect(createGitBranch({ cwd: tmp.path, branch: "dupe" })).rejects.toThrow(); }); }); @@ -194,12 +195,13 @@ describe("git integration", () => { describe("createGitWorktree", () => { it("creates a worktree directory at the expected path", async () => { - await initRepoWithCommit(tmpDir); - await createGitBranch({ cwd: tmpDir, branch: "wt-branch" }); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "wt-branch" }); - const wtPath = path.join(tmpDir, "worktree-out"); + const wtPath = path.join(tmp.path, "worktree-out"); const result = await createGitWorktree({ - cwd: tmpDir, + cwd: tmp.path, branch: "wt-branch", path: wtPath, }); @@ -209,17 +211,18 @@ describe("git integration", () => { expect(existsSync(wtPath)).toBe(true); expect(existsSync(path.join(wtPath, "README.md"))).toBe(true); - // Clean up worktree - await removeGitWorktree({ cwd: tmpDir, path: wtPath }); + // Clean up worktree before tmp dir disposal + await removeGitWorktree({ cwd: tmp.path, path: wtPath }); }); it("worktree has the correct branch checked out", async () => { - await initRepoWithCommit(tmpDir); - await createGitBranch({ cwd: tmpDir, branch: "wt-check" }); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "wt-check" }); - const wtPath = path.join(tmpDir, "wt-check-dir"); + const wtPath = path.join(tmp.path, "wt-check-dir"); await createGitWorktree({ - cwd: tmpDir, + cwd: tmp.path, branch: "wt-check", path: wtPath, }); @@ -228,19 +231,20 @@ describe("git integration", () => { const branchOutput = await git(wtPath, "branch --show-current"); expect(branchOutput).toBe("wt-check"); - await removeGitWorktree({ cwd: tmpDir, path: wtPath }); + await removeGitWorktree({ cwd: tmp.path, path: wtPath }); }); it("throws when branch is already checked out in main worktree", async () => { - await initRepoWithCommit(tmpDir); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); // Try to create a worktree for the current branch (already checked out) - const branches = await listGitBranches({ cwd: tmpDir }); + const branches = await listGitBranches({ cwd: tmp.path }); const currentBranch = branches.branches.find((b) => b.current)!.name; - const wtPath = path.join(tmpDir, "wt-conflict"); + const wtPath = path.join(tmp.path, "wt-conflict"); await expect( createGitWorktree({ - cwd: tmpDir, + cwd: tmp.path, branch: currentBranch, path: wtPath, }), @@ -248,18 +252,19 @@ describe("git integration", () => { }); it("removeGitWorktree cleans up the worktree", async () => { - await initRepoWithCommit(tmpDir); - await createGitBranch({ cwd: tmpDir, branch: "wt-remove" }); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "wt-remove" }); - const wtPath = path.join(tmpDir, "wt-remove-dir"); + const wtPath = path.join(tmp.path, "wt-remove-dir"); await createGitWorktree({ - cwd: tmpDir, + cwd: tmp.path, branch: "wt-remove", path: wtPath, }); expect(existsSync(wtPath)).toBe(true); - await removeGitWorktree({ cwd: tmpDir, path: wtPath }); + await removeGitWorktree({ cwd: tmp.path, path: wtPath }); expect(existsSync(wtPath)).toBe(false); }); }); @@ -268,11 +273,12 @@ describe("git integration", () => { describe("full flow: local branch checkout", () => { it("init → commit → create branch → checkout → verify current", async () => { - await initRepoWithCommit(tmpDir); - await createGitBranch({ cwd: tmpDir, branch: "feature-login" }); - await checkoutGitBranch({ cwd: tmpDir, branch: "feature-login" }); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "feature-login" }); + await checkoutGitBranch({ cwd: tmp.path, branch: "feature-login" }); - const result = await listGitBranches({ cwd: tmpDir }); + const result = await listGitBranches({ cwd: tmp.path }); const current = result.branches.find((b) => b.current); expect(current!.name).toBe("feature-login"); }); @@ -282,12 +288,13 @@ describe("git integration", () => { describe("full flow: worktree creation", () => { it("creates worktree from a non-current branch", async () => { - await initRepoWithCommit(tmpDir); - await createGitBranch({ cwd: tmpDir, branch: "feature-wt" }); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "feature-wt" }); - const wtPath = path.join(tmpDir, "my-worktree"); + const wtPath = path.join(tmp.path, "my-worktree"); const result = await createGitWorktree({ - cwd: tmpDir, + cwd: tmp.path, branch: "feature-wt", path: wtPath, }); @@ -296,7 +303,7 @@ describe("git integration", () => { expect(existsSync(result.worktree.path)).toBe(true); // Main repo still on original branch - const mainBranches = await listGitBranches({ cwd: tmpDir }); + const mainBranches = await listGitBranches({ cwd: tmp.path }); const mainCurrent = mainBranches.branches.find((b) => b.current); expect(mainCurrent!.name).not.toBe("feature-wt"); @@ -304,7 +311,7 @@ describe("git integration", () => { const wtBranch = await git(wtPath, "branch --show-current"); expect(wtBranch).toBe("feature-wt"); - await removeGitWorktree({ cwd: tmpDir, path: wtPath }); + await removeGitWorktree({ cwd: tmp.path, path: wtPath }); }); }); @@ -312,23 +319,24 @@ describe("git integration", () => { describe("full flow: thread switching (checkout toggling)", () => { it("checkout a → checkout b → checkout a → current matches", async () => { - await initRepoWithCommit(tmpDir); - await createGitBranch({ cwd: tmpDir, branch: "branch-a" }); - await createGitBranch({ cwd: tmpDir, branch: "branch-b" }); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "branch-a" }); + await createGitBranch({ cwd: tmp.path, branch: "branch-b" }); // Simulate switching to thread A's branch - await checkoutGitBranch({ cwd: tmpDir, branch: "branch-a" }); - let branches = await listGitBranches({ cwd: tmpDir }); + await checkoutGitBranch({ cwd: tmp.path, branch: "branch-a" }); + let branches = await listGitBranches({ cwd: tmp.path }); expect(branches.branches.find((b) => b.current)!.name).toBe("branch-a"); // Simulate switching to thread B's branch - await checkoutGitBranch({ cwd: tmpDir, branch: "branch-b" }); - branches = await listGitBranches({ cwd: tmpDir }); + await checkoutGitBranch({ cwd: tmp.path, branch: "branch-b" }); + branches = await listGitBranches({ cwd: tmp.path }); expect(branches.branches.find((b) => b.current)!.name).toBe("branch-b"); // Switch back to thread A - await checkoutGitBranch({ cwd: tmpDir, branch: "branch-a" }); - branches = await listGitBranches({ cwd: tmpDir }); + await checkoutGitBranch({ cwd: tmp.path, branch: "branch-a" }); + branches = await listGitBranches({ cwd: tmp.path }); expect(branches.branches.find((b) => b.current)!.name).toBe("branch-a"); }); }); @@ -337,42 +345,37 @@ describe("git integration", () => { describe("full flow: checkout conflict", () => { it("uncommitted changes prevent checkout to a diverged branch", async () => { - await initRepoWithCommit(tmpDir); - await createGitBranch({ cwd: tmpDir, branch: "diverged" }); + await using tmp = await makeTmpDir(); + await initRepoWithCommit(tmp.path); + await createGitBranch({ cwd: tmp.path, branch: "diverged" }); // Make diverged branch have different file content - await checkoutGitBranch({ cwd: tmpDir, branch: "diverged" }); - await writeFile(path.join(tmpDir, "README.md"), "diverged content\n"); - await git(tmpDir, "add ."); - await git(tmpDir, "commit -m 'diverge'"); + await checkoutGitBranch({ cwd: tmp.path, branch: "diverged" }); + await writeFile(path.join(tmp.path, "README.md"), "diverged content\n"); + await git(tmp.path, "add ."); + await git(tmp.path, "commit -m 'diverge'"); // Go back to default branch - const defaultBranch = ( - await git(tmpDir, "rev-parse --abbrev-ref HEAD") - ).includes("diverged") + const defaultBranch = (await git(tmp.path, "rev-parse --abbrev-ref HEAD")).includes("diverged") ? // we're on diverged, need to find the other one - (await listGitBranches({ cwd: tmpDir })).branches.find( + (await listGitBranches({ cwd: tmp.path })).branches.find( (b) => !b.current && b.name !== "diverged", )!.name - : (await git(tmpDir, "rev-parse --abbrev-ref HEAD")); + : await git(tmp.path, "rev-parse --abbrev-ref HEAD"); // Actually, let's just get back to the initial branch explicitly - const allBranches = await listGitBranches({ cwd: tmpDir }); - const initialBranch = allBranches.branches.find( - (b) => b.name !== "diverged", - )!.name; - await checkoutGitBranch({ cwd: tmpDir, branch: initialBranch }); + const allBranches = await listGitBranches({ cwd: tmp.path }); + const initialBranch = allBranches.branches.find((b) => b.name !== "diverged")!.name; + await checkoutGitBranch({ cwd: tmp.path, branch: initialBranch }); // Make local uncommitted changes to the same file - await writeFile(path.join(tmpDir, "README.md"), "local uncommitted\n"); + await writeFile(path.join(tmp.path, "README.md"), "local uncommitted\n"); // Attempt checkout should fail - await expect( - checkoutGitBranch({ cwd: tmpDir, branch: "diverged" }), - ).rejects.toThrow(); + await expect(checkoutGitBranch({ cwd: tmp.path, branch: "diverged" })).rejects.toThrow(); // Current branch should still be the initial one - const result = await listGitBranches({ cwd: tmpDir }); + const result = await listGitBranches({ cwd: tmp.path }); expect(result.branches.find((b) => b.current)!.name).toBe(initialBranch); }); }); diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json index 893a95a41938f60fe31795d61c92deabb4685f38..c914820301e2b75e5628e1981d90cc3ebb0884eb 100644 --- a/apps/desktop/tsconfig.json +++ b/apps/desktop/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "composite": true, "types": ["node", "electron"], - "lib": ["ES2023", "DOM"] + "lib": ["ES2023", "DOM", "esnext.disposable"] }, "include": ["src", "tsup.config.ts"] } -- 2.54.0