feat(FN-4813): complete Step 5 — add isolation transition runtime restart path

Fusion-Task-Id: FN-4813
Fusion-Task-Lineage: 846893a5-2afa-4817-8f64-8c444d2fd713
This commit is contained in:
Fusion (runfusion.ai)
2026-05-16 17:03:12 -07:00
committed by gsxdsm
parent 2ca18e983a
commit 63adddadf9
10 changed files with 513 additions and 96 deletions

View File

@@ -0,0 +1,53 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { mkdirSync, mkdtempSync } from "node:fs";
import { rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { CentralCore } from "../central-core.js";
describe("CentralCore.transitionProjectIsolation", () => {
let tempDir: string;
let projectPath: string;
let core: CentralCore;
beforeEach(async () => {
tempDir = mkdtempSync(join(tmpdir(), "fn-project-isolation-transition-"));
projectPath = join(tempDir, "project");
mkdirSync(projectPath, { recursive: true });
core = new CentralCore(tempDir);
await core.init();
});
afterEach(async () => {
await core.close();
await rm(tempDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
});
it("returns noop when next mode matches existing mode", async () => {
const project = await core.registerProject({
name: "Test",
path: projectPath,
isolationMode: "in-process",
});
const result = await core.transitionProjectIsolation(project.id, "in-process");
expect(result).toEqual({ ok: false, reason: "noop" });
});
it("updates mode and logs activity on success", async () => {
const project = await core.registerProject({
name: "Test",
path: projectPath,
isolationMode: "in-process",
});
const result = await core.transitionProjectIsolation(project.id, "child-process");
expect(result).toEqual({ ok: true });
const updated = await core.getProject(project.id);
expect(updated?.isolationMode).toBe("child-process");
const activity = await core.getRecentActivity({ projectId: project.id, limit: 10 });
expect(activity.some((entry) => entry.type === "project:isolation-transition")).toBe(true);
});
});

View File

@@ -514,6 +514,40 @@ export class CentralCore extends EventEmitter<CentralCoreEvents> {
return updated;
}
async transitionProjectIsolation(
projectId: string,
nextMode: IsolationMode,
opts: { force?: boolean } = {}
): Promise<{ ok: true } | { ok: false; reason: string; activeTaskCount?: number }> {
void opts;
this.ensureInitialized();
const project = await this.getProject(projectId);
if (!project) {
throw new Error(`Project not found: ${projectId}`);
}
if (project.isolationMode === nextMode) {
return { ok: false, reason: "noop" };
}
await this.updateProject(projectId, { isolationMode: nextMode });
await this.logActivity({
type: "project:isolation-transition",
projectId,
projectName: project.name,
timestamp: new Date().toISOString(),
details: `Project isolation mode transitioned: ${project.isolationMode} -> ${nextMode}`,
metadata: {
from: project.isolationMode,
to: nextMode,
},
});
return { ok: true };
}
/**
* Reconcile stale project statuses.
*