import assert from "node:assert/strict"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import test from "node:test"; import { canonicalWorkspace, findOverlappingWorkspace } from "../src/core/workspace-scope.js"; function writePeer(instances: string, botId: string, workspace: string): void { const directory = path.join(instances, botId); fs.mkdirSync(directory, { recursive: true }); fs.writeFileSync(path.join(directory, "config.json"), JSON.stringify({ configVersion: 3, bot: { id: botId, workspace, persona: "test", agent: { id: "fake", command: process.execPath, args: ["agent.mjs"], env: {} }, skills: [], permissions: { mode: "deny", allowedTools: [], allowedCommandPatterns: [] } }, gateway: { platform: { type: "webhook", secret: "test-secret" } }, runtime: { acp: {} } })); } test("peer scan rejects identical, parent, and child workspaces and allows disjoint scopes", async (t) => { const directory = await fs.promises.mkdtemp(path.join(os.tmpdir(), "gori-workspace-scope-")); t.after(() => fs.rmSync(directory, { recursive: true, force: true })); const instances = path.join(directory, "instances"); const workspace = path.join(directory, "workspace"); const child = path.join(workspace, "child"); const sibling = path.join(directory, "sibling"); fs.mkdirSync(child, { recursive: true }); fs.mkdirSync(sibling, { recursive: true }); // Disjoint workspaces are allowed. writePeer(instances, "sibling-bot", sibling); assert.equal(findOverlappingWorkspace(workspace, "current", instances), undefined); // An identical workspace is rejected. writePeer(instances, "same-bot", workspace); assert.equal(findOverlappingWorkspace(workspace, "current", instances), "same-bot"); // A child workspace is rejected from the parent's perspective. fs.rmSync(path.join(instances, "same-bot"), { recursive: true, force: true }); writePeer(instances, "child-bot", child); assert.equal(findOverlappingWorkspace(workspace, "current", instances), "child-bot"); // From the child's perspective an identical or parent peer workspace is rejected. assert.equal(findOverlappingWorkspace(child, "current", instances), "child-bot"); writePeer(instances, "same-bot", workspace); assert.ok(["same-bot", "child-bot"].includes(findOverlappingWorkspace(child, "current", instances)!)); // The instance's own entry is skipped even when its workspace matches. fs.rmSync(path.join(instances, "child-bot"), { recursive: true, force: true }); assert.equal(findOverlappingWorkspace(workspace, "same-bot", instances), undefined); // A missing instances directory means no peers to conflict with. assert.equal(findOverlappingWorkspace(workspace, "current", path.join(directory, "absent")), undefined); }); test("peer scan fails closed for unsafe or invalid peer configs", async (t) => { const directory = await fs.promises.mkdtemp(path.join(os.tmpdir(), "gori-workspace-scope-closed-")); t.after(() => fs.rmSync(directory, { recursive: true, force: true })); const instances = path.join(directory, "instances"); const workspace = path.join(directory, "workspace"); fs.mkdirSync(workspace, { recursive: true }); // Peer config declares a workspace that cannot be canonicalized. writePeer(instances, "missing-bot", path.join(directory, "missing")); assert.throws(() => findOverlappingWorkspace(workspace, "current", instances)); fs.rmSync(path.join(instances, "missing-bot"), { recursive: true, force: true }); // Corrupt peer config fails closed. const corrupt = path.join(instances, "corrupt-bot"); fs.mkdirSync(corrupt); fs.writeFileSync(path.join(corrupt, "config.json"), "not-json"); assert.throws(() => findOverlappingWorkspace(workspace, "current", instances), /Invalid peer Config v3/); fs.rmSync(path.join(corrupt, "config.json")); // Missing peer config fails closed. assert.throws(() => findOverlappingWorkspace(workspace, "current", instances), /missing config/); fs.rmSync(corrupt, { recursive: true, force: true }); // Symlinked peer entries are refused. writePeer(instances, "real-bot", path.join(directory, "sibling-real")); fs.mkdirSync(path.join(directory, "sibling-real"), { recursive: true }); fs.symlinkSync(path.join(instances, "real-bot"), path.join(instances, "linked-bot")); assert.throws(() => findOverlappingWorkspace(workspace, "current", instances), /unsafe peer instance entry/); }); test("canonicalWorkspace resolves symlinks and relative segments", async (t) => { const directory = await fs.promises.mkdtemp(path.join(os.tmpdir(), "gori-workspace-canonical-")); t.after(() => fs.rmSync(directory, { recursive: true, force: true })); const real = path.join(directory, "real"); fs.mkdirSync(real, { recursive: true }); const link = path.join(directory, "link"); fs.symlinkSync(real, link); assert.equal(canonicalWorkspace(link), fs.realpathSync.native(real)); assert.equal(canonicalWorkspace(path.join(real, "..", "real")), fs.realpathSync.native(real)); assert.throws(() => canonicalWorkspace(path.join(directory, "missing"))); });