Merge pull request #918 from abeperl/fix/fn-009-task-done-missing-worktree
fix: allow fn_task_done when worktree directory missing
This commit is contained in:
@@ -293,6 +293,7 @@
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Fixed `fn_task_done` to gracefully handle missing worktree directories. When a task's worktree has been deleted (common for documentation/coordination tasks with no code changes), `fn_task_done` previously failed with ENOENT attempting to spawn git commands in non-existent directories. The fix adds an `existsSync` check in `verifyWorktreeInvariants` before executing git commands, allowing legitimate task completions to proceed. This is safe because task completion is read-only, deliverables are stored in fusion.db (not the worktree), and if code changes were made, the worktree would exist. Resolves infinite loops where agents couldn't complete tasks without shell access to missing directories.
|
||||
- Updated dependencies [1f0bb7e]
|
||||
- @fusion/core@0.32.0
|
||||
- @fusion/pi-claude-cli@0.32.0
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import "./executor-test-helpers.js";
|
||||
import { TaskExecutor } from "../executor.js";
|
||||
import { createMockStore, mockedExistsSync, resetExecutorMocks } from "./executor-test-helpers.js";
|
||||
|
||||
describe("FN-009: verifyWorktreeInvariants with missing worktree directory", () => {
|
||||
let executor: TaskExecutor;
|
||||
let store: ReturnType<typeof createMockStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
resetExecutorMocks();
|
||||
store = createMockStore();
|
||||
executor = new TaskExecutor(store as any, "/repo");
|
||||
});
|
||||
|
||||
it("returns success when worktree directory does not exist", async () => {
|
||||
const task = {
|
||||
id: "FN-9001",
|
||||
title: "Test",
|
||||
description: "Test",
|
||||
column: "in-progress",
|
||||
worktree: "/repo/.worktrees/missing",
|
||||
branch: "fusion/fn-9001",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Mock existsSync to return false for the worktree path
|
||||
mockedExistsSync.mockImplementation((path: any) => {
|
||||
if (path === "/repo/.worktrees/missing") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// Access the private method using type assertion
|
||||
const result = await (executor as any).verifyWorktreeInvariants(task);
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("does not skip validation when worktree directory exists", async () => {
|
||||
const task = {
|
||||
id: "FN-9002",
|
||||
title: "Test",
|
||||
description: "Test",
|
||||
column: "in-progress",
|
||||
worktree: "/repo/.worktrees/existing",
|
||||
branch: "fusion/fn-9002",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Mock existsSync to return true for the worktree path
|
||||
mockedExistsSync.mockReturnValue(true);
|
||||
|
||||
const result = await (executor as any).verifyWorktreeInvariants(task);
|
||||
|
||||
// When worktree exists, validation proceeds normally.
|
||||
// The exact result depends on git command mocks, but we verify
|
||||
// that the function doesn't return early with { ok: true } due to missing directory.
|
||||
// We verify that existsSync was called by checking it was configured.
|
||||
expect(mockedExistsSync).toHaveBeenCalled();
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it("preserves validation failure when worktree path is null", async () => {
|
||||
const task = {
|
||||
id: "FN-9003",
|
||||
title: "Test",
|
||||
description: "Test",
|
||||
column: "in-progress",
|
||||
worktree: null,
|
||||
branch: "fusion/fn-9003",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const result = await (executor as any).verifyWorktreeInvariants(task);
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.reason).toBe("wrong_toplevel");
|
||||
expect(result.observed).toContain("missing task.worktree");
|
||||
});
|
||||
});
|
||||
@@ -5788,6 +5788,19 @@ export class TaskExecutor {
|
||||
};
|
||||
}
|
||||
|
||||
// FN-009: If worktree directory doesn't exist, skip git validation for task completion.
|
||||
// This is safe because:
|
||||
// 1. Task completion doesn't modify the worktree
|
||||
// 2. Deliverables (task documents, follow-up tasks) are stored in fusion.db
|
||||
// 3. If code changes were made, the worktree would exist
|
||||
// 4. This prevents ENOENT errors when agents complete documentation/coordination tasks
|
||||
if (!existsSync(worktreePath)) {
|
||||
executorLog.log(
|
||||
`${task.id}: worktree directory not found at ${worktreePath} — skipping git validation for task completion`,
|
||||
);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
try {
|
||||
const { stdout } = await execAsync("git rev-parse --show-toplevel", {
|
||||
cwd: worktreePath,
|
||||
|
||||
Reference in New Issue
Block a user