Files
stack/packages/bus/tests/citation-guard.test.mjs

205 lines
14 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { connect } from 'node:net';
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { Client } from '../src/client.mjs';
import { refusalGuidance } from '../src/refusals.mjs';
import { serve } from '../src/server.mjs';
import { FIXED_TEXTS, GuardError } from '../../library/src/guard.mjs';
import { git } from '../../library/tests/helpers.mjs';
import { organizationWriter, setReplyLimits } from '../../organization/src/organization.mjs';
import { businessDoc, rolesCopy, writeJson } from '../../organization/tests/helpers.mjs';
import { resolveReplyLimits } from '../../library/src/limits.mjs';
import { citationWorld, citedBody, fixtureCite, guardCases } from './citation-helpers.mjs';
for (const definition of ['coder', 'research']) test(`C2-06 ${definition} send_message guard covers every table refusal; inserts nothing`, async t => {
const f = citationWorld(t, { definition }); f.call(f.agent, 'role.claim');
writeFileSync(f.settings, JSON.stringify({ replyLimits: { requireCitation: true } }));
for (const [code, body] of guardCases(f.commit)) {
const count = f.store.get('SELECT count(*) n FROM messages').n;
await assert.rejects(f.call(f.agent, 'message.send', { to: 'human', body }), e => e.code === code && e.reason.length <= 300);
assert.equal(f.store.get('SELECT count(*) n FROM messages').n, count, code);
assert.equal(f.store.all("SELECT * FROM events WHERE json_extract(body,'$.action')='message.checked'").length, 0);
}
const sent = await f.call(f.agent, 'message.send', { to: 'human', body: citedBody([fixtureCite(f.commit)]) });
const row = f.store.get("SELECT body,subject FROM events WHERE json_extract(body,'$.action')='message.checked'");
assert.equal(row.subject, sent.id);
assert.deepEqual(JSON.parse(row.body).check, { citations: [{ scope: 'canon', agent: null, file: f.commit }] });
});
test('C2-06 stored check carries the file author trailer at an unrelated cited tip', async t => {
const f = citationWorld(t, { definition: 'research' }); f.call(f.agent, 'role.claim');
const path = 'Book One/Characters/Mara Vell.md';
writeFileSync(join(f.repo, path), readFileSync(join(f.repo, path), 'utf8') + 'A fixture note.\n');
git(f.repo, 'add', '-A'); git(f.repo, 'commit', '-qm', 'Fixture note\n\nMosaic-Agent: acme/scribe');
const file = git(f.repo, 'rev-parse', 'HEAD');
writeFileSync(join(f.repo, 'Inbox Note.md'), 'Unrelated fixture.\n'); git(f.repo, 'add', '-A'); git(f.repo, 'commit', '-qm', 'unrelated');
const cited = git(f.repo, 'rev-parse', 'HEAD'); f.setServed(cited);
assert.notEqual(cited, file);
const sent = await f.call(f.agent, 'message.send', { to: 'human', body: citedBody([fixtureCite(cited)]) });
const event = f.store.get("SELECT body FROM events WHERE subject=? AND json_extract(body,'$.action')='message.checked'", sent.id);
assert.deepEqual(JSON.parse(event.body).check, { citations: [{ scope: 'canon', agent: 'acme/scribe', file }] });
const [row] = await f.call(f.human, 'thread.messages', { thread: f.thread.id });
assert.deepEqual(row.check.citations, [{ scope: 'canon', agent: 'acme/scribe', file, last: file }]);
});
const supportingMapping = entry => {
entry.universes.harbor.supporting = ['Book One/Playground/'];
return entry;
};
test('C2-06 supporting cite cannot state canon, including a folder also marked playground', async t => {
const f = citationWorld(t, { definition: 'research', mapChange: supportingMapping }); f.call(f.agent, 'role.claim');
const count = f.store.get('SELECT count(*) n FROM messages').n;
await assert.rejects(f.call(f.agent, 'message.send', { to: 'human', body: citedBody([fixtureCite(f.commit, {
path: 'Book One/Playground/What If.md', lines: [4, 4], quote: 'Mara Vell', as: 'canon',
})]) }), { code: 'playground-cited-as-canon' });
assert.equal(f.store.get('SELECT count(*) n FROM messages').n, count);
});
test('C2-06 supporting file stores supporting scope and its real agent trailer', async t => {
const f = citationWorld(t, { definition: 'research', mapChange: supportingMapping }); f.call(f.agent, 'role.claim');
const path = 'Book One/Playground/What If.md';
writeFileSync(join(f.repo, path), readFileSync(join(f.repo, path), 'utf8') + 'Agent fixture note.\n');
git(f.repo, 'add', '-A'); git(f.repo, 'commit', '-qm', 'Supporting note\n\nMosaic-Agent: acme/scribe');
const file = git(f.repo, 'rev-parse', 'HEAD'); f.setServed(file);
const sent = await f.call(f.agent, 'message.send', { to: 'human', body: citedBody([fixtureCite(file, { path, lines: [4, 4], quote: 'Mara Vell' })]) });
const event = f.store.get("SELECT body FROM events WHERE subject=? AND json_extract(body,'$.action')='message.checked'", sent.id);
assert.deepEqual(JSON.parse(event.body).check, { citations: [{ scope: 'supporting', agent: 'acme/scribe', file }] });
});
// C2-15: a hybrid over harbor and second that excludes harbor's Places folder (path form).
const crossMapping = entry => {
entry.universes.cross = { kind: 'hybrid', name: 'Cross', sources: ['harbor', 'second'], books: ['Crossover'],
constraints: [{ exclude: 'Book One/Places' }] };
return entry;
};
test('C2-15 excluded cite beside a hybrid refuses and inserts nothing', async t => {
const f = citationWorld(t, { mapChange: crossMapping }); f.call(f.agent, 'role.claim');
mkdirSync(join(f.repo, 'Crossover/Chapters'), { recursive: true });
writeFileSync(join(f.repo, 'Crossover/Chapters/01.md'), 'A hybrid fixture.\n');
git(f.repo, 'add', '-A'); git(f.repo, 'commit', '-qm', 'hybrid fixture');
const commit = git(f.repo, 'rev-parse', 'HEAD'); f.setServed(commit);
const hybrid = fixtureCite(commit, { path: 'Crossover/Chapters/01.md', lines: [1, 1], quote: 'hybrid fixture' });
const hidden = fixtureCite(commit, { path: 'Book One/Places/Harbor Town.md', lines: [4, 4], quote: 'Salt.' });
const count = f.store.get('SELECT count(*) n FROM messages').n;
await assert.rejects(f.call(f.agent, 'message.send', { to: 'human', body: citedBody([hybrid, hidden]) }),
e => e.code === 'citation-cross-universe' && e.reason === 'citation 2 (Book One/Places/Harbor Town.md) is excluded from hybrid cross');
assert.equal(f.store.get('SELECT count(*) n FROM messages').n, count);
assert.equal(f.store.all("SELECT * FROM events WHERE json_extract(body,'$.action')='message.checked'").length, 0);
// The hybrid beside source canon it can see goes through.
await f.call(f.agent, 'message.send', { to: 'human', body: citedBody([hybrid, fixtureCite(commit)]) });
assert.equal(f.store.get('SELECT count(*) n FROM messages').n, count + 1);
});
test('C2-06 check evidence and insert are atomic when storing evidence fails', async t => {
const f = citationWorld(t); f.call(f.agent, 'role.claim');
f.store.run(`CREATE TRIGGER fixture_check_refused BEFORE INSERT ON events
WHEN json_extract(NEW.body,'$.action')='message.checked' BEGIN SELECT RAISE(ABORT,'fixture check failure'); END`);
await assert.rejects(f.call(f.agent, 'message.send', { to: 'human', body: citedBody([fixtureCite(f.commit)]) }));
assert.equal(f.store.get('SELECT count(*) n FROM messages').n, 0);
});
test('C2-06 live limits change both directions; unreadable file keeps checks and exact fixed texts', async t => {
const f = citationWorld(t); f.call(f.agent, 'role.claim');
const send = body => f.call(f.agent, 'message.send', { to: 'human', body });
await send('free');
writeFileSync(f.settings, JSON.stringify({ replyLimits: { requireCitation: true } }));
await assert.rejects(send('free'), { code: 'citation-missing' });
writeFileSync(f.settings, JSON.stringify({ replyLimits: { requireCitation: false } })); await send('free');
for (const invalid of ['{broken', 'null', '[]']) {
writeFileSync(f.settings, invalid);
const before = f.store.get('SELECT count(*) n FROM messages').n;
await assert.rejects(send('free'), { code: 'citation-missing' });
await assert.rejects(send(citedBody(Array(21).fill(fixtureCite(f.commit)))), { code: 'citations-over-limit' });
assert.equal(f.store.get('SELECT count(*) n FROM messages').n, before);
for (const text of Object.values(FIXED_TEXTS)) await send(text);
assert.equal((await f.call(f.human, 'library.status', { project: 'books' })).replyLimits.problem, 'reply-limits-unreadable');
}
rmSync(f.settings);
await assert.rejects(send('free'), { code: 'citation-missing' });
});
test('C2-06 page store is session-local, caps 20 fetches and 200 KiB, drops at disconnect', async t => {
const f = citationWorld(t, { definition: 'research' }); f.call(f.agent, 'role.claim');
const fetch = async (n, text = `page ${n}`) => f.broker.requestWeb(f.agent, { verb: 'web.fetch', args: { url: `https://fixture.invalid/${n}` } },
{ handle: async () => ({ result: { text }, summary: { outcome: 'ok' } }) });
const send = (n, quote) => f.call(f.agent, 'message.send', { to: 'human', body: citedBody([{ url: `https://fixture.invalid/${n}`, quote }]) });
await fetch(0); await send(0, 'page 0');
for (let n = 1; n <= 20; n++) await fetch(n);
await assert.rejects(send(0, 'page 0'), { code: 'citation-unfetched' }); await send(1, 'page 1');
await fetch(21, 'x'.repeat(200 * 1024) + 'AFTER_CAP');
await send(21, 'xxxx'); await assert.rejects(send(21, 'AFTER_CAP'), { code: 'citation-unfetched' });
f.broker.disconnect(f.agent);
f.broker.endLaunch({ business: 'acme', run: 'coder-run', reason: 'fixture', exitCode: 0 });
const replacement = f.broker.bindLaunch({ ...f.launch, run: 'next-run' });
f.call(replacement, 'role.claim');
await assert.rejects(f.call(replacement, 'message.send', { to: 'human', body: citedBody([{ url: 'https://fixture.invalid/21', quote: 'xxxx' }]) }), { code: 'citation-unfetched' });
});
test('C2-06 transport passes safe capped guard reason; ordinary refusal retains its code and adds static rail guidance', async t => {
const f = citationWorld(t, { guard: async () => { throw new GuardError('citation-invalid', 'guard reason '.repeat(50)); } });
f.call(f.agent, 'role.claim'); const path = join(f.root, 'socket');
const server = await serve({ broker: f.broker, path }); t.after(() => server.close());
const client = new Client({ path, cap: f.agent });
await assert.rejects(client.call('message.send', { to: 'human', body: 'fixture' }), e => e.code === 'citation-invalid' && e.reason === ('guard reason '.repeat(50)).slice(0, 300));
const raw = await new Promise((resolve, reject) => {
const socket = connect(path); let text = ''; socket.on('error', reject); socket.on('data', b => { text += b; }); socket.on('end', () => resolve(text));
socket.on('connect', () => socket.write(JSON.stringify({ cap: f.agent, verb: 'unknown-fixture' }) + '\n'));
});
assert.deepEqual(JSON.parse(raw), { ok: false, error: 'unknown-verb', ...refusalGuidance('unknown-verb') });
});
test('C2-06 non-library businesses retain role.claim shape and unguarded free sends', async t => {
const f = citationWorld(t, { library: false });
assert.deepEqual(f.call(f.agent, 'role.claim'), { role: 'coder', run: 'coder-run' });
assert.ok(f.call(f.agent, 'message.send', { to: 'human', body: 'free' }).id);
});
test('C2-06 thread messages carry file/last evidence with one read per distinct path', async t => {
const f = citationWorld(t, { definition: 'research' }); f.call(f.agent, 'role.claim');
const path = 'Book One/Characters/Mara Vell.md';
await f.call(f.agent, 'message.send', { to: 'human', body: citedBody([fixtureCite(f.commit), fixtureCite(f.commit)]) });
await f.call(f.agent, 'message.send', { to: 'human', body: citedBody([fixtureCite(f.commit)]) });
const rows = await f.call(f.human, 'thread.messages', { thread: f.thread.id });
assert.equal(rows.length, 2); assert.deepEqual(f.reads, [path]);
for (const row of rows) for (const c of row.check.citations) {
assert.equal(c.file, f.commit); assert.equal(c.last, f.commit); assert.equal(c.agent, null);
}
writeFileSync(join(f.repo, path), 'A changed fixture.\n'); git(f.repo, 'add', '-A'); git(f.repo, 'commit', '-qm', 'changed fixture');
const changed = git(f.repo, 'rev-parse', 'HEAD'); f.setServed(changed); f.reads.length = 0;
const newer = await f.call(f.human, 'thread.messages', { thread: f.thread.id });
assert.deepEqual(f.reads, [path]); assert.equal(newer[0].check.citations[0].file, f.commit); assert.equal(newer[0].check.citations[0].last, changed);
});
test('C2-06 guarded request snapshot cannot change between check and insert', async t => {
let finish;
const f = citationWorld(t, { guard: () => new Promise(r => { finish = r; }) }); f.call(f.agent, 'role.claim');
const request = { verb: 'message.send', args: { to: 'human', body: 'checked fixture' } };
const sent = f.broker.request(f.agent, request); request.args.body = 'unchecked replacement';
finish(null); await sent;
assert.equal(f.store.get("SELECT body FROM messages WHERE from_role='coder'").body, 'checked fixture');
});
test('C2-06 library.limits.set is human-only and invalid settings leave file and receipts unchanged', async t => {
let writer;
const f = citationWorld(t, { organization: (id, verb, args, actor) => {
assert.equal(verb, 'library.limits.set'); return setReplyLimits(writer, id, args, actor);
} });
const dir = join(f.root, 'config'), file = writeJson(join(dir, 'businesses/acme.json'), businessDoc(f.root));
writer = organizationWriter({ dir, rolesDir: rolesCopy(f.root) });
f.adapter.replyLimits = () => resolveReplyLimits(writer.readRaw('acme'));
f.call(f.agent, 'role.claim');
await f.call(f.human, 'library.limits.set', { revision: writer.read('acme').revision, replyLimits: { requireCitation: true } });
await assert.rejects(f.call(f.agent, 'message.send', { to: 'human', body: 'free' }), { code: 'citation-missing' });
const before = readFileSync(file), log = readFileSync(writer.logFile('acme'));
const args = { revision: writer.read('acme').revision, replyLimits: { citationsPerReply: 0 } };
for (const [cap, code] of [[f.human, 'invalid-request'], [f.agent, 'human-required'], [f.tester, 'tester-refused']]) {
assert.throws(() => f.call(cap, 'library.limits.set', args), { code });
assert.deepEqual(readFileSync(file), before); assert.deepEqual(readFileSync(writer.logFile('acme')), log);
}
assert.throws(() => f.call(f.human, 'library.limits.set', { ...args, replyLimits: { invented: true } }), { code: 'invalid-request' });
});