103 lines
5.0 KiB
TypeScript
103 lines
5.0 KiB
TypeScript
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")));
|
|
});
|