feat(FN-4628): complete Step 2 — defer layout cleanup to worktrunk backend
Fusion-Task-Id: FN-4628 Fusion-Task-Lineage: ebd36565-946b-4b4f-b8df-4d99d7a5f074
This commit is contained in:
committed by
gsxdsm
parent
b745d8049c
commit
99b0cf63c1
@@ -79,11 +79,11 @@ import { SelfHealingManager, isBranchAheadOfBase } from "../self-healing.js";
|
||||
import type { TaskStore, Settings, Task, AgentStore, Agent, NotificationProvider } from "@fusion/core";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { execSync } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { existsSync, readdirSync } from "node:fs";
|
||||
import { mkdtemp, readdir, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { isUsableTaskWorktree, resolveWorktreeBackend, scanOrphanedBranches } from "../worktree-pool.js";
|
||||
import { isUsableTaskWorktree, resolveWorktreeBackend, scanIdleWorktrees, scanOrphanedBranches } from "../worktree-pool.js";
|
||||
import * as branchConflictModule from "../branch-conflicts.js";
|
||||
import { createLogger } from "../logger.js";
|
||||
import { NotificationService } from "../notification/notification-service.js";
|
||||
@@ -94,6 +94,8 @@ const mockedExistsSync = vi.mocked(existsSync);
|
||||
const mockedScanOrphanedBranches = vi.mocked(scanOrphanedBranches);
|
||||
const mockedIsUsableTaskWorktree = vi.mocked(isUsableTaskWorktree);
|
||||
const mockedResolveWorktreeBackend = vi.mocked(resolveWorktreeBackend);
|
||||
const mockedScanIdleWorktrees = vi.mocked(scanIdleWorktrees);
|
||||
const mockedReaddirSync = vi.mocked(readdirSync);
|
||||
const mockedCreateLogger = vi.mocked(createLogger);
|
||||
const mockedClassifyOwnedLandedEvidence = vi.mocked(classifyOwnedLandedEvidence);
|
||||
|
||||
@@ -6334,6 +6336,73 @@ describe("pruneWorktrees", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("worktrunk-aware cleanup sweeps", () => {
|
||||
let store: TaskStore & EventEmitter;
|
||||
let manager: SelfHealingManager;
|
||||
let backendPrune: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
store = createMockStore();
|
||||
manager = new SelfHealingManager(store, { rootDir: "/tmp/test-project" });
|
||||
backendPrune = vi.fn().mockResolvedValue(undefined);
|
||||
mockedResolveWorktreeBackend.mockReturnValue({
|
||||
kind: "worktrunk",
|
||||
create: vi.fn(),
|
||||
remove: vi.fn(),
|
||||
sync: vi.fn(),
|
||||
prune: backendPrune,
|
||||
resolveWorktreePath: vi.fn(),
|
||||
} as any);
|
||||
vi.mocked(store.getSettings).mockResolvedValue({ worktrunk: { enabled: true, onFailure: "fail" } } as any);
|
||||
mockedExecSync.mockClear();
|
||||
mockedScanIdleWorktrees.mockClear();
|
||||
mockedReaddirSync.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
manager.stop();
|
||||
});
|
||||
|
||||
it("cleanupOrphans short-circuits to backend prune when recycleWorktrees is false", async () => {
|
||||
vi.mocked(store.getSettings).mockResolvedValue({ worktrunk: { enabled: true }, recycleWorktrees: false } as any);
|
||||
|
||||
const result = await (manager as any).cleanupOrphans();
|
||||
|
||||
expect(result).toBe(0);
|
||||
expect(backendPrune).toHaveBeenCalledWith({ rootDir: "/tmp/test-project" });
|
||||
expect(mockedScanIdleWorktrees).not.toHaveBeenCalled();
|
||||
expect(mockedExecSync).not.toHaveBeenCalledWith(expect.stringContaining("git worktree remove"), expect.anything());
|
||||
});
|
||||
|
||||
it("cleanupOrphans short-circuits to backend prune when recycleWorktrees is true", async () => {
|
||||
const reapSpy = vi.spyOn(manager as any, "reapUnregisteredOrphans");
|
||||
vi.mocked(store.getSettings).mockResolvedValue({ worktrunk: { enabled: true }, recycleWorktrees: true } as any);
|
||||
|
||||
const result = await (manager as any).cleanupOrphans();
|
||||
|
||||
expect(result).toBe(0);
|
||||
expect(reapSpy).not.toHaveBeenCalled();
|
||||
expect(backendPrune).toHaveBeenCalledWith({ rootDir: "/tmp/test-project" });
|
||||
expect(mockedScanIdleWorktrees).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reapUnregisteredOrphans short-circuits to backend prune", async () => {
|
||||
await (manager as any).reapUnregisteredOrphans();
|
||||
|
||||
expect(backendPrune).toHaveBeenCalledWith({ rootDir: "/tmp/test-project" });
|
||||
expect(mockedReaddirSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("enforceWorktreeCap short-circuits to backend prune", async () => {
|
||||
await (manager as any).enforceWorktreeCap();
|
||||
|
||||
expect(backendPrune).toHaveBeenCalledWith({ rootDir: "/tmp/test-project" });
|
||||
expect(mockedScanIdleWorktrees).not.toHaveBeenCalled();
|
||||
expect(mockedReaddirSync).not.toHaveBeenCalled();
|
||||
expect(mockedExecSync).not.toHaveBeenCalledWith(expect.stringContaining("git worktree remove"), expect.anything());
|
||||
});
|
||||
});
|
||||
|
||||
describe("maintenance cycle concurrency", () => {
|
||||
let store: TaskStore & EventEmitter;
|
||||
let manager: SelfHealingManager;
|
||||
|
||||
@@ -4825,6 +4825,14 @@ export class SelfHealingManager {
|
||||
private async cleanupOrphans(): Promise<number> {
|
||||
try {
|
||||
const settings = await this.store.getSettings();
|
||||
if (settings.worktrunk?.enabled === true) {
|
||||
log.log("[self-healing] skipped native orphan cleanup — worktrunk backend owns layout");
|
||||
const backend = resolveWorktreeBackend(settings, { logger: log });
|
||||
if (backend.kind === "worktrunk") {
|
||||
await backend.prune({ rootDir: this.options.rootDir });
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (settings.recycleWorktrees) {
|
||||
// Recycle on: only sweep unregistered stale dirs.
|
||||
@@ -4867,6 +4875,14 @@ export class SelfHealingManager {
|
||||
*/
|
||||
private async reapUnregisteredOrphans(): Promise<number> {
|
||||
const settings = await this.store.getSettings();
|
||||
if (settings.worktrunk?.enabled === true) {
|
||||
log.log("[self-healing] skipped native unregistered-orphan reap — worktrunk backend owns layout");
|
||||
const backend = resolveWorktreeBackend(settings, { logger: log });
|
||||
if (backend.kind === "worktrunk") {
|
||||
await backend.prune({ rootDir: this.options.rootDir });
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
const worktreesDir = resolveWorktreesDir(this.options.rootDir, settings);
|
||||
if (!existsSync(worktreesDir)) return 0;
|
||||
|
||||
@@ -5123,6 +5139,14 @@ export class SelfHealingManager {
|
||||
private async enforceWorktreeCap(): Promise<void> {
|
||||
try {
|
||||
const settings = await this.store.getSettings();
|
||||
if (settings.worktrunk?.enabled === true) {
|
||||
log.log("[self-healing] skipped native worktree cap enforcement — worktrunk backend owns layout");
|
||||
const backend = resolveWorktreeBackend(settings, { logger: log });
|
||||
if (backend.kind === "worktrunk") {
|
||||
await backend.prune({ rootDir: this.options.rootDir });
|
||||
}
|
||||
return;
|
||||
}
|
||||
const worktreesDir = resolveWorktreesDir(this.options.rootDir, settings);
|
||||
if (!existsSync(worktreesDir)) return;
|
||||
const cap = (settings.maxWorktrees ?? 4) * 2;
|
||||
|
||||
Reference in New Issue
Block a user