import { test } from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; const secret = 'sk-test-NOT-A-SECRET-1234'; function scratch(t) { const root = mkdtempSync(join(tmpdir(), 'r4-')); t.after(() => rmSync(root, { recursive: true, force: true })); return root; } function safe(label, Type, props = {}, omitted = secret) { return error => { assert.ok(error instanceof Type, error.stack); for (const [key, value] of Object.entries(props)) assert.equal(error[key], value); assert.ok(error.message.includes(label), error.message); assert.doesNotMatch(error.message + error.stack, /sk-test|NOT-A-SECRET|Unexpected token/); assert.ok(!error.message.includes(omitted), error.message); assert.equal(error.cause, undefined); return true; }; } import { spawnSync } from 'node:child_process'; import { EXIT } from '../src/runner.mjs'; const runner = join(import.meta.dirname, '../src/runner.mjs'); for (const at of ['session.json', 'stdin', 'policy.json']) test(`runner ${at} failure omits parser bytes and keeps usage exit`, t => { const root = scratch(t), policy = join(root, 'policy.json'); writeFileSync(join(root, 'session.json'), at === 'session.json' ? secret : JSON.stringify({ bundle: { policy } })); writeFileSync(policy, at === 'policy.json' ? secret : '{}'); const r = spawnSync(process.execPath, [runner, root], { encoding: 'utf8', input: at === 'stdin' ? secret + '\n' : JSON.stringify({ cap: 'a'.repeat(64) }) + '\n', env: { PATH: process.env.PATH, HOME: process.env.HOME, TMPDIR: process.env.TMPDIR } }); assert.equal(r.status, EXIT.usage, r.stderr); assert.ok(r.stderr.includes(at), r.stderr); assert.doesNotMatch(r.stdout + r.stderr, /sk-test|NOT-A-SECRET|Unexpected token/); }); test('runner keeps a read errno without relaying its message', t => { const root = scratch(t); mkdirSync(join(root, 'session.json')); const r = spawnSync(process.execPath, [runner, root], { encoding: 'utf8', input: '', env: { PATH: process.env.PATH, HOME: process.env.HOME, TMPDIR: process.env.TMPDIR } }); assert.equal(r.status, EXIT.usage); assert.match(r.stderr, /session.json/); assert.match(r.stderr, /runner: EISDIR:/); }); test('runner tools parser keeps SyntaxError without file bytes', t => { const root = scratch(t), policy = join(root, 'policy.json'), tools = join(root, 'tools.json'); writeFileSync(join(root, 'session.json'), JSON.stringify({ bundle: { policy, tools }, turnEnd: 'exit', harnessEnv: {} })); writeFileSync(policy, '{"needs":[]}'); writeFileSync(tools, secret); // Isolate the claim boundary: this test exercises the real post-claim file parser, // and intentionally never starts a broker, adapter or tool server. const code = `import { Readable } from 'node:stream'; import { Client } from ${JSON.stringify(new URL('../../bus/src/client.mjs', import.meta.url).href)}; import { main } from ${JSON.stringify(new URL('../src/runner.mjs', import.meta.url).href)}; Client.prototype.call = async () => ({}); try { await main(${JSON.stringify(root)}, { stdin: Readable.from([JSON.stringify({ cap: 'a'.repeat(64) }) + '\\n']), env: {}, log() {} }); } catch (e) { if (!(e instanceof SyntaxError)) throw e; console.error(e.stack); process.exitCode = 9; }`; const r = spawnSync(process.execPath, ['--input-type=module', '-e', code], { encoding: 'utf8', env: { PATH: process.env.PATH, HOME: process.env.HOME, TMPDIR: process.env.TMPDIR } }); assert.equal(r.status, 9, r.stderr); assert.match(r.stderr, /tools.json/); assert.doesNotMatch(r.stderr, /sk-test|NOT-A-SECRET|Unexpected token/); });