import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { readWorktreeMetadata, writeWorktreeMetadata, updateWorktreePRInfo, getWorktreePRInfo, readAllWorktreeMetadata, deleteWorktreeMetadata, type WorktreeMetadata, type WorktreePRInfo, } from "@/lib/worktree-metadata.js"; import fs from "fs/promises"; import path from "path"; import os from "os"; describe("worktree-metadata.ts", () => { let testProjectPath: string; beforeEach(async () => { testProjectPath = path.join(os.tmpdir(), `worktree-metadata-test-${Date.now()}`); await fs.mkdir(testProjectPath, { recursive: true }); }); afterEach(async () => { try { await fs.rm(testProjectPath, { recursive: true, force: true }); } catch { // Ignore cleanup errors } }); describe("sanitizeBranchName", () => { // Test through readWorktreeMetadata and writeWorktreeMetadata it("should sanitize branch names with invalid characters", async () => { const branch = "feature/test-branch"; const metadata: WorktreeMetadata = { branch, createdAt: new Date().toISOString(), }; await writeWorktreeMetadata(testProjectPath, branch, metadata); const result = await readWorktreeMetadata(testProjectPath, branch); expect(result).toEqual(metadata); }); it("should sanitize branch names with Windows invalid characters", async () => { const branch = "feature:test*branch?"; const metadata: WorktreeMetadata = { branch, createdAt: new Date().toISOString(), }; await writeWorktreeMetadata(testProjectPath, branch, metadata); const result = await readWorktreeMetadata(testProjectPath, branch); expect(result).toEqual(metadata); }); it("should sanitize Windows reserved names", async () => { const branch = "CON"; const metadata: WorktreeMetadata = { branch, createdAt: new Date().toISOString(), }; await writeWorktreeMetadata(testProjectPath, branch, metadata); const result = await readWorktreeMetadata(testProjectPath, branch); expect(result).toEqual(metadata); }); it("should handle empty branch name", async () => { const branch = ""; const metadata: WorktreeMetadata = { branch: "branch", createdAt: new Date().toISOString(), }; // Empty branch name should be sanitized to "_branch" await writeWorktreeMetadata(testProjectPath, branch, metadata); const result = await readWorktreeMetadata(testProjectPath, branch); expect(result).toEqual(metadata); }); it("should handle branch name that becomes empty after sanitization", async () => { // Test branch that would become empty after removing invalid chars const branch = "///"; const metadata: WorktreeMetadata = { branch: "branch", createdAt: new Date().toISOString(), }; await writeWorktreeMetadata(testProjectPath, branch, metadata); const result = await readWorktreeMetadata(testProjectPath, branch); expect(result).toEqual(metadata); }); }); describe("readWorktreeMetadata", () => { it("should return null when metadata file doesn't exist", async () => { const result = await readWorktreeMetadata(testProjectPath, "nonexistent-branch"); expect(result).toBeNull(); }); it("should read existing metadata", async () => { const branch = "test-branch"; const metadata: WorktreeMetadata = { branch, createdAt: new Date().toISOString(), }; await writeWorktreeMetadata(testProjectPath, branch, metadata); const result = await readWorktreeMetadata(testProjectPath, branch); expect(result).toEqual(metadata); }); it("should read metadata with PR info", async () => { const branch = "pr-branch"; const metadata: WorktreeMetadata = { branch, createdAt: new Date().toISOString(), pr: { number: 123, url: "https://github.com/owner/repo/pull/123", title: "Test PR", state: "open", createdAt: new Date().toISOString(), }, }; await writeWorktreeMetadata(testProjectPath, branch, metadata); const result = await readWorktreeMetadata(testProjectPath, branch); expect(result).toEqual(metadata); }); }); describe("writeWorktreeMetadata", () => { it("should create metadata directory if it doesn't exist", async () => { const branch = "new-branch"; const metadata: WorktreeMetadata = { branch, createdAt: new Date().toISOString(), }; await writeWorktreeMetadata(testProjectPath, branch, metadata); const result = await readWorktreeMetadata(testProjectPath, branch); expect(result).toEqual(metadata); }); it("should overwrite existing metadata", async () => { const branch = "existing-branch"; const metadata1: WorktreeMetadata = { branch, createdAt: new Date().toISOString(), }; const metadata2: WorktreeMetadata = { branch, createdAt: new Date().toISOString(), pr: { number: 456, url: "https://github.com/owner/repo/pull/456", title: "Updated PR", state: "closed", createdAt: new Date().toISOString(), }, }; await writeWorktreeMetadata(testProjectPath, branch, metadata1); await writeWorktreeMetadata(testProjectPath, branch, metadata2); const result = await readWorktreeMetadata(testProjectPath, branch); expect(result).toEqual(metadata2); }); }); describe("updateWorktreePRInfo", () => { it("should create new metadata if it doesn't exist", async () => { const branch = "new-pr-branch"; const prInfo: WorktreePRInfo = { number: 789, url: "https://github.com/owner/repo/pull/789", title: "New PR", state: "open", createdAt: new Date().toISOString(), }; await updateWorktreePRInfo(testProjectPath, branch, prInfo); const result = await readWorktreeMetadata(testProjectPath, branch); expect(result).not.toBeNull(); expect(result?.branch).toBe(branch); expect(result?.pr).toEqual(prInfo); }); it("should update existing metadata with PR info", async () => { const branch = "existing-pr-branch"; const metadata: WorktreeMetadata = { branch, createdAt: new Date().toISOString(), }; await writeWorktreeMetadata(testProjectPath, branch, metadata); const prInfo: WorktreePRInfo = { number: 999, url: "https://github.com/owner/repo/pull/999", title: "Updated PR", state: "merged", createdAt: new Date().toISOString(), }; await updateWorktreePRInfo(testProjectPath, branch, prInfo); const result = await readWorktreeMetadata(testProjectPath, branch); expect(result?.pr).toEqual(prInfo); }); it("should preserve existing metadata when updating PR info", async () => { const branch = "preserve-branch"; const originalCreatedAt = new Date().toISOString(); const metadata: WorktreeMetadata = { branch, createdAt: originalCreatedAt, }; await writeWorktreeMetadata(testProjectPath, branch, metadata); const prInfo: WorktreePRInfo = { number: 111, url: "https://github.com/owner/repo/pull/111", title: "PR", state: "open", createdAt: new Date().toISOString(), }; await updateWorktreePRInfo(testProjectPath, branch, prInfo); const result = await readWorktreeMetadata(testProjectPath, branch); expect(result?.createdAt).toBe(originalCreatedAt); expect(result?.pr).toEqual(prInfo); }); }); describe("getWorktreePRInfo", () => { it("should return null when metadata doesn't exist", async () => { const result = await getWorktreePRInfo(testProjectPath, "nonexistent"); expect(result).toBeNull(); }); it("should return null when metadata exists but has no PR info", async () => { const branch = "no-pr-branch"; const metadata: WorktreeMetadata = { branch, createdAt: new Date().toISOString(), }; await writeWorktreeMetadata(testProjectPath, branch, metadata); const result = await getWorktreePRInfo(testProjectPath, branch); expect(result).toBeNull(); }); it("should return PR info when it exists", async () => { const branch = "has-pr-branch"; const prInfo: WorktreePRInfo = { number: 222, url: "https://github.com/owner/repo/pull/222", title: "Has PR", state: "open", createdAt: new Date().toISOString(), }; await updateWorktreePRInfo(testProjectPath, branch, prInfo); const result = await getWorktreePRInfo(testProjectPath, branch); expect(result).toEqual(prInfo); }); }); describe("readAllWorktreeMetadata", () => { it("should return empty map when worktrees directory doesn't exist", async () => { const result = await readAllWorktreeMetadata(testProjectPath); expect(result.size).toBe(0); }); it("should return empty map when worktrees directory is empty", async () => { const worktreesDir = path.join(testProjectPath, ".automaker", "worktrees"); await fs.mkdir(worktreesDir, { recursive: true }); const result = await readAllWorktreeMetadata(testProjectPath); expect(result.size).toBe(0); }); it("should read all worktree metadata", async () => { const branch1 = "branch-1"; const branch2 = "branch-2"; const metadata1: WorktreeMetadata = { branch: branch1, createdAt: new Date().toISOString(), }; const metadata2: WorktreeMetadata = { branch: branch2, createdAt: new Date().toISOString(), pr: { number: 333, url: "https://github.com/owner/repo/pull/333", title: "PR 3", state: "open", createdAt: new Date().toISOString(), }, }; await writeWorktreeMetadata(testProjectPath, branch1, metadata1); await writeWorktreeMetadata(testProjectPath, branch2, metadata2); const result = await readAllWorktreeMetadata(testProjectPath); expect(result.size).toBe(2); expect(result.get(branch1)).toEqual(metadata1); expect(result.get(branch2)).toEqual(metadata2); }); it("should skip directories without worktree.json", async () => { const worktreesDir = path.join(testProjectPath, ".automaker", "worktrees"); const emptyDir = path.join(worktreesDir, "empty-dir"); await fs.mkdir(emptyDir, { recursive: true }); const branch = "valid-branch"; const metadata: WorktreeMetadata = { branch, createdAt: new Date().toISOString(), }; await writeWorktreeMetadata(testProjectPath, branch, metadata); const result = await readAllWorktreeMetadata(testProjectPath); expect(result.size).toBe(1); expect(result.get(branch)).toEqual(metadata); }); it("should skip files in worktrees directory", async () => { const worktreesDir = path.join(testProjectPath, ".automaker", "worktrees"); await fs.mkdir(worktreesDir, { recursive: true }); const filePath = path.join(worktreesDir, "not-a-dir.txt"); await fs.writeFile(filePath, "content"); const branch = "valid-branch"; const metadata: WorktreeMetadata = { branch, createdAt: new Date().toISOString(), }; await writeWorktreeMetadata(testProjectPath, branch, metadata); const result = await readAllWorktreeMetadata(testProjectPath); expect(result.size).toBe(1); expect(result.get(branch)).toEqual(metadata); }); it("should skip directories with malformed JSON", async () => { const worktreesDir = path.join(testProjectPath, ".automaker", "worktrees"); const badDir = path.join(worktreesDir, "bad-dir"); await fs.mkdir(badDir, { recursive: true }); const badJsonPath = path.join(badDir, "worktree.json"); await fs.writeFile(badJsonPath, "not valid json"); const branch = "valid-branch"; const metadata: WorktreeMetadata = { branch, createdAt: new Date().toISOString(), }; await writeWorktreeMetadata(testProjectPath, branch, metadata); const result = await readAllWorktreeMetadata(testProjectPath); expect(result.size).toBe(1); expect(result.get(branch)).toEqual(metadata); }); }); describe("deleteWorktreeMetadata", () => { it("should delete worktree metadata directory", async () => { const branch = "to-delete"; const metadata: WorktreeMetadata = { branch, createdAt: new Date().toISOString(), }; await writeWorktreeMetadata(testProjectPath, branch, metadata); let result = await readWorktreeMetadata(testProjectPath, branch); expect(result).not.toBeNull(); await deleteWorktreeMetadata(testProjectPath, branch); result = await readWorktreeMetadata(testProjectPath, branch); expect(result).toBeNull(); }); it("should handle deletion when metadata doesn't exist", async () => { // Should not throw await expect( deleteWorktreeMetadata(testProjectPath, "nonexistent") ).resolves.toBeUndefined(); }); }); });