Files
stack/packages/harness/tests/spec-tools.test.mjs
jason.woltjeandClaude Opus 5.5 3190ad762b bus, project: file-backed north-star specs with spec.read and record.write (row 138, #1632)
Row 138 round 2, Rocko's candidate e3f9a400 (43 paths) on 26a0b0fab.
Dewey approved (comment 28754) after round 1 changes (comment 28752):
D1-D5 mutants killed, the 17 touched test files run singly passed
124/124. Rocko's own run: 753 tests and 59/59 mutants killed.

The bus gains the project adapter through runtime.mjs, the way it
already injects the organization, so broker.mjs takes no new imports
and refuses with project-not-configured when no adapter is set. The
project socket timeout is 120s. Roles gain record.write. The schema
moves from 3e to 3f; the live migration follows MIGRATION.md and
needs Mos's window.

Sage's landing gate ran on c842686d5 plus the candidate: 25 steps,
all rc 0 (15 packages, 10 script suites), network sage-r138_default
removed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-15 20:49:44 -05:00

48 lines
3.0 KiB
JavaScript

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