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:
gsxdsm
2026-07-25 21:01:21 -07:00
parent 9f6aaa933d
commit c8427f1c1e
4 changed files with 69 additions and 130 deletions

View File

@@ -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[] {

View File

@@ -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

View File

@@ -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" }),

View File

@@ -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",