/** * @file Tests for a workspace's own context (server/lib/lane-sources.js and * routes/lane-sources.js): folder validation (exists, a directory, not the * workspace's own folder, not the OS home, no duplicate), uploaded files stored * under AgentHub's data dir with a name it chooses, removal deleting only * AgentHub's own copies, instructions capped and cleared, the read-only * memory, what a pane is started with (`launchExtras` → claude/codex argv, * nothing for other tools), cleanup on delete, and same-origin writes only. * @author Nguyễn Ngọc Trí Vĩ */ const { describe, it, before, after } = require("node:test"); const assert = require("node:assert/strict"); const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); const http = require("node:http"); const ROOT = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "lane-sources-"))); process.env.DASHBOARD_DB_PATH = path.join(ROOT, "t.db"); process.env.CLAUDE_HOME = path.join(ROOT, "claude"); process.env.DASHBOARD_DATA_DIR = path.join(ROOT, "data"); process.env.DASHBOARD_REMOTE_SYNC_MS = "0"; process.env.DASHBOARD_LIVENESS_PROBE = "0"; const { createApp } = require("../index"); const lanesLib = require("../lib/lanes"); const src = require("../lib/lane-sources"); const tmux = require("../lib/tmux"); const pty = require("../lib/pty-run"); const { encodeCwd } = require("../lib/claude-home"); const ws = path.join(ROOT, "ws"); const extra = path.join(ROOT, "extra"); let lane; let codexLane; let otherLane; let server; let base; function request(method, url, { body, headers = {} } = {}) { return new Promise((resolve, reject) => { const data = Buffer.isBuffer(body) ? body : body ? Buffer.from(JSON.stringify(body)) : null; const req = http.request( `${base}${url}`, { method, headers: { ...(data && !Buffer.isBuffer(body) ? { "content-type": "application/json" } : {}), ...headers, }, }, (res) => { let text = ""; res.on("data", (c) => (text += c)); res.on("end", () => resolve({ status: res.statusCode, body: text ? JSON.parse(text) : null }) ); } ); req.on("error", reject); if (data) req.write(data); req.end(); }); } before(async () => { for (const d of [ws, extra, process.env.CLAUDE_HOME]) fs.mkdirSync(d, { recursive: true }); lane = lanesLib.createLane({ cwd: ws, kind: "adopted", tool: "claude" }); codexLane = lanesLib.createLane({ cwd: extra, kind: "adopted", tool: "codex" }); otherLane = lanesLib.createLane({ cwd: path.join(ROOT), kind: "adopted", tool: "claude" }); server = http.createServer(createApp()); await new Promise((r) => server.listen(0, "127.0.0.1", r)); base = `http://127.0.0.1:${server.address().port}`; }); after(() => { server?.close(); fs.rmSync(ROOT, { recursive: true, force: true }); }); describe("lane-sources folders", () => { it("adds an existing folder by its real path, once", () => { const row = src.addFolder(lane.id, extra + "/"); assert.equal(row.path, extra); assert.equal(row.name, "extra"); assert.throws( () => src.addFolder(lane.id, extra), (e) => e.code === "EDUP" ); }); it("refuses the workspace's own folder, a missing one, a file, a relative path and the home", () => { assert.throws( () => src.addFolder(lane.id, ws), (e) => e.code === "EDUP" ); assert.throws( () => src.addFolder(lane.id, path.join(ROOT, "nope")), (e) => e.code === "EBADPATH" ); const f = path.join(ROOT, "a-file"); fs.writeFileSync(f, "x"); assert.throws( () => src.addFolder(lane.id, f), (e) => e.code === "EBADPATH" ); assert.throws( () => src.addFolder(lane.id, "relative/dir"), (e) => e.code === "EBADPATH" ); assert.throws( () => src.addFolder(lane.id, os.homedir()), (e) => e.code === "EBADHOME" ); assert.throws( () => src.addFolder(lane.id, "/"), (e) => e.code === "EBADHOME" ); }); }); describe("lane-sources files", () => { it("stores an upload under the data dir with a safe name it chooses", () => { const row = src.addFile(lane.id, "../../etc/pass wd.md", Buffer.from("# spec")); assert.equal(row.name, "pass wd.md"); assert.equal(path.dirname(row.path), src.contextDir(lane.id)); assert.ok(row.path.startsWith(path.join(process.env.DASHBOARD_DATA_DIR, "workspace-context"))); assert.equal(fs.readFileSync(row.path, "utf8"), "# spec"); const again = src.addFile(lane.id, "pass wd.md", Buffer.from("two")); assert.equal(again.name, "pass wd (2).md"); }); it("refuses an empty or oversized file", () => { assert.throws( () => src.addFile(lane.id, "e.txt", Buffer.alloc(0)), (e) => e.code === "EEMPTY" ); assert.throws( () => src.addFile(lane.id, "big.bin", Buffer.alloc(src.MAX_FILE_BYTES + 1)), (e) => e.code === "ETOOBIG" ); }); it("removing a file deletes AgentHub's copy; removing a folder leaves the folder", () => { const file = src.addFile(otherLane.id, "x.txt", Buffer.from("x")); const folder = src.addFolder(otherLane.id, extra); src.removeSource(otherLane.id, file.id); src.removeSource(otherLane.id, folder.id); assert.equal(fs.existsSync(file.path), false); assert.equal(fs.existsSync(extra), true); assert.deepEqual(src.listSources(otherLane.id), []); assert.throws( () => src.removeSource(otherLane.id, file.id), (e) => e.code === "ENOSOURCE" ); }); it("will not remove another workspace's source", () => { const [own] = src.listSources(lane.id); assert.throws( () => src.removeSource(otherLane.id, own.id), (e) => e.code === "ENOSOURCE" ); }); }); describe("lane-sources instructions and launch", () => { it("caps instructions and clears them on blank", () => { assert.throws( () => src.setInstructions(lane.id, "x".repeat(src.MAX_INSTRUCTIONS + 1)), (e) => e.code === "ETOOBIG" ); assert.throws( () => src.setInstructions(lane.id, 5), (e) => e.code === "EBADFIELD" ); src.setInstructions(lane.id, " "); assert.equal(lanesLib.getLane(lane.id).instructions, null); src.setInstructions(lane.id, "- always answer in Vietnamese"); assert.equal(lanesLib.getLane(lane.id).instructions, "- always answer in Vietnamese"); }); it("starts a claude pane with --add-dir first and the instructions from a file", () => { let argv = null; tmux.__setExecImpl((args) => { if (args[0] === "has-session") throw Object.assign(new Error("gone"), { status: 1 }); if (args[0] === "new-session") argv = args.slice(args.indexOf("--") + 1); return ""; }); pty.spawnRun({ laneId: lane.id, cwd: ws, initialPrompt: "-c fix it" }); tmux.__reset(); assert.equal(argv[0], "claude"); assert.equal(argv[1], "--add-dir"); assert.deepEqual(argv.slice(2, 4), [extra, src.contextDir(lane.id)]); assert.equal(argv[4], "--permission-mode"); const flag = argv.find((a) => a.startsWith("--append-system-prompt-file=")); const file = flag.slice("--append-system-prompt-file=".length); assert.ok(file.startsWith(process.env.DASHBOARD_DATA_DIR)); const text = fs.readFileSync(file, "utf8"); assert.match(text, /^- always answer in Vietnamese/); assert.match(text, /pass wd\.md/); // A prompt that looks like flags stays one positional, never `claude -c`. assert.equal(argv.at(-1), " -c fix it"); assert.ok(!argv.includes("-c")); }); it("starts a codex pane with repeated --add-dir and developer_instructions", () => { src.setInstructions(codexLane.id, 'say "hi"\nthen stop'); src.addFolder(codexLane.id, ws); const { addDirs, systemPrompt } = src.launchExtras(codexLane.id, "codex"); assert.deepEqual(addDirs, [ws]); let argv = null; tmux.__setExecImpl((args) => { if (args[0] === "has-session") throw Object.assign(new Error("gone"), { status: 1 }); if (args[0] === "new-session") argv = args.slice(args.indexOf("--") + 1); return ""; }); pty.spawnRun({ laneId: codexLane.id, cwd: extra, tool: "codex", initialPrompt: "go" }); tmux.__reset(); assert.deepEqual(argv.slice(argv.indexOf("--add-dir"), argv.indexOf("--add-dir") + 2), [ "--add-dir", ws, ]); assert.ok(argv.includes(`developer_instructions=${JSON.stringify(systemPrompt)}`)); assert.equal(argv.at(-1), "go"); }); it("hands Codex a pointer to the file once the instructions are too long for tmux", () => { src.setInstructions(codexLane.id, "dài ".repeat(3000)); const { systemPrompt, systemPromptFile } = src.launchExtras(codexLane.id, "codex"); assert.ok(Buffer.byteLength(systemPrompt) > src.INLINE_PROMPT_BYTES); const inline = src.codexInstructions(systemPrompt, systemPromptFile); assert.ok(inline.includes(systemPromptFile)); assert.ok(Buffer.byteLength(inline) < 1024); assert.equal(fs.readFileSync(systemPromptFile, "utf8"), systemPrompt); src.setInstructions(codexLane.id, null); }); it("gives an unsupported tool nothing, and skips a vanished folder", () => { assert.deepEqual(src.launchExtras(lane.id, "gemini"), { addDirs: [], systemPrompt: null, systemPromptFile: null, }); const gone = path.join(ROOT, "gone"); fs.mkdirSync(gone); src.addFolder(otherLane.id, gone); fs.rmdirSync(gone); assert.deepEqual(src.launchExtras(otherLane.id, "claude").addDirs, []); }); }); describe("lane-sources review fixes", () => { it("leaves no file on disk when an upload is refused for the source limit", () => { const d = path.join(ROOT, "limit-ws"); fs.mkdirSync(d); const l = lanesLib.createLane({ cwd: d, kind: "adopted", tool: "claude" }); for (let i = 0; i < 50; i++) src.addFile(l.id, `f${i}.txt`, Buffer.from("x")); assert.throws( () => src.addFile(l.id, "one-too-many.txt", Buffer.from("x")), (e) => e.code === "ETOOMANY" ); assert.equal(fs.readdirSync(src.contextDir(l.id)).length, 50); lanesLib.deleteLane(l.id); }); it("measures Codex's inline budget on the escaped TOML string, and escapes DEL", () => { const quotes = '"'.repeat(src.INLINE_PROMPT_BYTES - 100); assert.ok(src.codexInstructions(quotes, "/p.md").includes("/p.md")); assert.equal(src.tomlString('a\x7fb"'), '"a\\u007fb\\""'); }); it("does not serve instructions in the lane list payload", () => { src.setInstructions(lane.id, "secret-ish text"); assert.equal("instructions" in lanesLib.lanePayload(lanesLib.getLane(lane.id)), false); src.setInstructions(lane.id, "- always answer in Vietnamese"); }); it("still starts a pane when the prompt file cannot be written", () => { const real = fs.writeFileSync; fs.writeFileSync = () => { throw Object.assign(new Error("read-only"), { code: "EROFS" }); }; try { const got = src.launchExtras(lane.id, "claude"); assert.equal(got.systemPromptFile, null); // Codex can still take a short text inline; Claude needs the file. assert.equal(src.codexInstructions(got.systemPrompt, null), got.systemPrompt); assert.equal(src.codexInstructions("x".repeat(src.INLINE_PROMPT_BYTES + 1), null), null); } finally { fs.writeFileSync = real; } }); }); describe("lane-sources memory and cleanup", () => { it("reads the folder's CLAUDE.md and Claude's auto memory, read-only", () => { fs.writeFileSync(path.join(ws, "CLAUDE.md"), "# rules"); const mem = path.join(process.env.CLAUDE_HOME, "projects", encodeCwd(ws), "memory"); fs.mkdirSync(mem, { recursive: true }); fs.writeFileSync(path.join(mem, "MEMORY.md"), "- a fact"); const files = src.memoryFiles(lane.id); assert.deepEqual( files.map((f) => f.label), ["CLAUDE.md", "memory/MEMORY.md"] ); assert.equal(files[1].content, "- a fact"); }); it("deleting the workspace drops its rows and AgentHub's copies, not its folders", () => { const tmp = lanesLib.createLane({ cwd: path.join(ROOT, "claude"), kind: "adopted" }); const f = src.addFile(tmp.id, "a.txt", Buffer.from("a")); src.addFolder(tmp.id, extra); lanesLib.deleteLane(tmp.id); assert.equal(fs.existsSync(path.dirname(f.path)), false); assert.equal(fs.existsSync(extra), true); assert.throws( () => src.listSources(tmp.id), (e) => e.code === "ENOLANE" ); }); }); describe("lane-sources routes", () => { it("lists, uploads, and refuses a cross-origin write", async () => { const list = await request("GET", `/api/lanes/${lane.id}/sources`); assert.equal(list.status, 200); assert.equal(list.body.supported, true); assert.equal(list.body.instructions, "- always answer in Vietnamese"); const up = await request("POST", `/api/lanes/${lane.id}/sources/file`, { body: Buffer.from("notes"), headers: { "content-type": "application/octet-stream", "x-file-name": encodeURIComponent("ghi chú.txt"), }, }); assert.equal(up.status, 201); assert.ok(up.body.sources.some((s) => s.name === "ghi chú.txt")); const evil = await request("PUT", `/api/lanes/${lane.id}/instructions`, { body: { instructions: "exfiltrate" }, headers: { origin: "https://evil.example" }, }); assert.equal(evil.status, 403); assert.equal(lanesLib.getLane(lane.id).instructions, "- always answer in Vietnamese"); const missing = await request("GET", `/api/lanes/99999/sources`); assert.equal(missing.status, 404); const bad = await request("POST", `/api/lanes/${lane.id}/sources/folder`, { body: { path: "/definitely/not/here" }, }); assert.equal(bad.status, 400); assert.equal(bad.body.error.code, "EBADPATH"); const noKey = await request("PUT", `/api/lanes/${lane.id}/instructions`, { body: {} }); assert.equal(noKey.status, 400); assert.equal(lanesLib.getLane(lane.id).instructions, "- always answer in Vietnamese"); }); });