// #1873 (redact6): a seat refusal names the config, the seat or the registration by label, // never a host path. The sentinel is a component of every scratch path, so a refusal that // quotes a path quotes the sentinel too; each test also checks the scratch root and the label. import { test } from 'node:test'; import assert from 'node:assert/strict'; import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { SeatError, loadDataRoot, readRegistration, registrationPath, resolveSeat } from '../src/seat.mjs'; import { lockSessionWorkspace, newRun, readSessions, sessionStore, sessionWorkspace, sessionsFile, threadStore } from '../src/session.mjs'; const secret = 'sk-test-NOT-A-SECRET-1234'; function scratch(t) { const root = mkdtempSync(join(tmpdir(), 'r6-')); t.after(() => rmSync(root, { recursive: true, force: true })); const dir = join(root, secret); mkdirSync(dir); return { root, dir }; } function labelled(label, root) { return error => { assert.ok(error instanceof SeatError, error.stack); assert.equal(error.message, label); assert.ok(!error.message.includes(root), error.message); assert.doesNotMatch(error.message, /sk-test|NOT-A-SECRET/); return true; }; } function write(file, text) { mkdirSync(dirname(file), { recursive: true }); writeFileSync(file, text); return file; } test('config refusals name the config, not its path', t => { const { root, dir } = scratch(t); assert.throws(() => loadDataRoot(join(dir, 'config.json')), labelled('config not found', root)); assert.throws(() => loadDataRoot(write(join(dir, 'array.json'), '[]')), labelled('config is not an object', root)); assert.throws(() => loadDataRoot(write(join(dir, 'relative.json'), '{"dataRoot":"data"}')), labelled('config.dataRoot must be an absolute path', root)); }); test('seat directory refusals name the seat, not its directory', t => { const { root, dir } = scratch(t); assert.throws(() => resolveSeat(join(dir, 'agents', 'builder')), labelled('no such seat directory: seat builder', root)); mkdirSync(join(dir, 'agents', 'builder'), { recursive: true }); assert.throws(() => resolveSeat('builder', { repo: dir }), labelled('seat has no launch.sh: seat builder', root)); write(join(dir, 'agents', 'builder', 'launch.sh'), '#!/bin/sh\n'); chmodSync(join(dir, 'agents', 'builder', 'launch.sh'), 0o644); assert.throws(() => resolveSeat('builder', { repo: dir }), labelled('launch script is not executable: launch.sh of seat builder', root)); }); test('a registration that does not validate is named by its layout and seat', t => { const { root, dir } = scratch(t); write(registrationPath(dir, 'builder', 'repo'), '{}'); assert.throws(() => readRegistration(dir, 'builder', 'repo'), error => { assert.ok(error instanceof SeatError, error.stack); assert.match(error.message, /: registration repo\/builder$/); assert.ok(!error.message.includes(root), error.message); assert.doesNotMatch(error.message, /sk-test|NOT-A-SECRET/); return true; }); }); test('an unreadable or malformed sessions file is named by its place under the data root', t => { const { root, dir } = scratch(t); write(sessionsFile(dir), 'not json'); assert.throws(() => readSessions(dir), labelled('sessions file is unreadable: bus-host/sessions.json under the data root', root)); write(sessionsFile(dir), '{"sessionsVersion":0}'); assert.throws(() => readSessions(dir), labelled('sessions file is malformed: bus-host/sessions.json under the data root', root)); }); // Review round (#1873): session stores, workspaces, locks and run directories are named under // the data root by their checked ids, never by host path; codes and exits are unchanged. function coded(label, root, code) { return error => labelled(label, root)(error) && error.code === code && error.exitCode === 2; } test('an unsafe or unmakeable thread store directory is named under the data root, never by path', t => { const { root, dir } = scratch(t); mkdirSync(join(dir, 'threads'), { mode: 0o755 }); chmodSync(join(dir, 'threads'), 0o755); assert.throws(() => threadStore(dir, 'acme', 't1'), coded('thread store: under the data root, threads is unsafe', root, 'thread-store-refused')); // A system error keeps its code; its message names the directory by label. assert.throws(() => threadStore(join(dir, 'absent'), 'acme', 't1'), error => { assert.equal(error.code, 'ENOENT'); assert.equal(error.message, 'thread store: cannot mkdir under the data root, threads (ENOENT)'); assert.ok(!error.message.includes(root), error.message); return true; }); }); test('a legacy session store left behind is named under the data root, never by path', t => { const { root, dir } = scratch(t); mkdirSync(join(dir, 'threads', 'acme', 't1', 'session'), { recursive: true, mode: 0o700 }); assert.throws(() => sessionStore(dir, 'acme', 't1', 's1'), coded('thread store: under the data root, threads/acme/t1/session holds history no legacy session maps', root, 'legacy-store-unmapped')); }); test('a session workspace that overlaps a live one is named under the data root, never by path', t => { const { root, dir } = scratch(t); sessionWorkspace(dir, 'acme', 'i1', 's1'); assert.throws(() => sessionWorkspace(dir, 'acme', 'i1', 's2', { live: [join(dir, 'session-workspaces', 'acme')] }), coded('session workspace: under the data root, session-workspaces/acme/i1/s2 overlaps a live workspace', root, 'session-workspace-overlap')); }); test('a busy session workspace lock names the session, never the lock path', t => { const { root, dir } = scratch(t); const ws = join(dir, 's1'); writeFileSync(`${ws}.lock`, 'not json'); assert.throws(() => lockSessionWorkspace(ws, {}), coded('session workspace s1 is busy: its lock is unreadable', root, 'session-workspace-busy')); }); test('a data root too long for a socket names the run, never its directory', t => { const { root, dir } = scratch(t); assert.throws(() => newRun(join(dir, 'a'.repeat(70)), 'acme'), error => { assert.ok(error instanceof SeatError, error.stack); assert.equal(error.exitCode, 2); assert.match(error.message, /^data root path too long for a socket: run r[0-9a-f]{12}$/); assert.ok(!error.message.includes(root), error.message); return true; }); });