Files
stack/apps/orchestrator/src/git/worktree-manager.service.spec.ts
Jason Woltje fc87494137
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
fix(orchestrator): resolve all M6 remediation issues (#260-#269)
Addresses all 10 quality remediation issues for the orchestrator module:

TypeScript & Type Safety:
- #260: Fix TypeScript compilation errors in tests
- #261: Replace explicit 'any' types with proper typed mocks

Error Handling & Reliability:
- #262: Fix silent cleanup failures - return structured results
- #263: Fix silent Valkey event parsing failures with proper error handling
- #266: Improve error context in Docker operations
- #267: Fix secret scanner false negatives on file read errors
- #268: Fix worktree cleanup error swallowing

Testing & Quality:
- #264: Add queue integration tests (coverage 15% → 85%)
- #265: Fix Prettier formatting violations
- #269: Update outdated TODO comments

All tests passing (406/406), TypeScript compiles cleanly, ESLint clean.

Fixes #260, Fixes #261, Fixes #262, Fixes #263, Fixes #264
Fixes #265, Fixes #266, Fixes #267, Fixes #268, Fixes #269

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 12:44:04 -06:00

317 lines
11 KiB
TypeScript

import { ConfigService } from "@nestjs/config";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { WorktreeManagerService } from "./worktree-manager.service";
import { GitOperationsService } from "./git-operations.service";
import { WorktreeError } from "./types";
import * as path from "path";
// Mock simple-git
const mockGit = {
raw: vi.fn(),
};
vi.mock("simple-git", () => ({
simpleGit: vi.fn(() => mockGit),
}));
describe("WorktreeManagerService", () => {
let service: WorktreeManagerService;
let mockConfigService: ConfigService;
let mockGitOperationsService: GitOperationsService;
beforeEach(() => {
// Reset all mocks
vi.clearAllMocks();
// Create mock config service
mockConfigService = {
get: vi.fn((key: string) => {
if (key === "orchestrator.git.userName") return "Test User";
if (key === "orchestrator.git.userEmail") return "test@example.com";
return undefined;
}),
} as unknown as ConfigService;
// Create mock git operations service
mockGitOperationsService = new GitOperationsService(mockConfigService);
// Create service with mocks
service = new WorktreeManagerService(mockGitOperationsService);
});
describe("createWorktree", () => {
it("should create worktree with correct naming convention", async () => {
const repoPath = "/tmp/test-repo";
const agentId = "agent-123";
const taskId = "task-456";
const expectedPath = path.join("/tmp", "test-repo_worktrees", `agent-${agentId}-${taskId}`);
const branchName = `agent-${agentId}-${taskId}`;
mockGit.raw.mockResolvedValue(
`worktree ${expectedPath}\nHEAD abc123\nbranch refs/heads/${branchName}`
);
const result = await service.createWorktree(repoPath, agentId, taskId);
expect(result).toBeDefined();
expect(result.path).toBe(expectedPath);
expect(result.branch).toBe(branchName);
expect(mockGit.raw).toHaveBeenCalledWith([
"worktree",
"add",
expectedPath,
"-b",
branchName,
"develop",
]);
});
it("should create worktree with custom base branch", async () => {
const repoPath = "/tmp/test-repo";
const agentId = "agent-123";
const taskId = "task-456";
const baseBranch = "main";
const expectedPath = path.join("/tmp", "test-repo_worktrees", `agent-${agentId}-${taskId}`);
const branchName = `agent-${agentId}-${taskId}`;
mockGit.raw.mockResolvedValue(
`worktree ${expectedPath}\nHEAD abc123\nbranch refs/heads/${branchName}`
);
await service.createWorktree(repoPath, agentId, taskId, baseBranch);
expect(mockGit.raw).toHaveBeenCalledWith([
"worktree",
"add",
expectedPath,
"-b",
branchName,
baseBranch,
]);
});
it("should throw WorktreeError if worktree already exists", async () => {
const error = new Error("fatal: 'agent-123-task-456' already exists");
mockGit.raw.mockRejectedValue(error);
await expect(
service.createWorktree("/tmp/test-repo", "agent-123", "task-456")
).rejects.toThrow(WorktreeError);
try {
await service.createWorktree("/tmp/test-repo", "agent-123", "task-456");
} catch (e) {
expect(e).toBeInstanceOf(WorktreeError);
expect((e as WorktreeError).operation).toBe("createWorktree");
expect((e as WorktreeError).cause).toBe(error);
}
});
it("should throw WorktreeError on git command failure", async () => {
const error = new Error("git command failed");
mockGit.raw.mockRejectedValue(error);
await expect(
service.createWorktree("/tmp/test-repo", "agent-123", "task-456")
).rejects.toThrow(WorktreeError);
});
it("should validate agentId is not empty", async () => {
await expect(service.createWorktree("/tmp/test-repo", "", "task-456")).rejects.toThrow(
"agentId is required"
);
});
it("should validate taskId is not empty", async () => {
await expect(service.createWorktree("/tmp/test-repo", "agent-123", "")).rejects.toThrow(
"taskId is required"
);
});
it("should validate repoPath is not empty", async () => {
await expect(service.createWorktree("", "agent-123", "task-456")).rejects.toThrow(
"repoPath is required"
);
});
});
describe("removeWorktree", () => {
it("should remove worktree successfully", async () => {
const worktreePath = "/tmp/test-repo_worktrees/agent-123-task-456";
mockGit.raw.mockResolvedValue("");
await service.removeWorktree(worktreePath);
expect(mockGit.raw).toHaveBeenCalledWith(["worktree", "remove", worktreePath, "--force"]);
});
it("should handle non-existent worktree gracefully", async () => {
const worktreePath = "/tmp/test-repo_worktrees/non-existent";
const error = new Error("fatal: 'non-existent' is not a working tree");
mockGit.raw.mockRejectedValue(error);
// Should not throw, just log warning
await expect(service.removeWorktree(worktreePath)).resolves.not.toThrow();
});
it("should throw WorktreeError on removal failure", async () => {
const worktreePath = "/tmp/test-repo_worktrees/agent-123-task-456";
const error = new Error("permission denied");
mockGit.raw.mockRejectedValue(error);
// Should throw for non-worktree-not-found errors
await expect(service.removeWorktree(worktreePath)).rejects.toThrow();
});
it("should validate worktreePath is not empty", async () => {
await expect(service.removeWorktree("")).rejects.toThrow("worktreePath is required");
});
});
describe("listWorktrees", () => {
it("should return empty array when no worktrees exist", async () => {
const repoPath = "/tmp/test-repo";
mockGit.raw.mockResolvedValue(`/tmp/test-repo abc123 [develop]`);
const result = await service.listWorktrees(repoPath);
expect(result).toEqual([]);
});
it("should list all active worktrees", async () => {
const repoPath = "/tmp/test-repo";
const output = `/tmp/test-repo abc123 [develop]
/tmp/test-repo_worktrees/agent-123-task-456 def456 [agent-123-task-456]
/tmp/test-repo_worktrees/agent-789-task-012 abc789 [agent-789-task-012]`;
mockGit.raw.mockResolvedValue(output);
const result = await service.listWorktrees(repoPath);
expect(result).toHaveLength(2);
expect(result[0].path).toBe("/tmp/test-repo_worktrees/agent-123-task-456");
expect(result[0].commit).toBe("def456");
expect(result[0].branch).toBe("agent-123-task-456");
expect(result[1].path).toBe("/tmp/test-repo_worktrees/agent-789-task-012");
expect(result[1].commit).toBe("abc789");
expect(result[1].branch).toBe("agent-789-task-012");
});
it("should parse worktree info correctly", async () => {
const repoPath = "/tmp/test-repo";
const output = `/tmp/test-repo abc123 [develop]
/tmp/test-repo_worktrees/agent-123-task-456 def456 [agent-123-task-456]`;
mockGit.raw.mockResolvedValue(output);
const result = await service.listWorktrees(repoPath);
expect(result[0]).toEqual({
path: "/tmp/test-repo_worktrees/agent-123-task-456",
commit: "def456",
branch: "agent-123-task-456",
});
});
it("should throw WorktreeError on git command failure", async () => {
const error = new Error("git command failed");
mockGit.raw.mockRejectedValue(error);
await expect(service.listWorktrees("/tmp/test-repo")).rejects.toThrow(WorktreeError);
});
it("should validate repoPath is not empty", async () => {
await expect(service.listWorktrees("")).rejects.toThrow("repoPath is required");
});
});
describe("cleanupWorktree", () => {
it("should remove worktree on agent completion and return success", async () => {
const repoPath = "/tmp/test-repo";
const agentId = "agent-123";
const taskId = "task-456";
const worktreePath = path.join("/tmp", "test-repo_worktrees", `agent-${agentId}-${taskId}`);
mockGit.raw.mockResolvedValue("");
const result = await service.cleanupWorktree(repoPath, agentId, taskId);
expect(result).toEqual({ success: true });
expect(mockGit.raw).toHaveBeenCalledWith(["worktree", "remove", worktreePath, "--force"]);
});
it("should return failure result on cleanup errors", async () => {
const error = new Error("worktree not found");
mockGit.raw.mockRejectedValue(error);
const result = await service.cleanupWorktree("/tmp/test-repo", "agent-123", "task-456");
expect(result.success).toBe(false);
expect(result.error).toContain("Failed to remove worktree");
});
it("should handle non-Error objects in cleanup errors", async () => {
mockGit.raw.mockRejectedValue("string error");
const result = await service.cleanupWorktree("/tmp/test-repo", "agent-123", "task-456");
expect(result.success).toBe(false);
expect(result.error).toContain("Failed to remove worktree");
});
it("should validate agentId is not empty", async () => {
await expect(service.cleanupWorktree("/tmp/test-repo", "", "task-456")).rejects.toThrow(
"agentId is required"
);
});
it("should validate taskId is not empty", async () => {
await expect(service.cleanupWorktree("/tmp/test-repo", "agent-123", "")).rejects.toThrow(
"taskId is required"
);
});
it("should validate repoPath is not empty", async () => {
await expect(service.cleanupWorktree("", "agent-123", "task-456")).rejects.toThrow(
"repoPath is required"
);
});
});
describe("getWorktreePath", () => {
it("should generate correct worktree path", () => {
const repoPath = "/tmp/test-repo";
const agentId = "agent-123";
const taskId = "task-456";
const expectedPath = path.join("/tmp", "test-repo_worktrees", `agent-${agentId}-${taskId}`);
const result = service.getWorktreePath(repoPath, agentId, taskId);
expect(result).toBe(expectedPath);
});
it("should handle repo paths with trailing slashes", () => {
const repoPath = "/tmp/test-repo/";
const agentId = "agent-123";
const taskId = "task-456";
const expectedPath = path.join("/tmp", "test-repo_worktrees", `agent-${agentId}-${taskId}`);
const result = service.getWorktreePath(repoPath, agentId, taskId);
expect(result).toBe(expectedPath);
});
});
describe("getBranchName", () => {
it("should generate correct branch name", () => {
const agentId = "agent-123";
const taskId = "task-456";
const expectedBranch = `agent-${agentId}-${taskId}`;
const result = service.getBranchName(agentId, taskId);
expect(result).toBe(expectedBranch);
});
});
});