feat(FN-5444): add merge handoff test coverage for task worktree
Adds test coverage for merge queue and heartbeat handoff interactions (FN-5444), including source metadata expectations in heartbeat executor tests, merge handoff coverage gaps, and reuse scenarios in the merger worktree integration tests. Fusion-Task-Id: FN-5444 Fusion-Task-Lineage: 45e1b43f-8ae3-46ba-a5ee-25e8c661e753 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> Fusion-Task-Id: FN-5444
This commit is contained in:
@@ -294,6 +294,72 @@ describe("TaskStore merge queue", () => {
|
||||
expect(store.peekMergeQueue().some((entry) => entry.taskId === taskId)).toBe(false);
|
||||
});
|
||||
|
||||
it("FN-5444: emits metadata-rich enqueue-rejected and stale-lease-on-column-exit audits", async () => {
|
||||
const todoTask = await createTask();
|
||||
await store.moveTask(todoTask, "todo");
|
||||
expect(() => store.enqueueMergeQueue(todoTask)).toThrow(MergeQueueInvalidColumnError);
|
||||
|
||||
const rejected = store.getRunAuditEvents({ taskId: todoTask, mutationType: "mergeQueue:enqueue-rejected" });
|
||||
expect(rejected).toHaveLength(1);
|
||||
expect(rejected[0].metadata).toMatchObject({
|
||||
taskId: todoTask,
|
||||
column: "todo",
|
||||
reason: "not-in-review",
|
||||
});
|
||||
|
||||
const leasedTaskId = await createInReviewTask();
|
||||
const lease = store.acquireMergeQueueLease("worker-fn-5444", { leaseDurationMs: 60_000, now: "2099-05-19T00:00:10.000Z" });
|
||||
expect(lease?.taskId).toBe(leasedTaskId);
|
||||
await store.moveTask(leasedTaskId, "todo");
|
||||
|
||||
const staleLeaseAudit = store.getRunAuditEvents({ taskId: leasedTaskId, mutationType: "mergeQueue:stale-lease-on-column-exit" });
|
||||
expect(staleLeaseAudit).toHaveLength(1);
|
||||
expect(staleLeaseAudit[0].metadata).toMatchObject({
|
||||
taskId: leasedTaskId,
|
||||
previousColumn: "in-review",
|
||||
nextColumn: "todo",
|
||||
leasedBy: "worker-fn-5444",
|
||||
});
|
||||
expect(typeof staleLeaseAudit[0].metadata?.leaseExpiresAt).toBe("string");
|
||||
});
|
||||
|
||||
it("FN-5444: auto-cleanup-stale-row fires for targeted and untargeted acquisition", async () => {
|
||||
const staleTaskId = await createTask();
|
||||
await store.moveTask(staleTaskId, "todo");
|
||||
|
||||
const targetTaskId = await createInReviewTask();
|
||||
store.getDatabase().prepare("DELETE FROM mergeQueue WHERE taskId = ?").run(targetTaskId);
|
||||
store.enqueueMergeQueue(targetTaskId, { now: "2026-05-19T00:00:00.100Z" });
|
||||
store.getDatabase().prepare("INSERT INTO mergeQueue (taskId, enqueuedAt, priority, attemptCount) VALUES (?, ?, ?, 0)").run(
|
||||
staleTaskId,
|
||||
"2026-05-19T00:00:00.000Z",
|
||||
"normal",
|
||||
);
|
||||
|
||||
const targetedLease = store.acquireMergeQueueLease("worker-targeted", {
|
||||
targetTaskId,
|
||||
leaseDurationMs: 60_000,
|
||||
now: "2026-05-19T00:01:00.000Z",
|
||||
});
|
||||
expect(targetedLease?.taskId).toBe(targetTaskId);
|
||||
store.releaseMergeQueueLease(targetTaskId, "worker-targeted", { kind: "failure", error: "retry" });
|
||||
|
||||
const untargetedLease = store.acquireMergeQueueLease("worker-untargeted", {
|
||||
leaseDurationMs: 60_000,
|
||||
now: "2026-05-19T00:02:00.000Z",
|
||||
});
|
||||
expect(untargetedLease?.taskId).toBe(targetTaskId);
|
||||
|
||||
const cleanupEvents = store.getRunAuditEvents({ taskId: staleTaskId, mutationType: "mergeQueue:auto-cleanup-stale-row" });
|
||||
expect(cleanupEvents).toHaveLength(1);
|
||||
expect(cleanupEvents[0].metadata).toMatchObject({
|
||||
taskId: staleTaskId,
|
||||
column: "todo",
|
||||
reason: "not-in-review",
|
||||
});
|
||||
expect(store.peekMergeQueue().some((entry) => entry.taskId === staleTaskId)).toBe(false);
|
||||
});
|
||||
|
||||
it("auto-cleans polluted non-in-review rows before lease selection", async () => {
|
||||
const reviewTaskId = await createInReviewTask();
|
||||
const todoTaskId = await createTask();
|
||||
|
||||
@@ -3182,6 +3182,10 @@ describe("executeHeartbeat", () => {
|
||||
sourceType: "agent_heartbeat",
|
||||
sourceAgentId: "agent-001",
|
||||
sourceRunId: "run-001",
|
||||
sourceParentTaskId: "FN-001",
|
||||
sourceMetadata: expect.objectContaining({
|
||||
contentFingerprint: expect.any(String),
|
||||
}),
|
||||
}),
|
||||
}), expect.objectContaining({ settings: { autoSummarizeTitles: false } }));
|
||||
});
|
||||
|
||||
@@ -653,12 +653,12 @@ describe("acquireReuseHandoff", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("refuses when no merge queue lease can be acquired", async () => {
|
||||
it("FN-5444: no-lease refusal carries queue-head diagnostics and nulls when head is absent", async () => {
|
||||
const store = createStore();
|
||||
store.acquireMergeQueueLease.mockReturnValue(null);
|
||||
store.acquireMergeQueueLease.mockReturnValue({ taskId: "FN-5329" });
|
||||
store.peekMergeQueueHead.mockReturnValue({ taskId: "FN-5329", leasedBy: "merger-reuse-handoff", column: "todo" });
|
||||
|
||||
const refusal = await expectRefusal(
|
||||
const refusalWithHead = await expectRefusal(
|
||||
acquireReuseHandoff({
|
||||
task: await store.getTask("FN-5279"),
|
||||
store,
|
||||
@@ -667,11 +667,32 @@ describe("acquireReuseHandoff", () => {
|
||||
worktreePath: "/tmp/task-worktree",
|
||||
}),
|
||||
"lease-handoff-failed",
|
||||
"target-not-queued",
|
||||
"no-lease",
|
||||
);
|
||||
expect(refusal.payload).toMatchObject({
|
||||
expect(refusalWithHead.payload).toMatchObject({
|
||||
taskId: "FN-5279",
|
||||
worktreePath: "/tmp/task-worktree",
|
||||
acquiredTaskId: "FN-5329",
|
||||
queueHeadTaskId: "FN-5329",
|
||||
queueHeadLeasedBy: "merger-reuse-handoff",
|
||||
});
|
||||
|
||||
store.acquireMergeQueueLease.mockReturnValue({ taskId: "FN-5329" });
|
||||
store.peekMergeQueueHead.mockReturnValue(null);
|
||||
const refusalWithoutHead = await expectRefusal(
|
||||
acquireReuseHandoff({
|
||||
task: await store.getTask("FN-5279"),
|
||||
store,
|
||||
projectRoot: "/tmp/project-root",
|
||||
settings: {} as any,
|
||||
worktreePath: "/tmp/task-worktree",
|
||||
}),
|
||||
"lease-handoff-failed",
|
||||
"no-lease",
|
||||
);
|
||||
expect(refusalWithoutHead.payload).toMatchObject({
|
||||
queueHeadTaskId: null,
|
||||
queueHeadLeasedBy: null,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -424,6 +424,44 @@ describe("FN-5279 reliability interactions: merge reuse task worktree", () => {
|
||||
}
|
||||
}, 60_000);
|
||||
|
||||
it.skipIf(!hasGit)("FN-5444: moving task out of in-review during live lease preserves row until release cleanup", async () => {
|
||||
const { fixture, store, task } = await setupReuseHandoff({
|
||||
taskId: "FN-5444-RI-COLUMN-EXIT-LIVE-LEASE",
|
||||
fileName: "packages/engine/src/fn-5444-ri-column-exit.ts",
|
||||
fileContent: "export const exitLease = true;\n",
|
||||
commitMessage: "feat: add FN-5444 column exit lease coverage",
|
||||
skipWorktreeAdd: true,
|
||||
worktreeOverride: null,
|
||||
});
|
||||
|
||||
try {
|
||||
const lease = store.acquireMergeQueueLease("merger-reuse-handoff", {
|
||||
targetTaskId: task.id,
|
||||
leaseDurationMs: 60_000,
|
||||
now: "2099-05-19T00:00:10.000Z",
|
||||
});
|
||||
expect(lease?.taskId).toBe(task.id);
|
||||
|
||||
await store.moveTask(task.id, "todo");
|
||||
expect(store.peekMergeQueue().some((entry) => entry.taskId === task.id)).toBe(true);
|
||||
|
||||
const staleLeaseAudit = store.getRunAuditEvents({ taskId: task.id, mutationType: "mergeQueue:stale-lease-on-column-exit" });
|
||||
expect(staleLeaseAudit).toHaveLength(1);
|
||||
expect(staleLeaseAudit[0].metadata).toMatchObject({
|
||||
taskId: task.id,
|
||||
previousColumn: "in-review",
|
||||
nextColumn: "todo",
|
||||
leasedBy: "merger-reuse-handoff",
|
||||
});
|
||||
expect(typeof staleLeaseAudit[0].metadata?.leaseExpiresAt).toBe("string");
|
||||
|
||||
store.releaseMergeQueueLease(task.id, "merger-reuse-handoff", { kind: "success" });
|
||||
expect(store.peekMergeQueue().some((entry) => entry.taskId === task.id)).toBe(false);
|
||||
} finally {
|
||||
await fixture.cleanup();
|
||||
}
|
||||
}, 60_000);
|
||||
|
||||
it.skipIf(!hasGit)("already-landed branch auto-finalizes from the reused worktree path", async () => {
|
||||
const { fixture, rootDir, store, task, branch } = await setupReuseHandoff({
|
||||
taskId: "FN-5279-RI-ALREADY-LANDED",
|
||||
|
||||
@@ -899,7 +899,7 @@ export function packageNamesForFiles(rootDir: string, files: string[]): string[]
|
||||
*
|
||||
* @internal Exported for testing only.
|
||||
*/
|
||||
export function deriveScopedPnpmTestCommand(rootDir: string, baseBranch: string): string | null {
|
||||
export function deriveScopedPnpmTestCommand(rootDir: string, baseBranch: string, branch: string): string | null {
|
||||
// 1. Read and parse pnpm-workspace.yaml
|
||||
const workspacePath = join(rootDir, "pnpm-workspace.yaml");
|
||||
let workspaceContent: string;
|
||||
@@ -988,7 +988,7 @@ export function inferDefaultTestCommand(
|
||||
if (existsSync(join(rootDir, "pnpm-workspace.yaml"))) {
|
||||
if (baseBranch?.trim() && branch?.trim()) {
|
||||
try {
|
||||
const scoped = deriveScopedPnpmTestCommand(rootDir, baseBranch.trim());
|
||||
const scoped = deriveScopedPnpmTestCommand(rootDir, baseBranch.trim(), branch.trim());
|
||||
if (scoped) {
|
||||
mergerLog.log(
|
||||
`Scoped inferred test command to changed packages: ${scoped}`,
|
||||
|
||||
Reference in New Issue
Block a user