import test from 'node:test'; import assert from 'node:assert/strict'; import { typedTools, callTool } from '../src/tools.mjs'; import { resolvedFor, fakeToolSocket } from './helpers.mjs'; import { fixture } from '../../bus/tests/spec-fixtures.mjs'; test('every role sees read_spec, record writer follows its grant, citation schemas stay strict', t => { for (const role of ['coder', 'reviewer', 'pm', 'chief']) { const tools = typedTools({ resolved: resolvedFor(t, role) }); const read = tools.find(t => t.name === 'read_spec'), write = tools.find(t => t.name === 'write_record'); assert.deepEqual([read.verb, read.action], ['spec.read', null]); assert.deepEqual(read.parameters.required, ['project']); assert.deepEqual(Object.keys(read.parameters.properties), ['project', 'commit']); assert.equal(read.parameters.additionalProperties, false); assert.deepEqual([write.verb, write.action], ['record.write', 'record.write']); assert.deepEqual(write.parameters.required, ['project', 'path', 'record']); assert.equal(write.parameters.additionalProperties, false); assert.equal(Object.hasOwn(write.parameters.properties, 'author'), false); assert.equal(new RegExp(write.parameters.properties.path.pattern).test('spec/north-star.md'), false); assert.equal(new RegExp(write.parameters.properties.path.pattern).test('docs/design.md'), true); const raise = tools.find(t => t.name === 'raise_decision').parameters; assert.equal(raise.additionalProperties, false); assert.equal(raise.properties.spec_path.const, 'spec/north-star.md'); assert.equal(new RegExp(raise.properties.spec_commit.pattern).test('a'.repeat(40)), true); assert.equal(raise.required.includes('spec_commit'), false); } const restricted = resolvedFor(t, 'coder', doc => { doc.roles.coder.vars['limits.authority'] = ['message.send']; }); const tools = typedTools({ resolved: restricted }); assert.ok(tools.some(t => t.name === 'read_spec')); assert.ok(!tools.some(t => t.name === 'write_record')); }); test('typed socket forwards read content/history and agent record writes, and cannot write a spec', async t => { const f = await fixture(t); const tools = typedTools({ resolved: { definition: 'coder', limits: { tools: [], authority: { withinRole: ['record.write'], crossRole: [], } } } }); const byName = new Map(tools.map(tool => [tool.name, tool])); const server = await fakeToolSocket(t, f.root, (name, args) => f.broker.request(f.coder, { verb: byName.get(name).verb, args })); const spec = await callTool(server.path, 'read_spec', { project: 'stack' }); assert.equal(spec.commit, f.commit); assert.equal(spec.log[0].commit, f.commit); assert.match(spec.content, /North.star/i); const record = await callTool(server.path, 'write_record', { project: 'stack', path: 'docs/tool.md', record: '# Tool\n' }); assert.match(record.commit, /^[0-9a-f]{40}$/); await assert.rejects(callTool(server.path, 'write_record', { project: 'stack', path: 'spec/north-star.md', record: '# Bad' }), /project-spec-human-only/); });