feat(FN-4132): sync task ownership on agent reassignment

Adds a task reassignment sync helper to the core store, wires it into the system, and ships regression tests; includes a patch changeset for `@runfusion/fusion`.

Fusion-Task-Id: FN-4132

Fusion-Task-Lineage: 80ec3c62-7c09-49dd-9e49-74b203e48bf4
This commit is contained in:
Fusion
2026-05-12 10:04:56 -07:00
committed by gsxdsm
parent b387df8f75
commit 7e089e195a
6 changed files with 264 additions and 11 deletions

View File

@@ -1,5 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { execSync } from "node:child_process";
import { join, resolve } from "node:path";
import { tmpdir } from "node:os";
@@ -151,17 +151,42 @@ function assertIsolatedWorkspace(dir: string): void {
expect(resolve(dir).startsWith(resolve(repoRoot))).toBe(false);
}
const createdDirs = new Set<string>();
function cleanupTempDir(dir?: string): void {
if (!dir) return;
createdDirs.delete(dir);
for (let attempt = 1; attempt <= 3; attempt += 1) {
try {
if (!existsSync(dir)) return;
rmSync(dir, { recursive: true, force: true });
return;
} catch (error) {
if (attempt === 3) {
throw error;
}
}
}
}
afterAll(() => {
for (const dir of Array.from(createdDirs)) {
cleanupTempDir(dir);
}
});
describe("merger overlap guard", () => {
let dir: string;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "fusion-test-overlap-guard-"));
createdDirs.add(dir);
assertIsolatedWorkspace(dir);
initRepo(dir);
});
afterEach(() => {
rmSync(dir, { recursive: true, force: true });
cleanupTempDir(dir);
});
it("detects overlap when branch and recent main commits touch the same file", async () => {
@@ -302,12 +327,13 @@ describe("aiMergeTask overlap-aware fallback integration", () => {
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "fusion-test-overlap-merge-"));
createdDirs.add(dir);
assertIsolatedWorkspace(dir);
initRepo(dir);
});
afterEach(() => {
rmSync(dir, { recursive: true, force: true });
cleanupTempDir(dir);
});
it("defaults to restoring the branch version for overlapping files under smart-prefer-main", async () => {

View File

@@ -15,8 +15,8 @@
* 6. Untracked file pre-existing in working tree (user WIP) — NOT staged
*/
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync } from "node:fs";
import { describe, it, expect, beforeEach, afterEach, afterAll, vi } from "vitest";
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync, existsSync } from "node:fs";
import { join, resolve } from "node:path";
import { tmpdir } from "node:os";
import { execSync } from "node:child_process";
@@ -85,6 +85,30 @@ const STUB_SETTINGS = {
commitAuthorEnabled: false, // skip --author flag to avoid user config issues
};
const createdDirs = new Set<string>();
function cleanupTempDir(dir?: string): void {
if (!dir) return;
createdDirs.delete(dir);
for (let attempt = 1; attempt <= 3; attempt += 1) {
try {
if (!existsSync(dir)) return;
rmSync(dir, { recursive: true, force: true });
return;
} catch (error) {
if (attempt === 3) {
throw error;
}
}
}
}
afterAll(() => {
for (const dir of Array.from(createdDirs)) {
cleanupTempDir(dir);
}
});
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
@@ -94,12 +118,13 @@ describe("snapshotDirtyFiles", () => {
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "fusion-test-merger-snapshot-"));
createdDirs.add(dir);
assertIsolatedWorkspace(dir);
initRepo(dir);
});
afterEach(() => {
rmSync(dir, { recursive: true, force: true });
cleanupTempDir(dir);
});
it("returns empty set when working tree is clean", async () => {
@@ -159,14 +184,18 @@ describe("commitOrAmendMergeWithFixes — staging allowlist", () => {
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "fusion-test-merger-allowlist-"));
createdDirs.add(dir);
assertIsolatedWorkspace(dir);
initRepo(dir);
warnSpy = vi.spyOn(mergerLog, "warn");
});
afterEach(() => {
warnSpy.mockRestore();
rmSync(dir, { recursive: true, force: true });
try {
warnSpy.mockRestore();
} finally {
cleanupTempDir(dir);
}
});
// ── Scenario 1: Unrelated dirty file is excluded ───────────────────────
@@ -543,14 +572,18 @@ describe("commitOrAmendMergeWithFixes — embedded-space paths round-trip", () =
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), "fusion-test-merger-allowlist-spaces-"));
createdDirs.add(dir);
assertIsolatedWorkspace(dir);
initRepo(dir);
warnSpy = vi.spyOn(mergerLog, "warn");
});
afterEach(() => {
warnSpy.mockRestore();
rmSync(dir, { recursive: true, force: true });
try {
warnSpy.mockRestore();
} finally {
cleanupTempDir(dir);
}
});
it("stages and commits a tracked file edited by the fix agent whose path contains spaces", async () => {