feat(FN-783): complete Step 4 — reconcile stale overlap blockers safely

Fusion-Task-Id: FN-783

Co-authored-by: Fusion <noreply@runfusion.ai>
This commit is contained in:
Phil Larson
2026-06-25 03:15:43 -07:00
parent 53825ad0ab
commit e2a84d4dd6
3 changed files with 170 additions and 16 deletions

View File

@@ -360,4 +360,99 @@ describe("scheduler overlap starvation regression (FN-057)", () => {
);
});
it("does not preserve FN-779/FN-756 poisoned cross-repository blocker after write-scope sanitization", async () => {
const tasks = [
makeTask({ id: "FN-756", column: "in-progress", priority: "normal", title: "iPad mobile XCUITest work" }),
makeTask({
id: "FN-779",
column: "todo",
status: "queued",
priority: "high",
overlapBlockedBy: "FN-756",
title: "Fusion engine heartbeat suppression",
}),
];
const store = createStore(tasks, {
"FN-756": ["project.yml", "AtlasNotes.xcodeproj/**", "Tests/AtlasNotesMobileUITests/**", "Packages/MobileApp/**"],
"FN-779": ["packages/core/**", "packages/engine/**", "packages/dashboard/**", "packages/cli/**"],
});
const scheduler = new Scheduler(store);
(scheduler as any).running = true;
await scheduler.schedule();
expect(store.updateTask).toHaveBeenCalledWith("FN-779", { overlapBlockedBy: null });
expect(store.moveTask).toHaveBeenCalledWith("FN-779", "in-progress", expect.anything());
expect(store.updateTask).not.toHaveBeenCalledWith(
"FN-779",
expect.objectContaining({ status: "queued", overlapBlockedBy: "FN-756" }),
);
});
it("keeps true Atlas hot-file-family overlaps blocked", async () => {
const tasks = [
makeTask({ id: "FN-756", column: "in-progress", priority: "normal" }),
makeTask({ id: "FN-800", column: "todo", priority: "high" }),
];
const store = createStore(tasks, {
"FN-756": ["project.yml", "AtlasNotes.xcodeproj/**", "Tests/AtlasNotesMobileUITests/**", "Packages/MobileApp/**"],
"FN-800": ["Tests/AtlasNotesMobileUITests/**", "Packages/MobileApp/**"],
});
const scheduler = new Scheduler(store);
(scheduler as any).running = true;
await scheduler.schedule();
expect(store.updateTask).toHaveBeenCalledWith("FN-800", {
status: "queued",
blockedBy: null,
overlapBlockedBy: "FN-756",
});
expect(store.moveTask).not.toHaveBeenCalledWith("FN-800", "in-progress", expect.anything());
});
it("reroutes a stale overlap blocker to another current active lease", async () => {
const tasks = [
makeTask({ id: "FN-OLD", column: "done", priority: "normal" }),
makeTask({ id: "FN-NEW", column: "in-progress", priority: "normal" }),
makeTask({ id: "FN-900", column: "todo", status: "queued", priority: "high", overlapBlockedBy: "FN-OLD" }),
];
const store = createStore(tasks, {
"FN-OLD": ["packages/core/src/store.ts"],
"FN-NEW": ["packages/engine/src/scheduler.ts"],
"FN-900": ["packages/engine/src/scheduler.ts"],
});
const scheduler = new Scheduler(store);
(scheduler as any).running = true;
await scheduler.schedule();
expect(store.updateTask).toHaveBeenCalledWith("FN-900", {
status: "queued",
blockedBy: null,
overlapBlockedBy: "FN-NEW",
});
expect(store.logEntry).toHaveBeenCalledWith(
"FN-900",
"queued — blocked by active file-scope lease FN-NEW (column=in-progress)",
);
expect(store.moveTask).not.toHaveBeenCalledWith("FN-900", "in-progress", expect.anything());
});
it("clears an absent overlap blocker only after confirming no current overlap remains", async () => {
const tasks = [
makeTask({ id: "FN-901", column: "todo", status: "queued", priority: "normal", overlapBlockedBy: "FN-MISSING" }),
];
const store = createStore(tasks, {
"FN-901": ["packages/engine/src/scheduler.ts"],
});
const scheduler = new Scheduler(store);
(scheduler as any).running = true;
await scheduler.schedule();
expect(store.updateTask).toHaveBeenCalledWith("FN-901", { overlapBlockedBy: null });
expect(store.moveTask).toHaveBeenCalledWith("FN-901", "in-progress", expect.anything());
});
});

