feat(FN-4410): complete Step 3 — self-healing reclaim guards
Fusion-Task-Id: FN-4410 Fusion-Task-Lineage: 306d4fe0-d024-49dd-86b4-fb98d4ceff59
This commit is contained in:
@@ -5714,6 +5714,43 @@ describe("SelfHealingManager reclaimSelfOwnedBranchConflicts", () => {
|
|||||||
expect(inspectSpy).not.toHaveBeenCalled();
|
expect(inspectSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("skips tasks with recent active heartbeat runs", async () => {
|
||||||
|
const agentStore = {
|
||||||
|
listActiveHeartbeatRuns: vi.fn().mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: "run-1",
|
||||||
|
agentId: "agent-1",
|
||||||
|
startedAt: new Date().toISOString(),
|
||||||
|
endedAt: null,
|
||||||
|
status: "active",
|
||||||
|
contextSnapshot: { taskId: "FN-777" },
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
} as any;
|
||||||
|
manager = new SelfHealingManager(store, { rootDir: "/tmp/test-project", agentStore });
|
||||||
|
|
||||||
|
(store.listTasks as any)
|
||||||
|
.mockResolvedValueOnce([{ id: "FN-777", checkedOutBy: null, branch: "fusion/fn-777", worktree: "/tmp/fn-777" }])
|
||||||
|
.mockResolvedValueOnce([]);
|
||||||
|
|
||||||
|
const inspectSpy = vi.spyOn(branchConflictModule, "inspectBranchConflict");
|
||||||
|
const recovered = await manager.reclaimSelfOwnedBranchConflicts();
|
||||||
|
|
||||||
|
expect(recovered).toBe(0);
|
||||||
|
expect(inspectSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("only scans todo and in-progress columns", async () => {
|
||||||
|
(store.listTasks as any)
|
||||||
|
.mockResolvedValueOnce([])
|
||||||
|
.mockResolvedValueOnce([]);
|
||||||
|
|
||||||
|
await manager.reclaimSelfOwnedBranchConflicts();
|
||||||
|
|
||||||
|
expect(store.listTasks).toHaveBeenNthCalledWith(1, { column: "todo", slim: true });
|
||||||
|
expect(store.listTasks).toHaveBeenNthCalledWith(2, { column: "in-progress", slim: true });
|
||||||
|
});
|
||||||
|
|
||||||
it("escalates unrecoverable reclaim failures to in-review failed", async () => {
|
it("escalates unrecoverable reclaim failures to in-review failed", async () => {
|
||||||
(store.listTasks as any)
|
(store.listTasks as any)
|
||||||
.mockResolvedValueOnce([{ id: "FN-502", checkedOutBy: null, branch: "fusion/fn-502", worktree: "/tmp/fn-502" }])
|
.mockResolvedValueOnce([{ id: "FN-502", checkedOutBy: null, branch: "fusion/fn-502", worktree: "/tmp/fn-502" }])
|
||||||
|
|||||||
@@ -1313,9 +1313,29 @@ export class SelfHealingManager {
|
|||||||
...(await this.store.listTasks({ column: "in-progress", slim: true })),
|
...(await this.store.listTasks({ column: "in-progress", slim: true })),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const activeTaskIds = new Set<string>();
|
||||||
|
if (this.options.agentStore) {
|
||||||
|
try {
|
||||||
|
const activeRuns = await this.options.agentStore.listActiveHeartbeatRuns();
|
||||||
|
const activeWindowMs = RUNNING_ON_INACTIVE_TASK_STALE_RUN_MS;
|
||||||
|
const now = Date.now();
|
||||||
|
for (const run of activeRuns) {
|
||||||
|
const startedAtMs = Date.parse(run.startedAt ?? "");
|
||||||
|
if (!Number.isFinite(startedAtMs) || now - startedAtMs > activeWindowMs) continue;
|
||||||
|
const taskId = run.contextSnapshot && typeof run.contextSnapshot.taskId === "string"
|
||||||
|
? run.contextSnapshot.taskId.toUpperCase()
|
||||||
|
: null;
|
||||||
|
if (taskId) activeTaskIds.add(taskId);
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
log.warn(`Unable to enumerate active heartbeat runs for self-owned branch reclaim sweep: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let recovered = 0;
|
let recovered = 0;
|
||||||
for (const task of candidates) {
|
for (const task of candidates) {
|
||||||
if (task.checkedOutBy || !task.branch || !task.worktree) continue;
|
if (task.checkedOutBy || activeTaskIds.has(task.id.toUpperCase()) || !task.branch || !task.worktree) continue;
|
||||||
if (!await isUsableTaskWorktree(this.options.rootDir, task.worktree)) continue;
|
if (!await isUsableTaskWorktree(this.options.rootDir, task.worktree)) continue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user