Fix executor worktree invariant handling by re-anchoring nested task worktree paths to the actual git top-level. - add nested worktree root detection that only re-anchors when the top-level is a registered worktree inside the configured worktrees directory - update executor liveness gating and verifyWorktreeInvariants to persist re-anchored task.worktree values and retry checks safely - emit a new run-audit git mutation (worktree:reanchored) and add reliability tests/docs coverage plus a patch changeset Files changed: .changeset/fn-5772-worktree-reanchor.md | 7 +++ docs/architecture.md | 4 +- packages/engine/src/__tests__/reliability-interactions/executor-liveness-gate.test.ts | 33 ++++++++++ packages/engine/src/__tests__/verify-worktree-invariants-missing.test.ts | 62 +++++++++++++++++- packages/engine/src/__tests__/worktree-reanchor-nested-root.test.ts | 73 ++++++++++++++++++++++ packages/engine/src/executor.ts | 54 ++++++++++++++-- packages/engine/src/run-audit.ts | 1 + packages/engine/src/worktree-pool.ts | 63 +++++++++++++++++++ 8 files changed, 290 insertions(+), 7 deletions(-) Fusion-Task-Id: FN-5772 Fusion-Task-Lineage: 475de161-7bc1-4359-bb76-20c4c07196d9
155 lines
5.2 KiB
TypeScript
155 lines
5.2 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import "./executor-test-helpers.js";
|
|
import { TaskExecutor } from "../executor.js";
|
|
import { createMockStore, mockedExecSync, 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("re-anchors nested task worktree to registered root and passes invariants", async () => {
|
|
const task = {
|
|
id: "FN-9004",
|
|
title: "Test",
|
|
description: "Test",
|
|
column: "in-progress",
|
|
worktree: "/repo/.worktrees/gentle-flame/packages/core",
|
|
branch: "fusion/fn-9004",
|
|
dependencies: [],
|
|
steps: [],
|
|
currentStep: 0,
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
};
|
|
|
|
mockedExistsSync.mockReturnValue(true);
|
|
mockedExecSync.mockImplementation((cmd: string) => {
|
|
if (cmd.includes("rev-parse --show-toplevel")) return Buffer.from("/repo/.worktrees/gentle-flame\n");
|
|
if (cmd.includes("worktree list --porcelain")) {
|
|
return Buffer.from("worktree /repo\nbranch refs/heads/main\n\nworktree /repo/.worktrees/gentle-flame\nbranch refs/heads/fusion/fn-9004\n");
|
|
}
|
|
if (cmd.includes("rev-parse --abbrev-ref HEAD")) return Buffer.from("fusion/fn-9004\n");
|
|
if (cmd.includes("rev-list --count")) return Buffer.from("1\n");
|
|
return Buffer.from("");
|
|
});
|
|
|
|
const result = await (executor as any).verifyWorktreeInvariants(task);
|
|
|
|
expect(result).toEqual({ ok: true });
|
|
expect(store.updateTask).toHaveBeenCalledWith("FN-9004", { worktree: "/repo/.worktrees/gentle-flame" });
|
|
});
|
|
|
|
it("preserves wrong_toplevel for non-reanchorable mismatch", async () => {
|
|
const task = {
|
|
id: "FN-9005",
|
|
title: "Test",
|
|
description: "Test",
|
|
column: "in-progress",
|
|
worktree: "/repo/.worktrees/gentle-flame/packages/core",
|
|
branch: "fusion/fn-9005",
|
|
dependencies: [],
|
|
steps: [],
|
|
currentStep: 0,
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
};
|
|
|
|
mockedExistsSync.mockReturnValue(true);
|
|
mockedExecSync.mockImplementation((cmd: string) => {
|
|
if (cmd.includes("rev-parse --show-toplevel")) return Buffer.from("/repo\n");
|
|
return Buffer.from("");
|
|
});
|
|
|
|
const result = await (executor as any).verifyWorktreeInvariants(task);
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.reason).toBe("wrong_toplevel");
|
|
expect(store.updateTask).not.toHaveBeenCalledWith("FN-9005", expect.objectContaining({ worktree: expect.any(String) }));
|
|
});
|
|
|
|
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");
|
|
});
|
|
});
|