feat(FN-3954): blocker staleness fanout and escalation for tasks
Merges FN-3954: adds a blocker fanout system where task staleness automatically escalates to upstream blocking tasks, with a new `blocker-fanout` core module, dashboard hooks wiring that surfaces escalation status on the board and executor status bar, and a new settings toggle to disable escalation. Fusion-Task-Id: FN-3954
This commit is contained in:
@@ -28,6 +28,7 @@ describe("useAppSettings", () => {
|
||||
enginePaused: false,
|
||||
prAuthAvailable: true,
|
||||
taskStuckTimeoutMs: 600000,
|
||||
staleHighFanoutBlockerAgeThresholdMs: 7200000,
|
||||
showQuickChatFAB: false,
|
||||
} as never);
|
||||
|
||||
@@ -46,6 +47,7 @@ describe("useAppSettings", () => {
|
||||
expect(result.current.prAuthAvailable).toBe(true);
|
||||
expect(result.current.settingsLoaded).toBe(true);
|
||||
expect(result.current.taskStuckTimeoutMs).toBe(600000);
|
||||
expect(result.current.staleHighFanoutBlockerAgeThresholdMs).toBe(7200000);
|
||||
expect(result.current.showQuickChatFAB).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ describe("computeBlockerFanoutMap", () => {
|
||||
dependentIds: ["FN-2"],
|
||||
staleBlockedByDependentIds: [],
|
||||
isHighFanout: false,
|
||||
escalation: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -58,6 +59,7 @@ describe("computeBlockerFanoutMap", () => {
|
||||
dependentIds: ["FN-2", "FN-3"],
|
||||
staleBlockedByDependentIds: [],
|
||||
isHighFanout: false,
|
||||
escalation: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -75,6 +77,7 @@ describe("computeBlockerFanoutMap", () => {
|
||||
dependentIds: ["FN-2", "FN-3", "FN-4"],
|
||||
staleBlockedByDependentIds: [],
|
||||
isHighFanout: false,
|
||||
escalation: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -90,6 +93,7 @@ describe("computeBlockerFanoutMap", () => {
|
||||
dependentIds: ["FN-2", "FN-3"],
|
||||
staleBlockedByDependentIds: ["FN-3"],
|
||||
isHighFanout: false,
|
||||
escalation: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -131,6 +135,7 @@ describe("computeBlockerFanoutMap", () => {
|
||||
dependentIds: ["D1", "D2", "D3", "D4"],
|
||||
staleBlockedByDependentIds: [],
|
||||
isHighFanout: false,
|
||||
escalation: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -151,6 +156,7 @@ describe("computeBlockerFanoutMap", () => {
|
||||
dependentIds: ["D1", "D2", "D3", "D4", "D5", "DONE"],
|
||||
staleBlockedByDependentIds: [],
|
||||
isHighFanout: true,
|
||||
escalation: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -170,9 +176,48 @@ describe("computeBlockerFanoutMap", () => {
|
||||
dependentIds: ["D1", "D2", "D3", "D4", "ARCH"],
|
||||
staleBlockedByDependentIds: [],
|
||||
isHighFanout: false,
|
||||
escalation: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("escalates aged high fan-out blockers only when old enough", () => {
|
||||
const tasks = [
|
||||
createTask("B", "in-progress", { columnMovedAt: "2026-01-01T00:00:00.000Z" }),
|
||||
createTask("D1", "todo", { dependencies: ["B"] }),
|
||||
createTask("D2", "todo", { dependencies: ["B"] }),
|
||||
createTask("D3", "todo", { dependencies: ["B"] }),
|
||||
createTask("D4", "todo", { dependencies: ["B"] }),
|
||||
createTask("D5", "todo", { dependencies: ["B"] }),
|
||||
];
|
||||
|
||||
const entry = computeBlockerFanoutMap(tasks, {
|
||||
staleHighFanoutAgeThresholdMs: 60 * 60 * 1000,
|
||||
}).get("B");
|
||||
|
||||
expect(entry?.isHighFanout).toBe(true);
|
||||
expect(entry?.escalation?.blockerId).toBe("B");
|
||||
expect(entry?.escalation?.activeTodoCount).toBe(5);
|
||||
expect((entry?.escalation?.blockingAgeMs ?? 0) / (60 * 60 * 1000)).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("keeps short-lived high fan-out blockers quiet", () => {
|
||||
const tasks = [
|
||||
createTask("B", "in-progress", { columnMovedAt: new Date().toISOString() }),
|
||||
createTask("D1", "todo", { dependencies: ["B"] }),
|
||||
createTask("D2", "todo", { dependencies: ["B"] }),
|
||||
createTask("D3", "todo", { dependencies: ["B"] }),
|
||||
createTask("D4", "todo", { dependencies: ["B"] }),
|
||||
createTask("D5", "todo", { dependencies: ["B"] }),
|
||||
];
|
||||
|
||||
const entry = computeBlockerFanoutMap(tasks, {
|
||||
staleHighFanoutAgeThresholdMs: 60 * 60 * 1000,
|
||||
}).get("B");
|
||||
|
||||
expect(entry?.isHighFanout).toBe(true);
|
||||
expect(entry?.escalation).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps MAX_AUTO_MERGE_RETRIES aligned with engine self-healing source", () => {
|
||||
const testDir = dirname(fileURLToPath(import.meta.url));
|
||||
const source = readFileSync(resolve(testDir, "../../../../engine/src/self-healing.ts"), "utf8");
|
||||
|
||||
Reference in New Issue
Block a user