/** * Unit tests for parseWorktreeList parser. * * Tests cover normal output, edge cases, malformed input, and path normalization. */ import { describe, it, expect } from "bun:test"; import { parseWorktreeList } from "./pi-worktree-parser"; import { validateCreateWorktreeInput, buildWorktreeAddCommand, validatePathDoesNotExist, isCurrentBranch, } from "./pi-worktree-create"; describe("parseWorktreeList", () => { describe("normal output", () => { it("parses a single worktree entry with branch in brackets", () => { const rawOutput = "/home/user/project abc1234 [main]"; const result = parseWorktreeList(rawOutput, "/home/user/project"); expect(result).toHaveLength(1); expect(result[0].path).toBe("/home/user/project"); expect(result[0].commit).toBe("abc1234"); expect(result[0].branch).toBe("main"); expect(result[0].isCurrent).toBe(true); }); it("parses multiple worktree entries", () => { const rawOutput = `/home/user/project abc1234 [main] /home/user/feature-x def5678 [feature-x]`; const result = parseWorktreeList(rawOutput, "/home/user/project"); expect(result).toHaveLength(2); expect(result[0].branch).toBe("main"); expect(result[0].isCurrent).toBe(true); expect(result[1].branch).toBe("feature-x"); expect(result[1].isCurrent).toBe(false); }); it("handles detached HEAD entries", () => { const rawOutput = "/home/user/detached fedcba9 HEAD"; const result = parseWorktreeList(rawOutput, "/other/path"); expect(result).toHaveLength(1); expect(result[0].branch).toBe("(detached)"); expect(result[0].commit).toBe("fedcba9"); }); }); describe("path normalization (Task 001)", () => { it("marks isCurrent=true when paths match with trailing slash difference", () => { const rawOutput = "/home/user/project abc1234 [main]"; const result = parseWorktreeList(rawOutput, "/home/user/project/"); expect(result[0].isCurrent).toBe(true); }); it("marks isCurrent=true when paths match with different casing in components", () => { const rawOutput = "/home/user/Project abc1234 [main]"; // On Linux, path.resolve preserves case — this tests normalization of slashes const result = parseWorktreeList(rawOutput, "/home/user/Project/"); expect(result[0].isCurrent).toBe(true); }); }); describe("malformed input validation (Task 002)", () => { it("skips lines with fewer than 3 tokens", () => { const rawOutput = `/home/user/project abc1234 [main] /incomplete/path`; const result = parseWorktreeList(rawOutput, "/other"); expect(result).toHaveLength(1); expect(result[0].path).toBe("/home/user/project"); }); it("skips lines with only 2 tokens (missing branch)", () => { const rawOutput = `/home/user/project abc1234 [main] /incomplete/path def5678`; const result = parseWorktreeList(rawOutput, "/other"); expect(result).toHaveLength(1); }); it("returns empty array for completely malformed input", () => { const rawOutput = `just some random text`; const result = parseWorktreeList(rawOutput, "/any/path"); expect(result).toHaveLength(0); }); }); describe("empty input (Task 006)", () => { it("returns empty array for empty string", () => { const result = parseWorktreeList("", "/some/path"); expect(result).toHaveLength(0); }); it("returns empty array for whitespace-only input", () => { const result = parseWorktreeList(" \n \n ", "/some/path"); expect(result).toHaveLength(0); }); }); describe("branch names with special characters", () => { it("handles branch names with hyphens and slashes", () => { const rawOutput = "/home/user/feature abc1234 [feature/my-branch-name]"; const result = parseWorktreeList(rawOutput, "/other"); expect(result).toHaveLength(1); expect(result[0].branch).toBe("feature/my-branch-name"); }); it("handles branch names with underscores", () => { const rawOutput = "/home/user/feature abc1234 [issue_42_fix]"; const result = parseWorktreeList(rawOutput, "/other"); expect(result).toHaveLength(1); expect(result[0].branch).toBe("issue_42_fix"); }); }); }); describe("validateCreateWorktreeInput (Issue #4)", () => { it("returns null for valid path and branch", () => { const result = validateCreateWorktreeInput("/tmp/new-worktree", "feature-x"); expect(result).toBeNull(); }); it("returns error for empty path", () => { const result = validateCreateWorktreeInput("", "feature-x"); expect(result).not.toBeNull(); if (result) expect(result.message).toContain("cannot be empty"); }); it("returns error for empty branch name", () => { const result = validateCreateWorktreeInput("/tmp/new-worktree", ""); expect(result).not.toBeNull(); if (result) expect(result.message).toContain("cannot be empty"); }); it("returns error for path with only whitespace", () => { const result = validateCreateWorktreeInput(" ", "feature-x"); expect(result).not.toBeNull(); }); it("returns error for branch name with invalid characters (spaces)", () => { const result = validateCreateWorktreeInput("/tmp/new-worktree", "my bad branch"); expect(result).not.toBeNull(); if (result) expect(result.message).toContain("invalid characters"); }); it("accepts branch names with hyphens and slashes", () => { const result = validateCreateWorktreeInput("/tmp/new-worktree", "feature/my-branch-name"); expect(result).toBeNull(); }); it("accepts branch names with underscores", () => { const result = validateCreateWorktreeInput("/tmp/new-worktree", "issue_42_fix"); expect(result).toBeNull(); }); // Task 007: Branch name start/end validation it("rejects branch name starting with dash", () => { const result = validateCreateWorktreeInput("/tmp/x", "-feature"); expect(result).not.toBeNull(); if (result) expect(result.message).toContain("invalid character"); }); it("rejects branch name starting with slash", () => { const result = validateCreateWorktreeInput("/tmp/x", "/feature"); expect(result).not.toBeNull(); }); it("rejects branch name starting with tilde", () => { const result = validateCreateWorktreeInput("/tmp/x", "~user"); expect(result).not.toBeNull(); }); it("rejects branch name starting with caret", () => { const result = validateCreateWorktreeInput("/tmp/x", "^feature"); expect(result).not.toBeNull(); }); it("rejects branch name starting with dot", () => { const result = validateCreateWorktreeInput("/tmp/x", ".hidden"); expect(result).not.toBeNull(); }); it("rejects branch name ending with slash", () => { const result = validateCreateWorktreeInput("/tmp/x", "feature/"); expect(result).not.toBeNull(); if (result) expect(result.message).toContain("invalid character"); }); it("rejects branch name ending with dash", () => { const result = validateCreateWorktreeInput("/tmp/x", "feature-"); expect(result).not.toBeNull(); }); it("rejects branch name ending with dot", () => { const result = validateCreateWorktreeInput("/tmp/x", "feature."); expect(result).not.toBeNull(); }); // Task 008: Dangerous character validation it("rejects branch name with square brackets", () => { const result = validateCreateWorktreeInput("/tmp/x", "feature[v1]"); expect(result).not.toBeNull(); if (result) expect(result.message).toContain("invalid characters"); }); it("rejects branch name with double quotes", () => { const result = validateCreateWorktreeInput("/tmp/x", 'my"branch'); expect(result).not.toBeNull(); }); it("rejects branch name with single quotes", () => { const result = validateCreateWorktreeInput("/tmp/x", "my'branch"); expect(result).not.toBeNull(); }); it("rejects branch name with backslash", () => { const result = validateCreateWorktreeInput("/tmp/x", "feature\\name"); expect(result).not.toBeNull(); }); it("rejects branch name with asterisk", () => { const result = validateCreateWorktreeInput("/tmp/x", "feature*name"); expect(result).not.toBeNull(); }); it("rejects branch name with question mark", () => { const result = validateCreateWorktreeInput("/tmp/x", "feature?name"); expect(result).not.toBeNull(); }); }); describe("buildWorktreeAddCommand (Issue #4)", () => { it("returns command with -b flag when create_branch is true", () => { const result = buildWorktreeAddCommand({ path: "/tmp/new-worktree", branch: "feature-x", createBranch: true, }); expect(result).toEqual([ "worktree", "add", "-b", "feature-x", "/tmp/new-worktree", ]); }); it("returns command without -b flag when create_branch is false", () => { const result = buildWorktreeAddCommand({ path: "/tmp/new-worktree", branch: "existing-branch", createBranch: false, }); expect(result).toEqual([ "worktree", "add", "existing-branch", "/tmp/new-worktree", ]); }); it("defaults createBranch to true when not provided", () => { const result = buildWorktreeAddCommand({ path: "/tmp/new-worktree", branch: "new-feature", }); expect(result).toContain("-b"); }); it("places path as the last argument", () => { const result = buildWorktreeAddCommand({ path: "/tmp/new-worktree", branch: "feature-x", createBranch: true, }); expect(result[result.length - 1]).toBe("/tmp/new-worktree"); }); }); describe("validatePathDoesNotExist (Issue #4)", () => { it("returns null when path does not exist", () => { const result = validatePathDoesNotExist( "/tmp/nonexistent-path-abc123", (p) => p === "/existing/path", ); expect(result).toBeNull(); }); it("returns error with path in message when path exists", () => { const existingPath = "/tmp/existing-worktree"; const result = validatePathDoesNotExist(existingPath, (p) => p === existingPath); expect(result).not.toBeNull(); if (result) expect(result.message).toContain("/tmp/existing-worktree"); if (result) expect(result.message.toLowerCase()).toContain("already exists"); }); it("suggests choosing a different location", () => { const existingPath = "/tmp/existing-worktree"; const result = validatePathDoesNotExist(existingPath, (p) => p === existingPath); expect(result).not.toBeNull(); if (result) expect(result.suggestion?.toLowerCase()).toContain("different"); }); }); describe("isCurrentBranch (Issue #4)", () => { it("returns true when branch matches current branch", () => { expect(isCurrentBranch("main", "main")).toBe(true); }); it("returns false when branch differs from current branch", () => { expect(isCurrentBranch("feature-x", "main")).toBe(false); }); it("handles branch names with slashes", () => { expect(isCurrentBranch("feature/my-branch", "main")).toBe(false); expect(isCurrentBranch("feature/my-branch", "feature/my-branch")).toBe(true); }); }); // ─── Integration Tests for git_worktree_create execute flow (Task 004) ──── describe("git_worktree_create execute flow (Issue #4)", () => { // Minimal mock types matching what the extension needs interface MockExecResult { exitCode: number; stdout: string; stderr: string; } interface MockContext { cwd: string; } interface MockPI { exec: (cmd: string, args: string[], opts?: { cwd: string }) => Promise; } describe("validation pipeline ordering", () => { it("rejects empty path before checking filesystem", async () => { const result = validateCreateWorktreeInput("", "feature-x"); expect(result).not.toBeNull(); expect(result?.message).toContain("cannot be empty"); }); it("rejects invalid branch name before checking filesystem", async () => { const result = validateCreateWorktreeInput("/tmp/x", "-bad-name"); expect(result).not.toBeNull(); expect(result?.message).toContain("invalid character"); }); }); describe("path existence check", () => { it("returns error when path exists", () => { const existingPath = "/tmp/existing-worktree"; const result = validatePathDoesNotExist(existingPath, (p) => p === existingPath); expect(result).not.toBeNull(); expect(result?.message).toContain("/tmp/existing-worktree"); expect(result?.suggestion).toContain("different"); }); it("returns null when path does not exist", () => { const result = validatePathDoesNotExist( "/tmp/nonexistent-path-abc123", (p) => p === "/existing/path", ); expect(result).toBeNull(); }); }); describe("current branch check", () => { it("detects when target matches current branch", () => { expect(isCurrentBranch("main", "main")).toBe(true); }); it("allows different branch from current", () => { expect(isCurrentBranch("feature-x", "main")).toBe(false); }); }); describe("command building after all validations pass", () => { it("builds correct command with createBranch=true", () => { const args = buildWorktreeAddCommand({ path: "/tmp/new-worktree", branch: "feature-x", createBranch: true, }); expect(args).toEqual(["worktree", "add", "-b", "feature-x", "/tmp/new-worktree"]); }); it("builds correct command with createBranch=false", () => { const args = buildWorktreeAddCommand({ path: "/tmp/new-worktree", branch: "existing-branch", createBranch: false, }); expect(args).toEqual(["worktree", "add", "existing-branch", "/tmp/new-worktree"]); }); it("defaults createBranch to true when omitted", () => { const args = buildWorktreeAddCommand({ path: "/tmp/new-worktree", branch: "new-feature", }); expect(args).toContain("-b"); }); }); describe("error formatting consistency", () => { it("formats input validation error with suggestion", () => { const result = validateCreateWorktreeInput("", "feature-x"); expect(result).not.toBeNull(); expect(result?.suggestion).toBeDefined(); expect(result?.suggestion).toContain("Provide a valid path"); }); it("formats path exists error with suggestion", () => { const result = validatePathDoesNotExist("/tmp/x", () => true); expect(result).not.toBeNull(); expect(result?.suggestion).toBeDefined(); expect(result?.suggestion).toContain("different"); }); it("formats branch name error with suggestion", () => { const result = validateCreateWorktreeInput("/tmp/x", "bad name here"); expect(result).not.toBeNull(); expect(result?.suggestion).toBeDefined(); expect(result?.suggestion).toContain("hyphens"); }); }); });