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 { validateBinding } from '../src/binding.mjs'; import { createGateway } from '../src/gateway.mjs'; import { readTurn, listTurns } from '../src/journal.mjs'; import { DiscordError } from '../src/errors.mjs'; import { rawBinding } from './helpers.mjs'; for (const [field, edit] of [ ['guildId', d => { d.guildId = secret; }], ['name', d => { d.name = secret + '/'; }], ['path', d => { d.tools = { roots: [{ name: 'root', path: `/x/.${secret}`, write: false }] }; }], ]) test(`binding ${field} refusal omits its configured value`, () => { const d = rawBinding(); edit(d); assert.throws(() => validateBinding(d), safe(field, DiscordError, { exitCode: 2 })); }); test('gateway url refusal omits its value', () => assert.throws(() => createGateway({ url: secret, token: 'fake' }), safe('url', DiscordError, { exitCode: 1 }))); for (const mode of ['read', 'list']) test(`turn ${mode} parser omits file bytes and keeps SyntaxError`, t => { const root = join(scratch(t), secret); mkdirSync(join(root, 'turns'), { recursive: true }); const file = join(root, 'turns', 'turn-1.json'); writeFileSync(file, secret); // The refusal names the turn by its id, never by host path (PR #1916). assert.throws(() => mode === 'read' ? readTurn(root, 'turn-1') : listTurns(root), safe('turn record turn-1 is not valid JSON', SyntaxError, {}, file)); }); for (const duplicate of [true, false]) test(`binding approver ${duplicate ? 'duplicate' : 'invalid'} name omits user values`, t => { const root = scratch(t), keyFile = join(root, 'fixture-key'); writeFileSync(keyFile, 'fixture\n', { mode: 0o600 }); const name = secret.toLowerCase() + (duplicate ? '' : '\u0001'); const users = [{ id: '100000000000000100', name }]; if (duplicate) users.push({ id: '100000000000000101', name }); const raw = rawBinding({ users, tools: { roots: [{ name: 'docs', path: root }], setspark: { baseUrl: 'https://api.setspark.io', keyFile, principal: 'sage' } } }); assert.throws(() => validateBinding(raw), safe(duplicate ? 'users' : 'approvers', DiscordError, { exitCode: 2 }, secret.toLowerCase())); });