test(engine): repair Full Suite drifts after plan-worktree cutover
Re-pin the execSync allowlist after self-healing/executor line shifts, capture implementation-session tools under the graph-owned pause harness, treat worktree alone as not past planning for replan targets, and age starved- refinement fixtures past the post-escalation cooldown window.
This commit is contained in:
@@ -37,11 +37,11 @@ const allowlist: AllowlistEntry[] = [
|
||||
{ file: "src/already-merged-detector.ts", line: 223, primitive: "execSync", signature: "execSync(`git merge-base --is-ancestor ${shellQuote(branchTip)} ${shellQuote(baseBranch)}`, {", reason: SHORT_GIT_PLUMBING },
|
||||
{ file: "src/already-merged-detector.ts", line: 270, primitive: "execSync", signature: "branchTip = execSync(`git rev-parse --verify ${shellQuote(branchName)}`, {", reason: SHORT_GIT_PLUMBING },
|
||||
{ file: "src/already-merged-detector.ts", line: 345, primitive: "execSync", signature: "execSync(`git rev-parse --verify ${shellQuote(treeBranchName)}`, {", reason: SHORT_GIT_PLUMBING },
|
||||
// FNXC:EngineProcessRules 2026-07-22-17:30: refreshed drifted line numbers for these unchanged git-plumbing call-sites (self-healing.ts 4187/4193/4230/12705, executor.ts 15808) after FN-8490 + graph-owned-cutover follow-ups shifted lines above them. The call-site-level allowlist keys on file:line:signature, so a legitimate call moving lines reads as unmatched-site + stale-entry until the line is re-pinned here.
|
||||
{ file: "src/self-healing.ts", line: 4215, primitive: "execSync", signature: "const tipSha = String(execSync(`git rev-parse --verify ${shellQuote(branch)}`, {", reason: SHORT_GIT_PLUMBING },
|
||||
{ file: "src/self-healing.ts", line: 4221, primitive: "execSync", signature: "const uniqueCommitCount = Number.parseInt(String(execSync(`git rev-list --count ${shellQuote(branch)} --not ${shellQuote(\"main\")}`, {", reason: SHORT_GIT_PLUMBING },
|
||||
{ file: "src/self-healing.ts", line: 4258, primitive: "execSync", signature: "const branchesRaw = String(execSync(\"git branch --list 'fusion/*'\", {", reason: SHORT_GIT_PLUMBING },
|
||||
{ file: "src/self-healing.ts", line: 12839, primitive: "execSync", signature: "execSync(`git branch -d ${shellQuote(branch)}`, {", reason: SHORT_GIT_PLUMBING },
|
||||
// FNXC:EngineProcessRules 2026-07-26-20:50: re-pin drifted short git-plumbing lines after plan-worktree + branch-reclaim work (self-healing inspect/delete, executor baseCommitSha ancestry).
|
||||
{ file: "src/self-healing.ts", line: 4289, primitive: "execSync", signature: "const tipSha = String(execSync(`git rev-parse --verify ${shellQuote(branch)}`, {", reason: SHORT_GIT_PLUMBING },
|
||||
{ file: "src/self-healing.ts", line: 4295, primitive: "execSync", signature: "const uniqueCommitCount = Number.parseInt(String(execSync(`git rev-list --count ${shellQuote(branch)} --not ${shellQuote(\"main\")}`, {", reason: SHORT_GIT_PLUMBING },
|
||||
{ file: "src/self-healing.ts", line: 4332, primitive: "execSync", signature: "const branchesRaw = String(execSync(\"git branch --list 'fusion/*'\", {", reason: SHORT_GIT_PLUMBING },
|
||||
{ file: "src/self-healing.ts", line: 12977, primitive: "execSync", signature: "execSync(`git branch -d ${shellQuote(branch)}`, {", reason: SHORT_GIT_PLUMBING },
|
||||
{ file: "src/merger-workspace-test-commands.ts", line: 204, primitive: "execSync", signature: "changedFilesOutput = execSync(", reason: BOUNDED_GIT_DIFF },
|
||||
{ file: "src/merger-workspace-test-commands.ts", line: 301, primitive: "execSync", signature: "changedFilesOutput = execSync(", reason: BOUNDED_GIT_DIFF },
|
||||
{ file: "src/integration-branch.ts", line: 71, primitive: "execSync", signature: "const stdout = execSync(\"git symbolic-ref --short refs/remotes/origin/HEAD\", {", reason: SHORT_GIT_PLUMBING },
|
||||
@@ -63,7 +63,7 @@ const allowlist: AllowlistEntry[] = [
|
||||
{ file: "src/merger.ts", line: 10211, primitive: "execSync", signature: "const squashIsEmpty = execSync(", reason: SHORT_GIT_PLUMBING },
|
||||
{ file: "src/merger.ts", line: 10245, primitive: "execSync", signature: "const squashIsEmpty = execSync(", reason: SHORT_GIT_PLUMBING },
|
||||
{ file: "src/merger.ts", line: 10432, primitive: "execSync", signature: "execSync(\"git reset --merge\", { cwd: rootDir, stdio: \"pipe\" });", reason: SHORT_GIT_PLUMBING },
|
||||
{ file: "src/executor.ts", line: 16296, primitive: "execSync", signature: "execSync(`git merge-base --is-ancestor ${task.baseCommitSha} HEAD`, {", reason: SHORT_GIT_PLUMBING },
|
||||
{ file: "src/executor.ts", line: 16585, primitive: "execSync", signature: "execSync(`git merge-base --is-ancestor ${task.baseCommitSha} HEAD`, {", reason: SHORT_GIT_PLUMBING },
|
||||
];
|
||||
|
||||
function scanSource(file: string, source: string): ShelloutSite[] {
|
||||
|
||||
@@ -311,36 +311,9 @@ async function captureToolsWithStore(
|
||||
if (settingsOverride) {
|
||||
store.getSettings.mockResolvedValue({ ...(await store.getSettings()), ...settingsOverride });
|
||||
}
|
||||
// Simulate the real TaskStore: forward transitions persist, but in-progress
|
||||
// regressions on done/skipped steps are rejected so executor.ts can surface
|
||||
// the "already <status>" diagnostic.
|
||||
const stepStates: Array<{ name: string; status: string }> = [
|
||||
{ name: "Preflight", status: "done" },
|
||||
{ name: "Implement", status: "in-progress" },
|
||||
{ name: "Testing", status: "pending" as const },
|
||||
{ name: "Docs", status: "pending" as const },
|
||||
];
|
||||
store.getTask.mockImplementation(async () => ({
|
||||
id: "FN-TEST",
|
||||
title: "Test",
|
||||
description: "Test",
|
||||
column: "in-progress",
|
||||
dependencies: [],
|
||||
steps: stepStates.map((s) => ({ ...s })),
|
||||
currentStep: 1,
|
||||
log: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
...taskOverride,
|
||||
}));
|
||||
store.updateStep.mockImplementation(async (_taskId: string, stepIndex: number, status: string) => {
|
||||
const current = stepStates[stepIndex];
|
||||
const isRegression = status === "in-progress" && (current.status === "done" || current.status === "skipped");
|
||||
if (!isRegression) {
|
||||
current.status = status;
|
||||
}
|
||||
return { steps: stepStates.map((s) => ({ ...s })) };
|
||||
});
|
||||
if (taskOverride && Object.keys(taskOverride).length > 0) {
|
||||
await store.updateTask("FN-001", taskOverride);
|
||||
}
|
||||
mockedExistsSync.mockReturnValue(true);
|
||||
|
||||
let capturedTools: any[] = [];
|
||||
@@ -359,9 +332,15 @@ async function captureToolsWithStore(
|
||||
} as any;
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:EngineTests 2026-07-26-20:55:
|
||||
Match the engine-pause harness shape that still reaches implementation sessions under
|
||||
graph ownership (empty steps + harness default getTaskDocument/PROMPT.md). Over-specifying
|
||||
frozen steps/worktree on execute has stranded this surface on plan-only sessions.
|
||||
*/
|
||||
const executor = new TaskExecutor(store, "/tmp/test");
|
||||
await executor.execute({
|
||||
id: "FN-TEST",
|
||||
id: "FN-001",
|
||||
title: "Test",
|
||||
description: "Test",
|
||||
column: "in-progress",
|
||||
@@ -375,7 +354,9 @@ async function captureToolsWithStore(
|
||||
|
||||
const tools: Record<string, any> = {};
|
||||
for (const t of capturedTools) {
|
||||
tools[t.name] = t.execute;
|
||||
if (t?.name && typeof t.execute === "function" && tools[t.name] === undefined) {
|
||||
tools[t.name] = t.execute;
|
||||
}
|
||||
}
|
||||
return { tools, store };
|
||||
}
|
||||
@@ -645,84 +626,22 @@ describe("fn_task_add_dep tool", () => {
|
||||
async function captureAddDepTools(opts?: { existingDeps?: string[]; targetExists?: boolean }) {
|
||||
const existingDeps = opts?.existingDeps ?? [];
|
||||
const targetExists = opts?.targetExists ?? true;
|
||||
|
||||
const store = createMockStore();
|
||||
store.getTask.mockImplementation(async (id: string) => {
|
||||
if (id === "FN-TEST") {
|
||||
return {
|
||||
id: "FN-TEST",
|
||||
title: "Test",
|
||||
description: "Test task",
|
||||
column: "in-progress",
|
||||
dependencies: existingDeps,
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
prompt: "# test\n## Steps\n### Step 0: Preflight\n- [ ] check",
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
if (id === "FN-OTHER" && targetExists) {
|
||||
return {
|
||||
id: "FN-OTHER",
|
||||
title: "Other task",
|
||||
description: "Another task",
|
||||
column: "todo",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
prompt: "",
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
throw new Error(`Task ${id} not found`);
|
||||
});
|
||||
|
||||
store.updateStep.mockResolvedValue({
|
||||
steps: [
|
||||
{ name: "Preflight", status: "done" },
|
||||
{ name: "Implement", status: "in-progress" },
|
||||
],
|
||||
});
|
||||
|
||||
mockedExistsSync.mockReturnValue(true);
|
||||
|
||||
let capturedTools: any[] = [];
|
||||
mockedCreateFnAgent.mockImplementation(async (opts: any) => {
|
||||
capturedTools = [...capturedTools, ...(opts.customTools || [])];
|
||||
return {
|
||||
session: {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn(),
|
||||
sessionManager: {
|
||||
getLeafId: vi.fn().mockReturnValue("leaf-id"),
|
||||
branchWithSummary: vi.fn(),
|
||||
},
|
||||
navigateTree: vi.fn().mockResolvedValue({ cancelled: false }),
|
||||
},
|
||||
} as any;
|
||||
});
|
||||
|
||||
const executor = new TaskExecutor(store, "/tmp/test");
|
||||
await executor.execute({
|
||||
id: "FN-TEST",
|
||||
title: "Test",
|
||||
description: "Test",
|
||||
column: "in-progress",
|
||||
dependencies: existingDeps,
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const tools: Record<string, any> = {};
|
||||
for (const t of capturedTools) {
|
||||
tools[t.name] = t.execute;
|
||||
const { tools, store } = await captureToolsWithStore(undefined, { dependencies: existingDeps });
|
||||
if (targetExists) {
|
||||
await store.updateTask("FN-OTHER", {
|
||||
title: "Other task",
|
||||
description: "Another task",
|
||||
column: "todo",
|
||||
dependencies: [],
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
});
|
||||
} else {
|
||||
const baseGetTask = store.getTask.bind(store);
|
||||
store.getTask.mockImplementation(async (id: string) => {
|
||||
if (id === "FN-OTHER") throw new Error(`Task ${id} not found`);
|
||||
return baseGetTask(id);
|
||||
});
|
||||
}
|
||||
return { tools, store };
|
||||
}
|
||||
@@ -739,20 +658,20 @@ describe("fn_task_add_dep tool", () => {
|
||||
|
||||
expect(result.content[0].text).toContain("Added dependency");
|
||||
expect(result.content[0].text).toContain("triage");
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-TEST", {
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-001", {
|
||||
dependencies: ["FN-OTHER"],
|
||||
});
|
||||
});
|
||||
|
||||
it("returns error for self-dependency", async () => {
|
||||
const { tools, store } = await captureAddDepTools();
|
||||
store.updateTask.mockClear();
|
||||
|
||||
const result = await tools.fn_task_add_dep("call1", { task_id: "FN-TEST" });
|
||||
const result = await tools.fn_task_add_dep("call1", { task_id: "FN-001" });
|
||||
|
||||
expect(result.content[0].text).toContain("Cannot add self-dependency");
|
||||
expect(result.content[0].text).toContain("FN-TEST cannot depend on itself");
|
||||
// store.updateTask should NOT have been called for dependency update
|
||||
// (it may be called for worktree path updates, so we check specifically for dependencies)
|
||||
expect(result.content[0].text).toContain("FN-001 cannot depend on itself");
|
||||
// After mockClear, only tool-driven dependency writes remain.
|
||||
const depUpdateCalls = store.updateTask.mock.calls.filter(
|
||||
(call: any[]) => call[1]?.dependencies !== undefined,
|
||||
);
|
||||
@@ -761,6 +680,7 @@ describe("fn_task_add_dep tool", () => {
|
||||
|
||||
it("returns error for non-existent target task", async () => {
|
||||
const { tools, store } = await captureAddDepTools({ targetExists: false });
|
||||
store.updateTask.mockClear();
|
||||
|
||||
const result = await tools.fn_task_add_dep("call1", { task_id: "FN-OTHER" });
|
||||
|
||||
@@ -774,6 +694,7 @@ describe("fn_task_add_dep tool", () => {
|
||||
|
||||
it("returns informational message for duplicate dependency without duplicating", async () => {
|
||||
const { tools, store } = await captureAddDepTools({ existingDeps: ["FN-OTHER"] });
|
||||
store.updateTask.mockClear();
|
||||
|
||||
const result = await tools.fn_task_add_dep("call1", { task_id: "FN-OTHER" });
|
||||
|
||||
@@ -790,7 +711,7 @@ describe("fn_task_add_dep tool", () => {
|
||||
|
||||
await tools.fn_task_add_dep("call1", { task_id: "FN-OTHER", confirm: true });
|
||||
|
||||
expect(store.logEntry).toHaveBeenCalledWith("FN-TEST", "Added dependency on FN-OTHER — stopping execution for re-planning");
|
||||
expect(store.logEntry).toHaveBeenCalledWith("FN-001", "Added dependency on FN-OTHER — stopping execution for re-planning");
|
||||
});
|
||||
|
||||
it("appends to existing dependencies without overwriting when confirm=true", async () => {
|
||||
@@ -799,7 +720,7 @@ describe("fn_task_add_dep tool", () => {
|
||||
const result = await tools.fn_task_add_dep("call1", { task_id: "FN-OTHER", confirm: true });
|
||||
|
||||
expect(result.content[0].text).toContain("Added dependency");
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-TEST", {
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-001", {
|
||||
dependencies: ["FN-001", "FN-OTHER"],
|
||||
});
|
||||
});
|
||||
@@ -813,12 +734,14 @@ describe("fn_task_add_dep tool", () => {
|
||||
|
||||
it("returns warning without confirm=true and does NOT add dependency", async () => {
|
||||
const { tools, store } = await captureAddDepTools();
|
||||
store.updateTask.mockClear();
|
||||
store.logEntry.mockClear();
|
||||
|
||||
const result = await tools.fn_task_add_dep("call1", { task_id: "FN-OTHER" });
|
||||
|
||||
expect(result.content[0].text).toContain("stop execution and discard current work");
|
||||
expect(result.content[0].text).toContain("confirm=true");
|
||||
// Should NOT have updated dependencies
|
||||
// Should NOT have updated dependencies after the tool call
|
||||
const depUpdateCalls = store.updateTask.mock.calls.filter(
|
||||
(call: any[]) => call[1]?.dependencies !== undefined,
|
||||
);
|
||||
@@ -833,7 +756,7 @@ describe("fn_task_add_dep tool", () => {
|
||||
it("validation errors (self-dep, not-found, dedup) return immediately without requiring confirm", async () => {
|
||||
// Self-dep — no confirm needed
|
||||
const { tools: tools1 } = await captureAddDepTools();
|
||||
const selfResult = await tools1.fn_task_add_dep("call1", { task_id: "FN-TEST" });
|
||||
const selfResult = await tools1.fn_task_add_dep("call1", { task_id: "FN-001" });
|
||||
expect(selfResult.content[0].text).toContain("Cannot add self-dependency");
|
||||
|
||||
// Not found — no confirm needed
|
||||
|
||||
@@ -31,8 +31,19 @@ describe("reliability interaction: starved refinement x triage poll", () => {
|
||||
await mkdir(join(root, ".fusion", "tasks"), { recursive: true });
|
||||
|
||||
try {
|
||||
/*
|
||||
FNXC:EngineTests 2026-07-26-20:55:
|
||||
Recovery requires updatedAt older than STARVED_REFINEMENT_ESCALATION_COOLDOWN_MS (40m).
|
||||
Keep createdAt/updatedAt well below the fake now (11:00) so grace + cooldown pass.
|
||||
*/
|
||||
const tasks: Task[] = [
|
||||
triageTask({ id: "FN-R1", sourceType: "task_refine" }),
|
||||
triageTask({
|
||||
id: "FN-R1",
|
||||
sourceType: "task_refine",
|
||||
priority: "low",
|
||||
createdAt: "2026-05-15T09:00:00.000Z",
|
||||
updatedAt: "2026-05-15T09:00:00.000Z",
|
||||
}),
|
||||
...Array.from({ length: 6 }, (_, idx) => triageTask({ id: `FN-B${idx + 1}`, createdAt: `2026-05-15T09:${String(10 + idx).padStart(2, "0")}:00.000Z`, priority: "normal" })),
|
||||
triageTask({ id: "FN-P1", column: "todo", sourceType: "dashboard_ui", updatedAt: "2026-05-15T10:15:00.000Z" }),
|
||||
triageTask({ id: "FN-P2", column: "todo", sourceType: "dashboard_ui", updatedAt: "2026-05-15T10:16:00.000Z" }),
|
||||
|
||||
@@ -38,7 +38,12 @@ const planStep = (name: string): TaskStep => ({ name, status: "pending" });
|
||||
const planningGuardCases: PlanningGuardCase[] = [
|
||||
{ label: "empty triage task", task: { column: "triage", steps: [] }, stillPlanning: true },
|
||||
{ label: "unplanned todo seed", task: { column: "todo", steps: [] }, stillPlanning: true },
|
||||
{ label: "todo task with a worktree", task: { column: "todo", worktree: "/tmp/FN-1", steps: [] }, stillPlanning: false },
|
||||
/*
|
||||
FNXC:NodeWorktreeIsolation 2026-07-26-20:50:
|
||||
Planning acquires the task worktree up front, so worktree alone is NOT advancement.
|
||||
Only execution timestamps / execution columns prove the card left planning.
|
||||
*/
|
||||
{ label: "todo task with a worktree", task: { column: "todo", worktree: "/tmp/FN-1", steps: [] }, stillPlanning: true },
|
||||
{
|
||||
label: "planned-and-queued todo task with materialized steps",
|
||||
task: { column: "todo", steps: [planStep("step-1")] },
|
||||
@@ -80,11 +85,11 @@ const planningGuardCases: PlanningGuardCase[] = [
|
||||
stillPlanning: true,
|
||||
},
|
||||
|
||||
// FN-7977's protections must survive: real advancement still outranks a planning status.
|
||||
// Worktree under a planning status is still planning; execution timestamps are the durable signal.
|
||||
{
|
||||
label: "triage card an executor already claimed a worktree for",
|
||||
label: "triage card that already has a planning worktree",
|
||||
task: { column: "triage", worktree: "/tmp/FN-1", steps: [planStep("step-1")], status: "needs-replan" },
|
||||
stillPlanning: false,
|
||||
stillPlanning: true,
|
||||
},
|
||||
{
|
||||
label: "card that reached execution while a planning recovery was in flight",
|
||||
|
||||
Reference in New Issue
Block a user