172 lines
7.5 KiB
JavaScript
172 lines
7.5 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import {
|
|
assertInstanceKey,
|
|
prepareInstanceWorkspace,
|
|
recordInstall,
|
|
removeInstanceWorkspace
|
|
} from "../src/instance-workspace.js";
|
|
|
|
function roots() {
|
|
const base = fs.mkdtempSync(path.join(os.tmpdir(), "dev-server-copy-"));
|
|
const workspace = path.join(base, "workspace");
|
|
const instances = path.join(base, "instances");
|
|
fs.mkdirSync(workspace);
|
|
fs.mkdirSync(instances);
|
|
return { base, workspace, instances, cleanup: () => fs.rmSync(base, { recursive: true, force: true }) };
|
|
}
|
|
|
|
function execution(workspace, key, files) {
|
|
const directory = path.join(workspace, key);
|
|
fs.mkdirSync(directory, { recursive: true });
|
|
for (const [name, content] of Object.entries(files)) {
|
|
const file = path.join(directory, name);
|
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
fs.writeFileSync(file, content);
|
|
}
|
|
return directory;
|
|
}
|
|
|
|
test("an execution key must be one safe path segment", () => {
|
|
for (const bad of ["../escape", "a/b", "", ".", "with space", "x".repeat(65)]) {
|
|
assert.throws(() => assertInstanceKey(bad), /Invalid execution key/, `accepted ${JSON.stringify(bad)}`);
|
|
}
|
|
assert.equal(assertInstanceKey("7f3c1e2a-b4d5"), "7f3c1e2a-b4d5");
|
|
});
|
|
|
|
test("the copy holds the execution's own files and nothing above them", () => {
|
|
const item = roots();
|
|
try {
|
|
execution(item.workspace, "exec-a", { "package.json": "{}", "src/main.js": "console.log(1)" });
|
|
execution(item.workspace, "exec-b", { "secret.txt": "b's work" });
|
|
|
|
const prepared = prepareInstanceWorkspace({ workspaceRoot: item.workspace, instancesRoot: item.instances, key: "exec-a" });
|
|
|
|
assert.equal(fs.readFileSync(path.join(prepared.target, "src/main.js"), "utf8"), "console.log(1)");
|
|
assert.ok(!fs.existsSync(path.join(prepared.target, "secret.txt")));
|
|
assert.equal(prepared.entries, 3);
|
|
} finally { item.cleanup(); }
|
|
});
|
|
|
|
test("a symlink into another execution is dropped, not followed", () => {
|
|
// The tree is written by the coding agent, so this is the move that would turn the copy - whose
|
|
// whole purpose is isolation - into a window onto somebody else's work.
|
|
const item = roots();
|
|
try {
|
|
execution(item.workspace, "exec-b", { "secret.txt": "b's work" });
|
|
const source = execution(item.workspace, "exec-a", { "package.json": "{}" });
|
|
fs.symlinkSync(path.join(item.workspace, "exec-b"), path.join(source, "peek"));
|
|
|
|
const prepared = prepareInstanceWorkspace({ workspaceRoot: item.workspace, instancesRoot: item.instances, key: "exec-a" });
|
|
|
|
assert.ok(!fs.existsSync(path.join(prepared.target, "peek")));
|
|
assert.ok(!fs.existsSync(path.join(prepared.target, "peek", "secret.txt")));
|
|
} finally { item.cleanup(); }
|
|
});
|
|
|
|
test("an execution directory replaced by a symlink is refused outright", () => {
|
|
const item = roots();
|
|
try {
|
|
const outside = path.join(item.base, "outside");
|
|
fs.mkdirSync(outside);
|
|
fs.writeFileSync(path.join(outside, "elsewhere.txt"), "not yours");
|
|
fs.symlinkSync(outside, path.join(item.workspace, "exec-a"));
|
|
|
|
assert.throws(
|
|
() => prepareInstanceWorkspace({ workspaceRoot: item.workspace, instancesRoot: item.instances, key: "exec-a" }),
|
|
/escapes the workspace root/
|
|
);
|
|
} finally { item.cleanup(); }
|
|
});
|
|
|
|
test("node_modules is not copied, because the install recreates it", () => {
|
|
const item = roots();
|
|
try {
|
|
execution(item.workspace, "exec-a", { "package.json": "{}", "node_modules/left-pad/index.js": "module.exports=1" });
|
|
|
|
const prepared = prepareInstanceWorkspace({ workspaceRoot: item.workspace, instancesRoot: item.instances, key: "exec-a" });
|
|
|
|
assert.ok(!fs.existsSync(path.join(prepared.target, "node_modules")));
|
|
} finally { item.cleanup(); }
|
|
});
|
|
|
|
test("a refresh replaces project files and keeps the installed dependencies", () => {
|
|
const item = roots();
|
|
try {
|
|
const source = execution(item.workspace, "exec-a", { "package.json": "{}", "package-lock.json": "{\"v\":1}", "old.js": "gone soon" });
|
|
const first = prepareInstanceWorkspace({ workspaceRoot: item.workspace, instancesRoot: item.instances, key: "exec-a" });
|
|
fs.mkdirSync(path.join(first.target, "node_modules"), { recursive: true });
|
|
fs.writeFileSync(path.join(first.target, "node_modules", "marker"), "installed");
|
|
recordInstall(first.target);
|
|
|
|
fs.rmSync(path.join(source, "old.js"));
|
|
fs.writeFileSync(path.join(source, "new.js"), "fresh");
|
|
const second = prepareInstanceWorkspace({ workspaceRoot: item.workspace, instancesRoot: item.instances, key: "exec-a" });
|
|
|
|
assert.ok(!fs.existsSync(path.join(second.target, "old.js")), "a deleted file must not survive the refresh");
|
|
assert.equal(fs.readFileSync(path.join(second.target, "new.js"), "utf8"), "fresh");
|
|
assert.equal(fs.readFileSync(path.join(second.target, "node_modules", "marker"), "utf8"), "installed");
|
|
assert.equal(second.installIsCurrent, true);
|
|
} finally { item.cleanup(); }
|
|
});
|
|
|
|
test("a changed lockfile makes the recorded install stale", () => {
|
|
const item = roots();
|
|
try {
|
|
const source = execution(item.workspace, "exec-a", { "package-lock.json": "{\"v\":1}" });
|
|
const first = prepareInstanceWorkspace({ workspaceRoot: item.workspace, instancesRoot: item.instances, key: "exec-a" });
|
|
fs.mkdirSync(path.join(first.target, "node_modules"), { recursive: true });
|
|
recordInstall(first.target);
|
|
|
|
fs.writeFileSync(path.join(source, "package-lock.json"), "{\"v\":2}");
|
|
const second = prepareInstanceWorkspace({ workspaceRoot: item.workspace, instancesRoot: item.instances, key: "exec-a" });
|
|
|
|
assert.equal(second.installIsCurrent, false);
|
|
} finally { item.cleanup(); }
|
|
});
|
|
|
|
test("with no lockfile the install is never called current", () => {
|
|
const item = roots();
|
|
try {
|
|
execution(item.workspace, "exec-a", { "package.json": "{}" });
|
|
const prepared = prepareInstanceWorkspace({ workspaceRoot: item.workspace, instancesRoot: item.instances, key: "exec-a" });
|
|
fs.mkdirSync(path.join(prepared.target, "node_modules"), { recursive: true });
|
|
recordInstall(prepared.target);
|
|
|
|
const again = prepareInstanceWorkspace({ workspaceRoot: item.workspace, instancesRoot: item.instances, key: "exec-a" });
|
|
assert.equal(again.installIsCurrent, false);
|
|
} finally { item.cleanup(); }
|
|
});
|
|
|
|
test("a tree beyond the declared limits is refused instead of filling the volume", () => {
|
|
const item = roots();
|
|
try {
|
|
execution(item.workspace, "exec-a", { "a.txt": "x".repeat(1024), "b.txt": "y".repeat(1024) });
|
|
assert.throws(
|
|
() => prepareInstanceWorkspace({ workspaceRoot: item.workspace, instancesRoot: item.instances, key: "exec-a", limits: { maxBytes: 1500, maxEntries: 100 } }),
|
|
/exceeds 1500 bytes/
|
|
);
|
|
assert.throws(
|
|
() => prepareInstanceWorkspace({ workspaceRoot: item.workspace, instancesRoot: item.instances, key: "exec-a", limits: { maxBytes: 1e9, maxEntries: 1 } }),
|
|
/exceeds 1 entries/
|
|
);
|
|
} finally { item.cleanup(); }
|
|
});
|
|
|
|
test("discarding an execution reclaims its directory", () => {
|
|
const item = roots();
|
|
try {
|
|
execution(item.workspace, "exec-a", { "package.json": "{}" });
|
|
const prepared = prepareInstanceWorkspace({ workspaceRoot: item.workspace, instancesRoot: item.instances, key: "exec-a" });
|
|
assert.ok(fs.existsSync(prepared.target));
|
|
|
|
removeInstanceWorkspace(item.instances, "exec-a");
|
|
|
|
assert.ok(!fs.existsSync(prepared.target));
|
|
assert.ok(fs.existsSync(path.join(item.workspace, "exec-a")), "the workspace itself is not ours to delete");
|
|
} finally { item.cleanup(); }
|
|
});
|