View File

@@ -7184,7 +7184,7 @@ describe("FN-4538 overlapBlockedBy self-healing", () => {
return { id, column: "todo", paused: false, blockedBy: null, dependencies: [], mergeRetries: 0, ...overrides };
}
function makeStore(tasks: Record<string, unknown>[]) {
function makeStore(tasks: Record<string, unknown>[], scopes: Record<string, string[]> = {}) {
const store = createMockStore({
getSettings: vi.fn().mockResolvedValue({
autoUnpauseEnabled: false,
@@ -7192,6 +7192,7 @@ describe("FN-4538 overlapBlockedBy self-healing", () => {
globalPause: false,
enginePaused: false,
} as unknown as Settings),
parseFileScopeFromPrompt: vi.fn(async (taskId: string) => scopes[taskId] ?? ["packages/engine/src/scheduler.ts"]),
});
(store.listTasks as ReturnType<typeof vi.fn>).mockImplementation(async (options?: { column?: string }) => {
if (options?.column === "todo") return tasks.filter((task) => task.column === "todo");
@@ -7391,6 +7392,31 @@ describe("FN-4538 overlapBlockedBy self-healing", () => {
expect(store.updateTask).not.toHaveBeenCalledWith("FN-TARGET", expect.objectContaining({ status: null }));
manager.stop();
});
it("FN-783: clearStaleBlockedBy clears queued status when overlap blocker no longer shares effective write scope", async () => {
const overlapBlocker = makeTask("FN-ACTIVE", { column: "in-progress" });
const target = makeTask("FN-TARGET", {
column: "todo",
status: "queued",
blockedBy: undefined,
overlapBlockedBy: "FN-ACTIVE",
dependencies: [],
});
const store = makeStore([target, overlapBlocker], {
"FN-ACTIVE": ["project.yml", "Tests/AtlasNotesMobileUITests/**"],
"FN-TARGET": ["packages/core/**", "packages/engine/**"],
});
const manager = new SelfHealingManager(store, { rootDir: "/tmp/test-project" });
await manager.clearStaleBlockedBy();
expect(store.updateTask).toHaveBeenCalledWith("FN-TARGET", { blockedBy: null, overlapBlockedBy: null, status: null });
expect(store.logEntry).not.toHaveBeenCalledWith(
"FN-TARGET",
"Auto-recovered: preserved queued status — still blocked by file scope overlap with FN-ACTIVE",
);
manager.stop();
});
});
describe("stale triage processing eviction before recovery", () => {

View File

@@ -3651,6 +3651,27 @@ export class SelfHealingManager {
await this.reconcileTaskWorktreeMetadata({ includeTaskIds: new Set([taskId]) });
const allTasks = await this.store.listTasks({ slim: true, includeArchived: true });
const taskById = new Map(allTasks.map((t) => [t.id, t]));
const overlapIgnorePaths = settings.overlapIgnorePaths ?? [];
const filteredScopeByTaskId = new Map<string, string[]>();
const getFilteredFileScope = async (scopeTaskId: string): Promise<string[]> => {
const cached = filteredScopeByTaskId.get(scopeTaskId);
if (cached) return cached;
const scope = await this.store.parseFileScopeFromPrompt(scopeTaskId);
const filteredScope = filterPathsByIgnoreList(scope, overlapIgnorePaths);
filteredScopeByTaskId.set(scopeTaskId, filteredScope);
return filteredScope;
};
const hasActiveFileScopeOverlapBlocker = async (dependent: Task, blockerId: string | null | undefined): Promise<boolean> => {
if (!blockerId) return false;
const blocker = taskById.get(blockerId);
if (!blocker || blocker.paused || blocker.userPaused) return false;
if (blocker.column !== "in-progress" && !(blocker.column === "in-review" && !blocker.paused)) return false;
const dependentScope = await getFilteredFileScope(dependent.id);
if (dependentScope.length === 0 || isCoordinationOnlyTask(dependent, dependentScope)) return false;
const blockerScope = await getFilteredFileScope(blocker.id);
if (blockerScope.length === 0 || isCoordinationOnlyTask(blocker, blockerScope)) return false;
return pathsOverlap(dependentScope, blockerScope);
};
const todoTasks = await this.store.listTasks({ column: "todo", slim: true });
const inProgressTasks = await this.store.listTasks({ column: "in-progress", slim: true });
const inReviewTasks = (await this.store.listTasks({ column: "in-review", slim: true })).filter((t) => !t.paused);
@@ -3666,11 +3687,7 @@ export class SelfHealingManager {
return dep && dep.column !== "done" && dep.column !== "in-review" && dep.column !== "archived";
});
const overlapBlockedBy = dependent.overlapBlockedBy === taskId ? null : (dependent.overlapBlockedBy ?? null);
const overlapBlockerTask = overlapBlockedBy ? taskById.get(overlapBlockedBy) : undefined;
const hasActiveOverlapBlocker = Boolean(
overlapBlockerTask
&& (overlapBlockerTask.column === "in-progress" || (overlapBlockerTask.column === "in-review" && !overlapBlockerTask.paused)),
);
const hasActiveOverlapBlocker = await hasActiveFileScopeOverlapBlocker(dependent, overlapBlockedBy);
if (todoTaskIds.has(dependent.id)) {
if (unresolvedDeps.length > 0) {
@@ -4748,6 +4765,28 @@ export class SelfHealingManager {
const allTasks = await this.store.listTasks({ includeArchived: true });
const taskById = new Map(allTasks.map((task) => [task.id, task]));
const overlapIgnorePaths = settings.overlapIgnorePaths ?? [];
const filteredScopeByTaskId = new Map<string, string[]>();
const getFilteredFileScope = async (taskId: string): Promise<string[]> => {
const cached = filteredScopeByTaskId.get(taskId);
if (cached) return cached;
const scope = await this.store.parseFileScopeFromPrompt(taskId);
const filteredScope = filterPathsByIgnoreList(scope, overlapIgnorePaths);
filteredScopeByTaskId.set(taskId, filteredScope);
return filteredScope;
};
const hasActiveFileScopeOverlapBlocker = async (task: Task, blockerId: string | null | undefined): Promise<boolean> => {
if (!blockerId) return false;
const blocker = taskById.get(blockerId);
if (!blocker || blocker.paused || blocker.userPaused) return false;
if (blocker.column !== "in-progress" && !(blocker.column === "in-review" && !blocker.paused)) return false;
const taskScope = await getFilteredFileScope(task.id);
if (taskScope.length === 0 || isCoordinationOnlyTask(task, taskScope)) return false;
const blockerScope = await getFilteredFileScope(blocker.id);
if (blockerScope.length === 0 || isCoordinationOnlyTask(blocker, blockerScope)) return false;
return pathsOverlap(taskScope, blockerScope);
};
let recovered = 0;
const todoTaskIds = new Set(todoTasks.map((task) => task.id));
@@ -4759,11 +4798,9 @@ export class SelfHealingManager {
for (const [taskId, lastLoggedBlockerId] of this.preservedQueuedOverlapLogged) {
const memoTask = taskById.get(taskId);
const memoOverlapBlocker = memoTask?.overlapBlockedBy ? taskById.get(memoTask.overlapBlockedBy) : undefined;
const memoHasActiveOverlapBlocker = Boolean(
memoOverlapBlocker
&& (memoOverlapBlocker.column === "in-progress" || (memoOverlapBlocker.column === "in-review" && !memoOverlapBlocker.paused)),
);
const memoHasActiveOverlapBlocker = memoTask
? await hasActiveFileScopeOverlapBlocker(memoTask, memoTask.overlapBlockedBy)
: false;
if (
!candidates.has(taskId)
|| memoTask?.column !== "todo"
@@ -4784,11 +4821,7 @@ export class SelfHealingManager {
// treated as resolved here by design.
return dep && !dep.deletedAt && dep.column !== "done" && dep.column !== "in-review" && dep.column !== "archived";
});
const overlapBlocker = task.overlapBlockedBy ? taskById.get(task.overlapBlockedBy) : undefined;
const hasActiveOverlapBlocker = Boolean(
overlapBlocker
&& (overlapBlocker.column === "in-progress" || (overlapBlocker.column === "in-review" && !overlapBlocker.paused)),
);
const hasActiveOverlapBlocker = await hasActiveFileScopeOverlapBlocker(task, task.overlapBlockedBy);
if (blockedTaskIds.has(task.id)) {
if (!blockerId) continue;