Files
stack/packages/control-board/tests/path-redaction-labels.test.mjs
jason.woltje 809b1b0100 test(control-board): board API body carries the seen marks label, not the path (#1873)
The board API returns a scan refusal in its JSON error body. The test
starts a board on port 0 over a seen.json that is not an object and
checks the body names seen.json by label, with no sentinel or scratch
path. It fails on BASE, where the body quotes the host path.
2026-11-11 08:53:31 -06:00

99 lines
4.9 KiB
JavaScript

// #1873 (redact6): a control-board refusal names the config, the seen marks file or the seat
// 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 { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { ConfigError, loadConfig, loadRegistrations, loadSeen } from '../src/scan.mjs';
import { readRegistration, registrationPath, SeatError } from '../../seat/src/seat.mjs';
import { startServer } from '../src/serve.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 clean(text, root) {
assert.ok(!text.includes(root), text);
assert.doesNotMatch(text, /sk-test|NOT-A-SECRET/);
}
function labelled(label, root) {
return error => {
assert.ok(error instanceof ConfigError, error.stack);
assert.equal(error.message, label);
clean(error.message, root);
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(() => loadConfig(join(dir, 'config.json')), labelled('config not found', root));
assert.throws(() => loadConfig(write(join(dir, 'array.json'), '[]')), labelled('config is not an object', root));
assert.throws(() => loadConfig(write(join(dir, 'relative.json'), '{"dataRoot":"data"}')), labelled('config.dataRoot must be an absolute path', root));
});
test('seen marks refusals name seen.json, not its path', t => {
const { root, dir } = scratch(t);
write(join(dir, 'seen.json'), '[]');
assert.throws(() => loadSeen(dir), labelled('seen marks file seen.json must be a JSON object', root));
write(join(dir, 'seen.json'), '{"a":1}');
assert.throws(() => loadSeen(dir), labelled('seen marks file seen.json has a non-string value for "a"', root));
});
test('a bad registration is reported by layout and seat, never by its path', t => {
const { root, dir } = scratch(t);
write(registrationPath(dir, 'builder', 'repo'), '{}');
const { registrations, errors } = loadRegistrations(dir);
assert.deepEqual(registrations, []);
assert.equal(errors.length, 1);
assert.match(errors[0], /^seat repo\/builder: .*: registration repo\/builder$/);
clean(errors[0], root);
});
// Review round (#1873): a key or name that could be a host path is described, never echoed.
test('a seen marks key or a registration field that is not a plain name is described, never echoed', t => {
const { root, dir } = scratch(t);
writeFileSync(join(dir, 'seen.json'), JSON.stringify({ [join(dir, 'host')]: 1 }));
assert.throws(() => loadSeen(dir), labelled('seen marks file seen.json has a non-string value for that is not a plain name', root));
const f = registrationPath(dir, 'builder', 'repo');
mkdirSync(dirname(f), { recursive: true });
writeFileSync(f, JSON.stringify({ [join(dir, 'host')]: 1 }));
assert.throws(() => readRegistration(dir, 'builder', 'repo'), e => {
assert.ok(e instanceof SeatError, e.stack);
assert.equal(e.exitCode, 2);
assert.equal(e.message, 'registration has an unknown field: that is not a plain name: registration repo/builder');
clean(e.message, root);
return true;
});
});
test('an invalid seat name or layout states the constraint, never the value, and keeps exit 4', t => {
const { root, dir } = scratch(t);
assert.throws(() => registrationPath(dir, join(dir, 'x'), 'repo'), e => e instanceof SeatError && e.exitCode === 4 && e.message === 'invalid seat name (lowercase letters, digits, ., _ or -, at most 64)' && (clean(e.message, root), true));
assert.throws(() => registrationPath(dir, 'builder', join(dir, 'x')), e => e instanceof SeatError && e.exitCode === 4 && e.message === 'invalid layout (one of repo, fleet, unknown)' && (clean(e.message, root), true));
});
// The board API returns a scan refusal in its JSON body, so the HTTP body carries the label too.
test('a seen marks refusal reaches the board API body by label, never by path', async t => {
const { root, dir } = scratch(t);
write(join(dir, 'seen.json'), '[]');
const server = await startServer({ host: '127.0.0.1', port: 0, specs: [], boardDir: dir, isAlive: () => true, page: '<html></html>' });
t.after(() => new Promise(done => server.close(done)));
const res = await fetch(`http://127.0.0.1:${server.address().port}/api/board`);
const body = await res.text();
assert.equal(res.status, 500);
assert.deepEqual(JSON.parse(body), { error: 'seen marks file seen.json must be a JSON object' });
clean(body, root);
});