## Summary Implement the `git_worktree_list` tool that parses `git worktree list` output into structured data with path, branch, commit hash, and current directory marker. ## Changes - Implemented `parseWorktreeList()` helper to parse raw `git worktree list` output - Handles standard branches (e.g., `[main]`) - Handles detached HEAD (marked as `(detached)`) - Marks the current working directory with `isCurrent: true` - Wired parser into `git_worktree_list` tool execute function - Returns both formatted display text and structured `details.worktrees` array - Added error handling for non-git-repo directories - Restructured tool definitions inside extension factory to capture `pi` via closure ## Testing - [x] Parsing logic verified against real `git worktree list` output (3 worktrees) - [ ] Manual verification in pi TUI pending extension load test ## Checklist - [x] Self-reviewed the diff - [x] Code follows project conventions (AGENTS.md, DESIGN.md, DOMAIN.md) - [x] Descriptive naming (no single-character variables) - [x] Error handling with clear messages and suggestions Co-authored-by: David Kong <davkon@gmail.com> Reviewed-on: #16
117 lines
3.9 KiB
TypeScript
117 lines
3.9 KiB
TypeScript
/**
|
|
* 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";
|
|
|
|
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");
|
|
});
|
|
});
|
|
});
|