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; function scratch(t) { const root = mkdtempSync(join(tmpdir(), 'r3-')); t.after(() => rmSync(root, { recursive: true, force: true })); return root; } function write(file, text = malformed) { mkdirSync(dirname(file), { recursive: true }); writeFileSync(file, text, { mode: 0o600 }); return file; } import { spawnSync } from 'node:child_process'; import { readFileSync } from 'node:fs'; import { fixture } from './helpers.mjs'; const repo = join(import.meta.dirname, '../../..'); test('task file JSON refusal preserves exit 2 and file without parser bytes', t => { const file = write(join(scratch(t), 'task.json')); const r = spawnSync(process.execPath, [join(repo, 'scripts/mosaic-task.mjs'), 'validate', file], { encoding: 'utf8', env: { PATH: process.env.PATH, HOME: process.env.HOME, TMPDIR: process.env.TMPDIR } }); assert.equal(r.status, 2, r.stderr); assert.ok(r.stderr.includes(file), r.stderr); assert.match(r.stderr, /task file is not valid JSON/); assert.doesNotMatch(r.stdout + r.stderr, /sk-test|Unexpected token/); }); test('task file read refusal retains exit 2, file and filesystem code', t => { const file = write(join(scratch(t), 'task.json'), '{}'); // A directory is refused by lstat before the read. Inject a read failure // after that unchanged regular-file gate, including on root-run CI hosts. const preload = `import fs from 'node:fs'; const read = fs.readFileSync; fs.readFileSync = function(path, ...args) { if (path === ${JSON.stringify(file)}) throw Object.assign(new Error(${JSON.stringify(secret)}), { code: 'EIO' }); return read.call(this, path, ...args); };`; const r = spawnSync(process.execPath, ['--import', 'data:text/javascript,' + encodeURIComponent(preload), join(repo, 'scripts/mosaic-task.mjs'), 'validate', file], { encoding: 'utf8', env: { PATH: process.env.PATH, HOME: process.env.HOME, TMPDIR: process.env.TMPDIR } }); assert.equal(r.status, 2, r.stderr); assert.ok(r.stderr.includes(file), r.stderr); assert.match(r.stderr, /task file is not valid JSON.*\(EIO\)/); assert.doesNotMatch(r.stderr, /sk-test/); }); test('case2 precheck business helper omits parser bytes in its BUSINESS fact', t => { const root = scratch(t), f = fixture(root); const file = write(join(root, 'config/businesses/acme.json')); // Execute the production facts helper, without host or credential checks. const shell = readFileSync(join(repo, 'scripts/case2-precheck.sh'), 'utf8'); const start = shell.indexOf('facts="$(cd "$REPO" && node --input-type=module -e \'') + 'facts="$(cd "$REPO" && node --input-type=module -e \''.length; assert.ok(start > 100); const end = shell.indexOf("' \"$REPO\" \"$BUSINESS\"", start); assert.ok(end > start); const r = spawnSync(process.execPath, ['--input-type=module', '-e', shell.slice(start, end), repo, 'acme'], { encoding: 'utf8', env: { ...f.env, HOME: process.env.HOME, TMPDIR: process.env.TMPDIR, PATH: process.env.PATH } }); assert.equal(r.status, 0, r.stderr); assert.ok(r.stdout.includes(`BUSINESS\tbad\tcannot read ${file}:`), r.stdout); assert.doesNotMatch(r.stdout + r.stderr, /sk-test|Unexpected token/); });