test(FN-4935): complete Step 5 — liveness gate regression coverage
Ref: Runfusion/Fusion#601 Fusion-Task-Id: FN-4935 Fusion-Task-Lineage: 8c842b69-1427-47be-9ba5-8ec66449cc7c
This commit is contained in:
committed by
gsxdsm
parent
bac93a4977
commit
f562c856da
@@ -3,6 +3,7 @@ import "./executor-test-helpers.js";
|
|||||||
import { TaskExecutor } from "../executor.js";
|
import { TaskExecutor } from "../executor.js";
|
||||||
import * as worktreePool from "../worktree-pool.js";
|
import * as worktreePool from "../worktree-pool.js";
|
||||||
import { resolveWorktreesDir } from "../worktree-paths.js";
|
import { resolveWorktreesDir } from "../worktree-paths.js";
|
||||||
|
import * as worktreeAcquisition from "../worktree-acquisition.js";
|
||||||
import { createMockStore, mockedCreateFnAgent, mockedExecSync, resetExecutorMocks } from "./executor-test-helpers.js";
|
import { createMockStore, mockedCreateFnAgent, mockedExecSync, resetExecutorMocks } from "./executor-test-helpers.js";
|
||||||
|
|
||||||
function task(overrides: Record<string, unknown> = {}) {
|
function task(overrides: Record<string, unknown> = {}) {
|
||||||
@@ -25,6 +26,7 @@ function task(overrides: Record<string, unknown> = {}) {
|
|||||||
|
|
||||||
describe("FN-4114 worktree liveness assertion", () => {
|
describe("FN-4114 worktree liveness assertion", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
resetExecutorMocks();
|
resetExecutorMocks();
|
||||||
mockedExecSync.mockImplementation((cmd: string) => {
|
mockedExecSync.mockImplementation((cmd: string) => {
|
||||||
if (cmd.includes("rev-parse HEAD")) return Buffer.from("abc123\n");
|
if (cmd.includes("rev-parse HEAD")) return Buffer.from("abc123\n");
|
||||||
@@ -36,19 +38,26 @@ describe("FN-4114 worktree liveness assertion", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("FN-4114 aborts before createFnAgent when worktree is missing", async () => {
|
it("FN-4114 aborts before createFnAgent when worktree is missing", async () => {
|
||||||
vi.spyOn(worktreePool, "isUsableTaskWorktree").mockResolvedValue(false);
|
vi.spyOn(worktreeAcquisition, "acquireTaskWorktree").mockResolvedValue({
|
||||||
|
worktreePath: "/repo/.worktrees/swift-falcon",
|
||||||
|
branch: "fusion/fn-4114",
|
||||||
|
source: "pool",
|
||||||
|
hydrated: true,
|
||||||
|
isResume: false,
|
||||||
|
});
|
||||||
|
vi.spyOn(worktreePool, "classifyTaskWorktree").mockResolvedValue({ ok: false, classification: "missing", reason: "worktree directory does not exist" });
|
||||||
const store = createMockStore();
|
const store = createMockStore();
|
||||||
store.getTask.mockResolvedValue(task());
|
store.getTask.mockResolvedValue(task({ sessionFile: null }));
|
||||||
|
|
||||||
const executor = new TaskExecutor(store as any, "/repo");
|
const executor = new TaskExecutor(store as any, "/repo");
|
||||||
await executor.execute(task() as any);
|
await executor.execute(task({ sessionFile: null }) as any);
|
||||||
|
|
||||||
expect(mockedCreateFnAgent).not.toHaveBeenCalled();
|
expect(mockedCreateFnAgent).not.toHaveBeenCalled();
|
||||||
expect(store.moveTask).toHaveBeenCalledWith("FN-4114", "todo", { preserveProgress: true });
|
expect(store.moveTask).toHaveBeenCalledWith("FN-4114", "todo", { preserveProgress: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("FN-4114 aborts when worktree realpath collides with repo root", async () => {
|
it("FN-4114 aborts when worktree realpath collides with repo root", async () => {
|
||||||
vi.spyOn(worktreePool, "isUsableTaskWorktree").mockResolvedValue(true);
|
vi.spyOn(worktreePool, "classifyTaskWorktree").mockResolvedValue({ ok: true });
|
||||||
mockedExecSync.mockImplementation((cmd: string) => {
|
mockedExecSync.mockImplementation((cmd: string) => {
|
||||||
if (cmd.includes("rev-parse HEAD")) return Buffer.from("abc123\n");
|
if (cmd.includes("rev-parse HEAD")) return Buffer.from("abc123\n");
|
||||||
return Buffer.from("");
|
return Buffer.from("");
|
||||||
@@ -68,7 +77,7 @@ describe("FN-4114 worktree liveness assertion", () => {
|
|||||||
{ name: "absolute worktreesDir", settings: { worktreesDir: "/custom/trees" }, outsidePath: "/repo/not-a-worktree" },
|
{ name: "absolute worktreesDir", settings: { worktreesDir: "/custom/trees" }, outsidePath: "/repo/not-a-worktree" },
|
||||||
{ name: "relative worktreesDir", settings: { worktreesDir: "custom-trees" }, outsidePath: "/repo/not-a-worktree" },
|
{ name: "relative worktreesDir", settings: { worktreesDir: "custom-trees" }, outsidePath: "/repo/not-a-worktree" },
|
||||||
])("FN-4114 enforces configured worktreesDir ($name)", async ({ settings, outsidePath }) => {
|
])("FN-4114 enforces configured worktreesDir ($name)", async ({ settings, outsidePath }) => {
|
||||||
vi.spyOn(worktreePool, "isUsableTaskWorktree").mockResolvedValue(true);
|
vi.spyOn(worktreePool, "classifyTaskWorktree").mockResolvedValue({ ok: true });
|
||||||
const store = createMockStore();
|
const store = createMockStore();
|
||||||
const baseSettings = await store.getSettings();
|
const baseSettings = await store.getSettings();
|
||||||
const mergedSettings = { ...baseSettings, ...settings };
|
const mergedSettings = { ...baseSettings, ...settings };
|
||||||
@@ -99,7 +108,7 @@ describe("FN-4114 worktree liveness assertion", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("FN-4114 accepts usable pool-acquired worktrees", async () => {
|
it("FN-4114 accepts usable pool-acquired worktrees", async () => {
|
||||||
vi.spyOn(worktreePool, "isUsableTaskWorktree").mockResolvedValue(true);
|
vi.spyOn(worktreePool, "classifyTaskWorktree").mockResolvedValue({ ok: true });
|
||||||
const store = createMockStore();
|
const store = createMockStore();
|
||||||
store.getTask.mockResolvedValue(task());
|
store.getTask.mockResolvedValue(task());
|
||||||
|
|
||||||
@@ -112,4 +121,46 @@ describe("FN-4114 worktree liveness assertion", () => {
|
|||||||
|
|
||||||
expect(mockedCreateFnAgent).toHaveBeenCalled();
|
expect(mockedCreateFnAgent).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("FN-4935 skips liveness gate for fresh acquisition even with assigned worktree", async () => {
|
||||||
|
const classifySpy = vi.spyOn(worktreePool, "classifyTaskWorktree").mockResolvedValue({ ok: false, classification: "unregistered", reason: "not registered in git worktree list" });
|
||||||
|
vi.spyOn(worktreeAcquisition, "acquireTaskWorktree").mockResolvedValue({
|
||||||
|
worktreePath: "/repo/.worktrees/new-path",
|
||||||
|
branch: "fusion/fn-4114",
|
||||||
|
source: "fresh",
|
||||||
|
hydrated: true,
|
||||||
|
isResume: false,
|
||||||
|
});
|
||||||
|
const store = createMockStore();
|
||||||
|
store.getTask.mockResolvedValue(task({ worktree: "/repo/.worktrees/stale" }));
|
||||||
|
|
||||||
|
mockedCreateFnAgent.mockImplementation(async () => ({
|
||||||
|
session: { prompt: vi.fn().mockResolvedValue(undefined), dispose: vi.fn() },
|
||||||
|
}) as any);
|
||||||
|
|
||||||
|
const executor = new TaskExecutor(store as any, "/repo");
|
||||||
|
await executor.execute(task({ worktree: "/repo/.worktrees/stale" }) as any);
|
||||||
|
|
||||||
|
expect(classifySpy).not.toHaveBeenCalled();
|
||||||
|
expect(mockedCreateFnAgent).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("FN-4935 runs liveness gate for pooled/reused assigned worktree", async () => {
|
||||||
|
const classifySpy = vi.spyOn(worktreePool, "classifyTaskWorktree").mockResolvedValue({ ok: false, classification: "unregistered", reason: "not registered in git worktree list" });
|
||||||
|
vi.spyOn(worktreeAcquisition, "acquireTaskWorktree").mockResolvedValue({
|
||||||
|
worktreePath: "/repo/.worktrees/swift-falcon",
|
||||||
|
branch: "fusion/fn-4114",
|
||||||
|
source: "pool",
|
||||||
|
hydrated: true,
|
||||||
|
isResume: false,
|
||||||
|
});
|
||||||
|
const store = createMockStore();
|
||||||
|
store.getTask.mockResolvedValue(task({ worktree: "/repo/.worktrees/swift-falcon", sessionFile: null }));
|
||||||
|
|
||||||
|
const executor = new TaskExecutor(store as any, "/repo");
|
||||||
|
await executor.execute(task({ worktree: "/repo/.worktrees/swift-falcon", sessionFile: null }) as any);
|
||||||
|
|
||||||
|
expect(classifySpy).toHaveBeenCalled();
|
||||||
|
expect(store.moveTask).toHaveBeenCalledWith("FN-4114", "todo", { preserveProgress: true });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import "../executor-test-helpers.js";
|
||||||
|
import { TaskExecutor } from "../../executor.js";
|
||||||
|
import { createFnAgent } from "../../pi.js";
|
||||||
|
import * as worktreePool from "../../worktree-pool.js";
|
||||||
|
import * as worktreeAcquisition from "../../worktree-acquisition.js";
|
||||||
|
import { createMockStore, mockedExecSync, resetExecutorMocks } from "../executor-test-helpers.js";
|
||||||
|
|
||||||
|
const mockedCreateFnAgent = vi.mocked(createFnAgent);
|
||||||
|
|
||||||
|
function makeTask(overrides: Record<string, unknown> = {}) {
|
||||||
|
return {
|
||||||
|
id: "FN-4935-T",
|
||||||
|
title: "Liveness gate",
|
||||||
|
description: "",
|
||||||
|
column: "in-progress",
|
||||||
|
dependencies: [],
|
||||||
|
worktree: "/repo/.worktrees/stale-path",
|
||||||
|
branch: "fusion/fn-4935-t",
|
||||||
|
taskDoneRetryCount: 0,
|
||||||
|
steps: [{ name: "Step 1", status: "in-progress" as const }],
|
||||||
|
currentStep: 0,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
...overrides,
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("reliability interactions: FN-4935 executor liveness gate", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
resetExecutorMocks();
|
||||||
|
mockedExecSync.mockImplementation((cmd: string) => {
|
||||||
|
if (cmd.includes("rev-parse HEAD")) return Buffer.from("abc123\n");
|
||||||
|
if (cmd.includes("rev-parse --show-toplevel")) return Buffer.from("/repo/.worktrees/new-path\n");
|
||||||
|
if (cmd.includes("rev-parse --abbrev-ref HEAD")) return Buffer.from("fusion/fn-4935-t\n");
|
||||||
|
if (cmd.includes("rev-list --count")) return Buffer.from("1\n");
|
||||||
|
return Buffer.from("");
|
||||||
|
});
|
||||||
|
mockedCreateFnAgent.mockImplementation(async () => ({
|
||||||
|
session: { prompt: vi.fn().mockResolvedValue(undefined), dispose: vi.fn() },
|
||||||
|
}) as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips gate for fresh acquisition", async () => {
|
||||||
|
const classifySpy = vi.spyOn(worktreePool, "classifyTaskWorktree").mockResolvedValue({ ok: false, classification: "unregistered", reason: "not registered in git worktree list" });
|
||||||
|
vi.spyOn(worktreeAcquisition, "acquireTaskWorktree").mockResolvedValue({
|
||||||
|
worktreePath: "/repo/.worktrees/new-path",
|
||||||
|
branch: "fusion/fn-4935-t",
|
||||||
|
source: "fresh",
|
||||||
|
hydrated: true,
|
||||||
|
isResume: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = createMockStore();
|
||||||
|
const executor = new TaskExecutor(store as any, "/repo");
|
||||||
|
await executor.execute(makeTask());
|
||||||
|
|
||||||
|
expect(classifySpy).not.toHaveBeenCalled();
|
||||||
|
expect(mockedCreateFnAgent).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gates pooled acquisition and requeues with audit event", async () => {
|
||||||
|
vi.spyOn(worktreeAcquisition, "acquireTaskWorktree").mockResolvedValue({
|
||||||
|
worktreePath: "/repo/.worktrees/new-path",
|
||||||
|
branch: "fusion/fn-4935-t",
|
||||||
|
source: "pool",
|
||||||
|
hydrated: true,
|
||||||
|
isResume: false,
|
||||||
|
});
|
||||||
|
vi.spyOn(worktreePool, "classifyTaskWorktree").mockResolvedValue({ ok: false, classification: "unregistered", reason: "not registered in git worktree list" });
|
||||||
|
vi.spyOn(worktreePool, "describeRegisteredWorktrees").mockResolvedValue({
|
||||||
|
rawOutput: "",
|
||||||
|
canonicalized: ["/repo/.worktrees/a", "/repo/.worktrees/b"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = createMockStore();
|
||||||
|
const events: any[] = [];
|
||||||
|
store.recordRunAuditEvent = vi.fn(async (event: any) => events.push(event));
|
||||||
|
|
||||||
|
const executor = new TaskExecutor(store as any, "/repo");
|
||||||
|
await executor.execute(makeTask({ sessionFile: null }));
|
||||||
|
|
||||||
|
expect(store.logEntry).toHaveBeenCalledWith(
|
||||||
|
"FN-4935-T",
|
||||||
|
expect.stringContaining("not_usable_task_worktree:unregistered"),
|
||||||
|
undefined,
|
||||||
|
expect.anything(),
|
||||||
|
);
|
||||||
|
expect(store.logEntry).toHaveBeenCalledWith(
|
||||||
|
"FN-4935-T",
|
||||||
|
expect.stringContaining("registered=[/repo/.worktrees/a, /repo/.worktrees/b]"),
|
||||||
|
undefined,
|
||||||
|
expect.anything(),
|
||||||
|
);
|
||||||
|
expect(store.moveTask).toHaveBeenCalledWith("FN-4935-T", "todo", { preserveProgress: true });
|
||||||
|
expect(store.updateTask).toHaveBeenCalledWith("FN-4935-T", expect.objectContaining({ taskDoneRetryCount: 1 }));
|
||||||
|
expect(events.some((event) => (event.type === "worktree:incomplete-detected" || event.mutationType === "worktree:incomplete-detected") && event.metadata?.source === "executor-liveness-gate" && event.metadata?.terminalAction === "requeue-todo")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
"missing",
|
||||||
|
"incomplete",
|
||||||
|
"unregistered",
|
||||||
|
"outside-work-tree",
|
||||||
|
] as const)("formats failure message for %s", async (classification) => {
|
||||||
|
vi.spyOn(worktreeAcquisition, "acquireTaskWorktree").mockResolvedValue({
|
||||||
|
worktreePath: "/repo/.worktrees/new-path",
|
||||||
|
branch: "fusion/fn-4935-t",
|
||||||
|
source: "existing",
|
||||||
|
hydrated: true,
|
||||||
|
isResume: true,
|
||||||
|
});
|
||||||
|
vi.spyOn(worktreePool, "classifyTaskWorktree").mockResolvedValue({ ok: false, classification, reason: `reason-${classification}` });
|
||||||
|
|
||||||
|
const store = createMockStore();
|
||||||
|
const executor = new TaskExecutor(store as any, "/repo");
|
||||||
|
await executor.execute(makeTask());
|
||||||
|
|
||||||
|
expect(store.logEntry).toHaveBeenCalledWith(
|
||||||
|
"FN-4935-T",
|
||||||
|
expect.stringContaining(`not_usable_task_worktree:${classification} (reason-${classification})`),
|
||||||
|
undefined,
|
||||||
|
expect.anything(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parks in-review at retry cap", async () => {
|
||||||
|
vi.spyOn(worktreeAcquisition, "acquireTaskWorktree").mockResolvedValue({
|
||||||
|
worktreePath: "/repo/.worktrees/new-path",
|
||||||
|
branch: "fusion/fn-4935-t",
|
||||||
|
source: "pool",
|
||||||
|
hydrated: true,
|
||||||
|
isResume: false,
|
||||||
|
});
|
||||||
|
vi.spyOn(worktreePool, "classifyTaskWorktree").mockResolvedValue({ ok: false, classification: "incomplete", reason: "missing .git metadata" });
|
||||||
|
|
||||||
|
const store = createMockStore();
|
||||||
|
const events: any[] = [];
|
||||||
|
store.recordRunAuditEvent = vi.fn(async (event: any) => events.push(event));
|
||||||
|
|
||||||
|
const executor = new TaskExecutor(store as any, "/repo");
|
||||||
|
await executor.execute(makeTask({ taskDoneRetryCount: 999, sessionFile: null }));
|
||||||
|
|
||||||
|
expect(store.moveTask).toHaveBeenCalledWith("FN-4935-T", "in-review");
|
||||||
|
expect(events.some((event) => (event.type === "worktree:incomplete-detected" || event.mutationType === "worktree:incomplete-detected") && event.metadata?.terminalAction === "park-in-review")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("continues when registered snapshot helper fails", async () => {
|
||||||
|
vi.spyOn(worktreeAcquisition, "acquireTaskWorktree").mockResolvedValue({
|
||||||
|
worktreePath: "/repo/.worktrees/new-path",
|
||||||
|
branch: "fusion/fn-4935-t",
|
||||||
|
source: "pool",
|
||||||
|
hydrated: true,
|
||||||
|
isResume: false,
|
||||||
|
});
|
||||||
|
vi.spyOn(worktreePool, "classifyTaskWorktree").mockResolvedValue({ ok: false, classification: "unregistered", reason: "not registered in git worktree list" });
|
||||||
|
vi.spyOn(worktreePool, "describeRegisteredWorktrees").mockRejectedValueOnce(new Error("boom"));
|
||||||
|
|
||||||
|
const store = createMockStore();
|
||||||
|
const executor = new TaskExecutor(store as any, "/repo");
|
||||||
|
await executor.execute(makeTask({ sessionFile: null }));
|
||||||
|
|
||||||
|
expect(store.logEntry).toHaveBeenCalledWith(
|
||||||
|
"FN-4935-T",
|
||||||
|
expect.stringContaining("not_usable_task_worktree:unregistered"),
|
||||||
|
undefined,
|
||||||
|
expect.anything(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user