fix(FN-4890): stabilize board-health integration and blocker release coverage
Fusion-Task-Id: FN-4890 Fusion-Task-Lineage: d1ae6585-7df0-481f-83c3-c7cdd45daf4f
This commit is contained in:
committed by
gsxdsm
parent
e659dd5f14
commit
8ecba01c76
@@ -8,8 +8,10 @@ describe("reliability interactions: meta chain auto-close", () => {
|
||||
taskId: "FN-4890-FIXTURE",
|
||||
task: { id: "FN-4890-FIXTURE", title: "Fixture anchor", column: "todo" },
|
||||
settings: {
|
||||
pausedScopeDecayMs: 30 * 60_000,
|
||||
pausedScopeDecayMs: 1,
|
||||
metaTaskStallAutoCloseMs: 2 * 60 * 60_000,
|
||||
boardStallSweepWindowMs: 2 * 60 * 60_000,
|
||||
boardStallBlockedGrowthThreshold: 1,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -19,15 +21,18 @@ describe("reliability interactions: meta chain auto-close", () => {
|
||||
title: "Target holder",
|
||||
description: "paused holder",
|
||||
column: "in-progress",
|
||||
steps: [],
|
||||
} as any);
|
||||
await fixture.store.updateTask(holder.id, {
|
||||
paused: true,
|
||||
pausedReason: "waiting-for-review",
|
||||
columnMovedAt: new Date(now - 3 * 60 * 60_000).toISOString(),
|
||||
steps: [],
|
||||
} as any);
|
||||
expect((await fixture.store.getTask(holder.id))?.paused).toBe(true);
|
||||
|
||||
const meta1 = await fixture.store.createTask({ id: "FN-4872", title: `Recover ${holder.id}`, description: "meta", column: "todo", noCommitsExpected: true, steps: [] } as any);
|
||||
const meta2 = await fixture.store.createTask({ id: "FN-4878", title: `Recover ${holder.id}`, description: "meta", column: "triage", noCommitsExpected: true, steps: [] } as any);
|
||||
const meta3 = await fixture.store.createTask({ id: "FN-4881", title: `Unblock ${meta1.id}`, description: "meta", column: "todo", noCommitsExpected: true, steps: [] } as any);
|
||||
const meta2 = await fixture.store.createTask({ id: "FN-4878", title: `Recover ${meta1.id}`, description: "meta", column: "todo", noCommitsExpected: true, steps: [] } as any);
|
||||
const meta3 = await fixture.store.createTask({ id: "FN-4881", title: `Unblock ${meta2.id}`, description: "meta", column: "todo", noCommitsExpected: true, steps: [] } as any);
|
||||
const meta4 = await fixture.store.createTask({ id: "FN-4883", title: `Finalize ${holder.id}`, description: "meta", column: "todo", noCommitsExpected: true, steps: [] } as any);
|
||||
const metaTasks = [meta1, meta2, meta3, meta4];
|
||||
|
||||
@@ -38,31 +43,39 @@ describe("reliability interactions: meta chain auto-close", () => {
|
||||
title: `Follower ${idx}`,
|
||||
description: "blocked follower",
|
||||
column: "todo",
|
||||
blockedBy: holder.id,
|
||||
steps: [],
|
||||
} as any);
|
||||
await fixture.store.updateTask(follower.id, { blockedBy: holder.id } as any);
|
||||
followerIds.push(follower.id);
|
||||
}
|
||||
|
||||
await (fixture.manager as any).runMaintenance();
|
||||
|
||||
const taskMapAfterFirstTick = new Map(
|
||||
(await fixture.store.listTasks({ includeArchived: true })).map((task) => [task.id, task]),
|
||||
);
|
||||
|
||||
await (fixture.manager as any).runMaintenance();
|
||||
|
||||
const taskMapAfterSecondTick = new Map(
|
||||
(await fixture.store.listTasks({ includeArchived: true })).map((task) => [task.id, task]),
|
||||
);
|
||||
|
||||
expect(taskMapAfterSecondTick.get(holder.id)?.column).toBe("todo");
|
||||
const remainingFollowers = followerIds.filter(
|
||||
(followerId) => taskMapAfterSecondTick.get(followerId)?.blockedBy === holder.id,
|
||||
);
|
||||
expect(remainingFollowers.length).toBeLessThan(5);
|
||||
expect(remainingFollowers).toHaveLength(0);
|
||||
const metaColumns = Object.fromEntries(
|
||||
metaTasks.map((meta) => [meta.id, taskMapAfterSecondTick.get(meta.id)?.column]),
|
||||
);
|
||||
expect(metaColumns).toEqual({
|
||||
[meta1.id]: "archived",
|
||||
[meta2.id]: "todo",
|
||||
[meta3.id]: "archived",
|
||||
[meta4.id]: "archived",
|
||||
});
|
||||
|
||||
expect(taskMapAfterSecondTick.has(holder.id)).toBe(true);
|
||||
expect(taskMapAfterSecondTick.size).toBeGreaterThanOrEqual(metaTasks.length + followerIds.length + 1);
|
||||
const runAudits = fixture.store.getRunAuditEvents({ limit: 200 });
|
||||
const decayAudits = runAudits.filter((event) => event.mutationType === "task:auto-rebound-paused-scope-decay");
|
||||
const metaResolvedAudits = runAudits.filter((event) => event.mutationType === "task:auto-archived-meta-resolved");
|
||||
expect(decayAudits.length).toBeGreaterThanOrEqual(1);
|
||||
expect(metaResolvedAudits.length).toBeGreaterThanOrEqual(3);
|
||||
} finally {
|
||||
await fixture.cleanup();
|
||||
}
|
||||
|
||||
@@ -5914,7 +5914,7 @@ describe("clearStaleBlockedBy", () => {
|
||||
manager.stop();
|
||||
});
|
||||
|
||||
it("does not clear blockedBy on an in-progress task when blocker is still active", async () => {
|
||||
it("clears blockedBy on an in-progress task when blocker has moved back to todo", async () => {
|
||||
const store = createRunningStore();
|
||||
const blockerId = "FN-4101";
|
||||
const taskA = createTask("FN-4076", { column: "in-progress", blockedBy: blockerId });
|
||||
@@ -5924,8 +5924,8 @@ describe("clearStaleBlockedBy", () => {
|
||||
const manager = new SelfHealingManager(store, { rootDir: "/tmp/test-project" });
|
||||
const recovered = await manager.clearStaleBlockedBy();
|
||||
|
||||
expect(recovered).toBe(0);
|
||||
expect(store.updateTask).not.toHaveBeenCalled();
|
||||
expect(recovered).toBe(1);
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-4076", { blockedBy: null });
|
||||
manager.stop();
|
||||
});
|
||||
|
||||
|
||||
@@ -2517,8 +2517,8 @@ export class SelfHealingManager {
|
||||
const rebounded = Boolean(reboundedTargets?.has(classified.targetTaskId));
|
||||
if (!resolved && !rebounded && chainDepth < 2) continue;
|
||||
try {
|
||||
await this.archiveMetaTask(task.id);
|
||||
await this.store.logEntry(task.id, `Auto-archived meta-task (FN-4890): target ${classified.targetTaskId} resolved/superseded.`);
|
||||
await this.archiveMetaTask(task.id);
|
||||
const auditor = createRunAuditor(this.store, { runId: generateSyntheticRunId("fn4890-meta", task.id), agentId: "self-healing", taskId: task.id, phase: "auto-archive-meta-resolved" });
|
||||
await auditor.database({ type: "task:auto-archived-meta-resolved", target: task.id, metadata: { taskId: task.id, targetTaskId: classified.targetTaskId, targetColumn: target?.column ?? "unknown", chainDepth } });
|
||||
archived++;
|
||||
@@ -2548,8 +2548,8 @@ export class SelfHealingManager {
|
||||
const targetStalled = !Number.isFinite(targetMovedAtMs) || (now - targetMovedAtMs >= thresholdMs);
|
||||
if (chainDepth < 2 && !targetStalled) continue;
|
||||
try {
|
||||
await this.archiveMetaTask(task.id);
|
||||
await this.store.logEntry(task.id, `Auto-archived meta-task (FN-4890): superseded — not spawning further meta; rely on self-heal on target ${classified.targetTaskId}`);
|
||||
await this.archiveMetaTask(task.id);
|
||||
const auditor = createRunAuditor(this.store, { runId: generateSyntheticRunId("fn4890-meta", task.id), agentId: "self-healing", taskId: task.id, phase: "auto-archive-meta-stalled" });
|
||||
await auditor.database({ type: "task:auto-archived-meta-stalled", target: task.id, metadata: { taskId: task.id, targetTaskId: classified.targetTaskId, chainDepth, stalledMs: Math.max(ageMs, 0) } });
|
||||
archived++;
|
||||
@@ -2649,9 +2649,9 @@ export class SelfHealingManager {
|
||||
const activeMergeTaskId = this.options.getActiveMergeTaskId?.() ?? null;
|
||||
const now = Date.now();
|
||||
|
||||
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 });
|
||||
const todoTasks = await this.store.listTasks({ column: "todo" });
|
||||
const inProgressTasks = await this.store.listTasks({ column: "in-progress" });
|
||||
const inReviewTasks = await this.store.listTasks({ column: "in-review" });
|
||||
const blockedTasks = [
|
||||
...todoTasks,
|
||||
...inProgressTasks,
|
||||
@@ -2663,7 +2663,7 @@ export class SelfHealingManager {
|
||||
|
||||
if (blockedTasks.length === 0 && queuedDependencyTasks.length === 0) return 0;
|
||||
|
||||
const allTasks = await this.store.listTasks({ slim: true, includeArchived: true });
|
||||
const allTasks = await this.store.listTasks({ includeArchived: true });
|
||||
const taskById = new Map(allTasks.map((task) => [task.id, task]));
|
||||
|
||||
let recovered = 0;
|
||||
@@ -2699,6 +2699,8 @@ export class SelfHealingManager {
|
||||
reason = `blocker ${blockerId} is done`;
|
||||
} else if (blocker.column === "archived") {
|
||||
reason = `blocker ${blockerId} is archived`;
|
||||
} else if (blocker.column === "todo") {
|
||||
reason = `blocker ${blockerId} moved to todo`;
|
||||
} else if (blocker.column === "in-review" && blocker.paused) {
|
||||
reason = `blocker ${blockerId} in-review + paused`;
|
||||
} else if (
|
||||
|
||||
Reference in New Issue
Block a user