82 lines
3.9 KiB
JavaScript
82 lines
3.9 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { fixture, git } from '../../bus/tests/spec-fixtures.mjs';
|
|
import { serve } from '../../bus/src/server.mjs';
|
|
import { Client } from '../../bus/src/client.mjs';
|
|
import { toolServer } from '../src/runner.mjs';
|
|
import { callTool, typedTools } from '../src/tools.mjs';
|
|
|
|
const requirement = { recordVersion: 1, id: 'REQ-1', title: 'Title', text: 'Text', status: 'draft' };
|
|
const decision = { recordVersion: 1, id: 'DEC-1', question: 'Which?',
|
|
options: [{ key: 'yes', text: 'Yes' }, { key: 'no', text: 'No' }],
|
|
recommendation: 'yes', status: 'open', choice: null };
|
|
|
|
async function setup(t) {
|
|
const f = await fixture(t);
|
|
const busPath = join(f.root, 'bus.sock');
|
|
const bus = await serve({ broker: f.broker, path: busPath });
|
|
t.after(() => bus.close());
|
|
const client = new Client({ path: busPath, cap: f.coder });
|
|
const tools = typedTools({ resolved: { definition: 'coder', limits: { tools: [], authority: {
|
|
withinRole: ['record.write'], crossRole: [],
|
|
} } } });
|
|
const path = join(f.root, 'tools.sock');
|
|
const server = await toolServer({ path, tools, client, local: {} });
|
|
t.after(() => server.close());
|
|
return { ...f, write: (recordPath, record) => callTool(path, 'write_record', { project: 'stack', path: recordPath, record }) };
|
|
}
|
|
|
|
for (const [kind, record] of [['requirements', requirement], ['decisions', decision]]) {
|
|
test(`JSON string ${kind} records commit the same bytes as object records`, async t => {
|
|
const object = await setup(t), string = await setup(t);
|
|
const path = `${kind}/${record.id}.json`;
|
|
const written = await object.write(path, record);
|
|
const parsed = await string.write(path, JSON.stringify(record));
|
|
assert.match(parsed.commit, /^[0-9a-f]{40}$/);
|
|
const expected = git(object.repo, 'show', `${written.commit}:${path}`) + '\n';
|
|
assert.equal(readFileSync(join(string.repo, path), 'utf8'), expected);
|
|
assert.equal(git(string.repo, 'show', `${parsed.commit}:${path}`) + '\n', expected);
|
|
const doc = '# Markdown remains text\n';
|
|
await string.write('docs/record.md', doc);
|
|
assert.equal(readFileSync(join(string.repo, 'docs/record.md'), 'utf8'), doc);
|
|
await assert.rejects(string.write('docs/object.md', { text: 'not Markdown' }), /project-record-invalid/);
|
|
});
|
|
}
|
|
|
|
for (const [name, value, detail] of [
|
|
['unparseable', 'secret-marker {', /unparseable string/],
|
|
['array', '["secret-marker"]', /array/],
|
|
['number', '42', /number/],
|
|
['null', 'null', /null/],
|
|
]) {
|
|
test(`agent sees safe detail for ${name} JSON record`, async t => {
|
|
const f = await setup(t);
|
|
await assert.rejects(f.write('requirements/REQ-1.json', value), e => {
|
|
assert.match(e.message, /refused: project-record-invalid/);
|
|
assert.match(e.message, detail);
|
|
assert.doesNotMatch(e.message, /secret-marker/);
|
|
return true;
|
|
});
|
|
});
|
|
}
|
|
|
|
test('agent sees validation detail and static guidance without unrelated refusal reasons', async t => {
|
|
const f = await setup(t);
|
|
await assert.rejects(f.write('requirements/REQ-1.json', { ...requirement, title: '' }),
|
|
/refused: project-record-invalid: requirement\.title: too short/);
|
|
const refusal = message => error => {
|
|
assert.equal(error.message.split('\n')[0], message);
|
|
assert.match(error.message, /\nRail: .+\nNext step: .+/);
|
|
assert.doesNotMatch(error.message, /secret-marker/);
|
|
return true;
|
|
};
|
|
await assert.rejects(f.write('spec/secret-marker.md', '# text'), refusal('refused: project-spec-human-only'));
|
|
const path = join(f.root, 'other-tools.sock');
|
|
const server = await toolServer({ path, tools: [{ name: 'write_record', verb: 'record.write' }], local: {},
|
|
client: { call() { throw Object.assign(new Error('hidden'), { code: 'project-busy', reason: 'secret-marker' }); } } });
|
|
t.after(() => server.close());
|
|
await assert.rejects(callTool(path, 'write_record', {}), refusal('refused: project-busy'));
|
|
});
|