Files
stack/packages/discord/tests/last-values.test.mjs
jason.woltje 4c8bf9ca4e fix(organization,cli): refusals name the file by label, never by host path
PR #1916 rule: a refusal names the field or file, never a config value
or a host path. readJsonFile's three refusals and parseJsonFileText now
name the file by its `what` label only. Callers whose label alone is
ambiguous add a name that already passed a strict pattern (business id,
role name, org id, turn id, project key) or a fixed path under the data
root. Exit codes, refusal codes and check order are unchanged.

organization: util, access, project, config-command, library-seats,
org-service deleteOrg receipt, agent-levels regex; T1 files role.mjs and
organization-config.mjs change only the readJsonFile label.
cli: access lock, accounts owner, host state/lock, notifier journal,
notify-config, run-evidence, dev-fixture, git-socket, org-bus, org-console.
discord: turn record label names the turn id.

Sentinel tests (sk-test-NOT-A-SECRET-1234 as a path component) cover each
changed refusal and fail on BASE; old path pins updated.
2026-11-11 01:04:15 -06:00

45 lines
2.9 KiB
JavaScript

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()));
});