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 { DiscordError } from '../src/errors.mjs'; import { loadDataRoot, loadBinding } from '../src/binding.mjs'; import { readApprovals } from '../src/approvals.mjs'; import { readInboxIds } from '../src/journal.mjs'; import extension from '../extension/tools.mjs'; import { TOOLS_ENV } from '../src/tools.mjs'; for (const [name, read, relative, label] of [ ['config', (file) => loadDataRoot(file), 'config.json', 'config is not valid JSON'], ['binding', (file) => loadBinding(file), 'binding.json', 'binding binding is not valid JSON'], ['approvals', (_file, root) => readApprovals(root), 'approvals.jsonl', 'approvals.jsonl:2: not valid JSON'], ['journal', (_file, root) => readInboxIds(root), 'inbox.jsonl', 'inbox.jsonl:2: not valid JSON'], ]) test(`${name} JSON refusal omits parser bytes`, t => { const root = scratch(t), ledger = name === 'approvals' || name === 'journal'; const file = write(join(root, relative), ledger ? '{}\n' + malformed : malformed); assert.throws(() => read(file, root), safe(label, DiscordError, { exitCode: 2 })); }); test('tools environment JSON refusal names the config key without its bytes', () => { const previous = process.env[TOOLS_ENV]; process.env[TOOLS_ENV] = malformed; try { assert.throws(() => extension({}), safe(TOOLS_ENV, Error)); } finally { if (previous === undefined) delete process.env[TOOLS_ENV]; else process.env[TOOLS_ENV] = previous; } }); test('config read refusal retains the filesystem code', t => { const file = scratch(t); assert.throws(() => loadDataRoot(file), error => { safe('config is not valid JSON', DiscordError, { exitCode: 2 })(error); assert.match(error.message, /\(EISDIR\)$/); return true; }); });