Files
stack/packages/seat/tests/refusal-redaction.test.mjs
jason.woltje f3f33a6171 fix(organization,cli,discord,seat,control-board): refusals name the field or file by label, never a host path (#1873)
redact6. A refusal names the field, the file or the instance. It never quotes a
config value, a host path, or a raw e.message or stderr that carries one.

- organization: checkCredentialRef names the field and each forbidden root by
  label; OrgError keeps .path only for a document field ($...); the registry,
  fence and lock refusals use the quiet helpers (call and code).
- cli: loadSystem never relays mosaic-config stderr: instance refusals keep
  their code and names, everything else reads as a field and its constraint
  against a labelled config. Project-key, access warning, halt gate, sessions
  file, launch socket, harness relays and the workspace rename name labels.
  Launch-log reasons read a Node system error as its call and code.
- scripts/mosaic-config: configuration not found names where the config came from.
- discord, seat, control-board: config, token, context, tool-root, journal,
  binding, registration and seen-marks refusals name labels.

Exit codes, refusal codes and check order are unchanged. Each new or changed
test fails on BASE be698e602.
2026-11-11 08:53:31 -06:00

48 lines
2.5 KiB
JavaScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
const secret = 'sk-test-NOT-A-SECRET-1234';
// JSON.parse quotes a prefix near this fault on Node 26; check partial leaks too.
const malformed = secret;
// #1873: the sentinel is a path component, so a refusal that quotes a path quotes it too.
function scratch(t) {
const root = mkdtempSync(join(tmpdir(), 'r3-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
const dir = join(root, secret);
mkdirSync(dir);
return dir;
}
function write(file, text = malformed) {
mkdirSync(dirname(file), { recursive: true });
writeFileSync(file, text, { mode: 0o600 });
return file;
}
function safe(label, ErrorType, properties = {}) {
return error => {
assert.ok(error instanceof ErrorType);
for (const [key, value] of Object.entries(properties)) assert.equal(error[key], value);
assert.ok(error.message.includes(label), error.message);
assert.ok(!error.message.includes(tmpdir()), error.message);
assert.doesNotMatch(error.message, /sk-test|Unexpected token/);
assert.doesNotMatch(error.stack, /sk-test/);
assert.equal(error.cause, undefined);
return true;
};
}
import { SeatError, loadDataRoot, readRegistration, registrationPath } from '../src/seat.mjs';
for (const kind of ['config', 'registration']) test(`${kind} JSON refusal names only the file`, t => {
const root = scratch(t), file = write(kind === 'config' ? join(root, 'config.json') : registrationPath(root, 'builder', 'repo'));
assert.throws(() => kind === 'config' ? loadDataRoot(file) : readRegistration(root, 'builder', 'repo'), safe(kind === 'config' ? 'config is not valid JSON' : 'registration repo/builder is not valid JSON', SeatError, { exitCode: 2 }));
});
for (const kind of ['config', 'registration']) test(`${kind} read refusal retains the filesystem code`, t => {
const root = scratch(t), file = kind === 'config' ? join(root, 'config.json') : registrationPath(root, 'builder', 'repo');
mkdirSync(file, { recursive: true });
assert.throws(() => kind === 'config' ? loadDataRoot(file) : readRegistration(root, 'builder', 'repo'), error => {
safe(kind === 'config' ? 'config is not valid JSON' : 'registration repo/builder is not valid JSON', SeatError, { exitCode: 2 })(error);
assert.match(error.message, /\(EISDIR\)$/);
return true;
});
});