feat(FN-4538): complete Step 3 — preserve overlap block in self-healing

Fusion-Task-Id: FN-4538
Fusion-Task-Lineage: 332b4c78-b67f-45d0-9181-0bb539b662c1
This commit is contained in:
Fusion
2026-05-14 21:54:37 -07:00
committed by gsxdsm
parent a9eca52670
commit cdb25d0359
2 changed files with 50 additions and 19 deletions

View File

@@ -518,7 +518,7 @@ describe("SelfHealingManager", () => {
await manager.runStartupRecovery();
expect(store.updateTask).toHaveBeenCalledWith("A", { blockedBy: null, status: null });
expect(store.updateTask).toHaveBeenCalledWith("A", { blockedBy: null, overlapBlockedBy: null, status: null });
expect(store.logEntry).toHaveBeenCalledWith("A", expect.stringContaining("Auto-recovered: cleared stale blockedBy"));
});
@@ -546,7 +546,7 @@ describe("SelfHealingManager", () => {
await manager.runStartupRecovery();
expect(store.updateTask).not.toHaveBeenCalledWith("A", { blockedBy: null, status: null });
expect(store.updateTask).not.toHaveBeenCalledWith("A", { blockedBy: null, overlapBlockedBy: null, status: null });
});
});
@@ -5012,7 +5012,7 @@ describe("clearStaleBlockedBy", () => {
const recovered = await manager.clearStaleBlockedBy();
expect(recovered).toBe(1);
expect(store.updateTask).toHaveBeenCalledWith("A", { blockedBy: null, status: null });
expect(store.updateTask).toHaveBeenCalledWith("A", { blockedBy: null, overlapBlockedBy: null, status: null });
expect(store.logEntry).toHaveBeenCalledWith("A", expect.stringContaining("FN-MISSING"));
expect(store.logEntry).toHaveBeenCalledWith("A", expect.stringContaining("missing"));
manager.stop();
@@ -5029,7 +5029,7 @@ describe("clearStaleBlockedBy", () => {
const recovered = await manager.clearStaleBlockedBy();
expect(recovered).toBe(1);
expect(store.updateTask).toHaveBeenCalledWith("A", { blockedBy: null, status: null });
expect(store.updateTask).toHaveBeenCalledWith("A", { blockedBy: null, overlapBlockedBy: null, status: null });
expect(store.logEntry).toHaveBeenCalledWith("A", expect.stringContaining(blockerId));
expect(store.logEntry).toHaveBeenCalledWith("A", expect.stringContaining(column));
manager.stop();
@@ -5046,7 +5046,7 @@ describe("clearStaleBlockedBy", () => {
const recovered = await manager.clearStaleBlockedBy();
expect(recovered).toBe(1);
expect(store.updateTask).toHaveBeenCalledWith("A", { blockedBy: null, status: null });
expect(store.updateTask).toHaveBeenCalledWith("A", { blockedBy: null, overlapBlockedBy: null, status: null });
expect(store.logEntry).toHaveBeenCalledWith("A", expect.stringContaining(blockerId));
expect(store.logEntry).toHaveBeenCalledWith("A", expect.stringContaining("in-review + paused"));
manager.stop();
@@ -5063,7 +5063,7 @@ describe("clearStaleBlockedBy", () => {
const recovered = await manager.clearStaleBlockedBy();
expect(recovered).toBe(1);
expect(store.updateTask).toHaveBeenCalledWith("A", { blockedBy: null, status: null });
expect(store.updateTask).toHaveBeenCalledWith("A", { blockedBy: null, overlapBlockedBy: null, status: null });
expect(store.logEntry).toHaveBeenCalledWith("A", expect.stringContaining(blockerId));
expect(store.logEntry).toHaveBeenCalledWith("A", expect.stringContaining("mergeRetries 3/3"));
manager.stop();
@@ -5094,7 +5094,7 @@ describe("clearStaleBlockedBy", () => {
const recovered = await manager.clearStaleBlockedBy();
expect(recovered).toBe(1);
expect(store.updateTask).toHaveBeenCalledWith("A", { blockedBy: null, status: null });
expect(store.updateTask).toHaveBeenCalledWith("A", { blockedBy: null, overlapBlockedBy: null, status: null });
expect(store.logEntry).toHaveBeenCalledWith("A", expect.stringContaining("not among unresolved dependencies"));
manager.stop();
});
@@ -5130,7 +5130,7 @@ describe("clearStaleBlockedBy", () => {
const recovered = await manager.clearStaleBlockedBy();
expect(recovered).toBe(1);
expect(store.updateTask).toHaveBeenCalledWith("A", { blockedBy: null, status: null });
expect(store.updateTask).toHaveBeenCalledWith("A", { blockedBy: null, overlapBlockedBy: null, status: null });
expect(store.logEntry).toHaveBeenCalledWith("A", expect.stringContaining(`blocker ${blockerId}`));
expect(store.logEntry).toHaveBeenCalledWith("A", expect.stringContaining("stale for"));
manager.stop();
@@ -5178,7 +5178,7 @@ describe("clearStaleBlockedBy", () => {
const recovered = await manager.clearStaleBlockedBy();
expect(recovered).toBe(1);
expect(store.updateTask).toHaveBeenCalledWith("A", { blockedBy: null, status: null });
expect(store.updateTask).toHaveBeenCalledWith("A", { blockedBy: null, overlapBlockedBy: null, status: null });
manager.stop();
vi.useRealTimers();
});
@@ -5213,7 +5213,7 @@ describe("clearStaleBlockedBy", () => {
const recovered = await manager.clearStaleBlockedBy();
expect(recovered).toBe(1);
expect(store.updateTask).toHaveBeenCalledWith("FN-4013", { blockedBy: null, status: null });
expect(store.updateTask).toHaveBeenCalledWith("FN-4013", { blockedBy: null, overlapBlockedBy: null, status: null });
expect(store.logEntry).toHaveBeenCalledWith("FN-4013", expect.stringContaining("missing-worktree session start"));
manager.stop();
});
@@ -5345,7 +5345,7 @@ describe("clearStaleBlockedBy", () => {
const recovered = await manager.clearStaleBlockedBy();
expect(recovered).toBe(1);
expect(store.updateTask).toHaveBeenCalledWith("FN-3170", { blockedBy: null, status: null });
expect(store.updateTask).toHaveBeenCalledWith("FN-3170", { blockedBy: null, overlapBlockedBy: null, status: null });
expect(store.logEntry).toHaveBeenCalledWith(
"FN-3170",
"Auto-recovered: cleared stale queued status — all dependencies satisfied",

View File

@@ -2004,7 +2004,9 @@ export class SelfHealingManager {
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);
const dependents = [...todoTasks, ...inProgressTasks, ...inReviewTasks].filter((t) => t.blockedBy === taskId);
const dependents = [...todoTasks, ...inProgressTasks, ...inReviewTasks].filter(
(t) => t.blockedBy === taskId || t.overlapBlockedBy === taskId,
);
const todoTaskIds = new Set(todoTasks.map((t) => t.id));
for (const dependent of dependents) {
try {
@@ -2012,23 +2014,39 @@ export class SelfHealingManager {
const dep = taskById.get(depId);
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)),
);
if (todoTaskIds.has(dependent.id)) {
if (unresolvedDeps.length > 0) {
const nextBlocker = unresolvedDeps[0]!;
await this.store.updateTask(dependent.id, { blockedBy: nextBlocker, status: "queued" });
await this.store.updateTask(dependent.id, { blockedBy: nextBlocker, overlapBlockedBy, status: "queued" });
await this.store.logEntry(
dependent.id,
`Auto-recovered (FN-4523): cleared stale blockedBy — blocker ${taskId} is done; now blocked by ${nextBlocker}`,
);
} else if (hasActiveOverlapBlocker) {
await this.store.updateTask(dependent.id, { blockedBy: null, overlapBlockedBy, status: "queued" });
await this.store.logEntry(
dependent.id,
`Auto-recovered (FN-4523): preserved queued status — still blocked by file scope overlap with ${overlapBlockedBy}`,
);
} else {
await this.store.updateTask(dependent.id, { blockedBy: null, status: null });
await this.store.updateTask(dependent.id, { blockedBy: null, overlapBlockedBy: null, status: null });
await this.store.logEntry(
dependent.id,
`Auto-recovered (FN-4523): cleared stale blockedBy — blocker ${taskId} is done`,
);
}
} else {
await this.store.updateTask(dependent.id, { blockedBy: null });
await this.store.updateTask(dependent.id, {
blockedBy: null,
...(dependent.overlapBlockedBy === taskId ? { overlapBlockedBy: null } : {}),
});
await this.store.logEntry(
dependent.id,
`Auto-recovered (FN-4523): cleared stale blockedBy — blocker ${taskId} is done`,
@@ -2116,7 +2134,7 @@ export class SelfHealingManager {
...inReviewTasks.filter((task) => !task.paused),
].filter((task) => typeof task.blockedBy === "string" && task.blockedBy.trim().length > 0);
const queuedDependencyTasks = todoTasks.filter(
(task) => task.status === "queued" && task.dependencies.length > 0,
(task) => task.status === "queued" && (task.dependencies.length > 0 || Boolean(task.overlapBlockedBy)),
);
if (blockedTasks.length === 0 && queuedDependencyTasks.length === 0) return 0;
@@ -2139,6 +2157,11 @@ export class SelfHealingManager {
const dep = taskById.get(depId);
return dep && 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)),
);
if (blockedTaskIds.has(task.id)) {
if (!blockerId) continue;
@@ -2190,8 +2213,11 @@ export class SelfHealingManager {
const nextBlocker = unresolvedDeps[0]!;
await this.store.updateTask(task.id, { blockedBy: nextBlocker, status: "queued" });
await this.store.logEntry(task.id, `Auto-recovered: refreshed stale blockedBy — ${reason}; now blocked by ${nextBlocker}`);
} else if (hasActiveOverlapBlocker) {
await this.store.updateTask(task.id, { blockedBy: null, status: "queued" });
await this.store.logEntry(task.id, `Auto-recovered: preserved queued status — still blocked by file scope overlap with ${task.overlapBlockedBy}`);
} else {
await this.store.updateTask(task.id, { blockedBy: null, status: null });
await this.store.updateTask(task.id, { blockedBy: null, overlapBlockedBy: null, status: null });
await this.store.logEntry(task.id, `Auto-recovered: cleared stale blockedBy — ${reason}`);
}
} else {
@@ -2214,8 +2240,13 @@ export class SelfHealingManager {
if (unresolvedDeps.length === 0) {
if (queuedDependencyTaskIds.has(task.id)) {
try {
await this.store.updateTask(task.id, { blockedBy: null, status: null });
await this.store.logEntry(task.id, "Auto-recovered: cleared stale queued status — all dependencies satisfied");
if (hasActiveOverlapBlocker) {
await this.store.updateTask(task.id, { blockedBy: null, status: "queued" });
await this.store.logEntry(task.id, `Auto-recovered: preserved queued status — still blocked by file scope overlap with ${task.overlapBlockedBy}`);
} else {
await this.store.updateTask(task.id, { blockedBy: null, overlapBlockedBy: null, status: null });
await this.store.logEntry(task.id, "Auto-recovered: cleared stale queued status — all dependencies satisfied");
}
recovered++;
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);