feat(FN-3508): resolve node-specific working dirs and align runtime surfaces

- Add canonical working-directory resolution with node mapping support in core and engine paths
- Update runtime/multi-project documentation and include published-package changesets for the merged work
- Migrate roadmap dashboard surface to the bundled plugin registry flow and adjust lazy-view integration/tests
- Default non-ephemeral agents to active state and extend related agent/route/executor test coverage

Fusion-Task-Id: FN-3508
This commit is contained in:
Fusion
2026-05-08 17:04:14 -07:00
committed by gsxdsm
parent 5d226a28ab
commit ea34afaff6
10 changed files with 128 additions and 12 deletions

View File

@@ -862,6 +862,43 @@ describe("CentralCore", () => {
);
});
it("resolves working directories strictly from exact project/node mappings", async () => {
const projectPath = join(tempDir, "mapping-resolver-project");
mkdirSync(projectPath);
projectPaths.push(projectPath);
const project = await central.registerProject({
name: "Mapping Resolver",
path: projectPath,
});
const remoteNode = await central.registerNode({ name: "mapping-resolver-remote", type: "remote", url: "http://remote.example" });
const otherNode = await central.registerNode({ name: "mapping-resolver-other", type: "remote", url: "http://other.example" });
const localNode = (await central.listNodes()).find((node) => node.type === "local");
expect(localNode).toBeDefined();
await expect(central.resolveLocalProjectWorkingDirectory(project.id)).resolves.toBe(projectPath);
await central.upsertProjectNodePathMapping({
projectId: project.id,
nodeId: remoteNode.id,
path: "/remote/project/root",
});
await expect(central.resolveProjectWorkingDirectory(project.id, remoteNode.id)).resolves.toBe(
"/remote/project/root",
);
await expect(central.resolveProjectWorkingDirectory("proj_missing", remoteNode.id)).rejects.toThrow(
"Project not found: proj_missing",
);
await expect(central.resolveProjectWorkingDirectory(project.id, "node_missing")).rejects.toThrow(
"Node not found: node_missing",
);
await expect(central.resolveProjectWorkingDirectory(project.id, otherNode.id)).rejects.toThrow(
`Project/node path mapping not found for projectId=${project.id} nodeId=${otherNode.id}`,
);
});
it("should check local node health and emit node:health:changed", async () => {
const node = await central.registerNode({ name: "local-health", type: "local" });

View File

@@ -1779,6 +1779,40 @@ export class CentralCore extends EventEmitter<CentralCoreEvents> {
return row?.path;
}
async resolveProjectWorkingDirectory(projectId: string, nodeId: string): Promise<string> {
this.ensureInitialized();
const project = await this.getProject(projectId);
if (!project) {
throw new Error(`Project not found: ${projectId}`);
}
const node = await this.getNode(nodeId);
if (!node) {
throw new Error(`Node not found: ${nodeId}`);
}
const mappedPath = await this.getProjectNodePath(projectId, nodeId);
if (!mappedPath) {
throw new Error(
`Project/node path mapping not found for projectId=${projectId} nodeId=${nodeId}`,
);
}
return mappedPath;
}
async resolveLocalProjectWorkingDirectory(projectId: string): Promise<string> {
this.ensureInitialized();
const localNode = await this.getLocalNode();
if (!localNode) {
throw new Error("Local node not found");
}
return this.resolveProjectWorkingDirectory(projectId, localNode.id);
}
async listProjectNodePathMappings(filters?: {
projectId?: string;
nodeId?: string;