61 lines
3.9 KiB
JavaScript
61 lines
3.9 KiB
JavaScript
// Row 288 M3: creative write tools and independent closed-schema coverage.
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { typedTools, untypedActions } from '../src/tools.mjs';
|
|
import { resolvedFor } from './helpers.mjs';
|
|
|
|
const LOOKUPS = ['library_find', 'library_get', 'library_search', 'library_manuscript', 'library_universe'];
|
|
const WRITES = ['library_doc_write', 'library_doc_delete'];
|
|
const CREATIVE = ['companion', 'research', 'ideas', 'proofreader', 'fact-checker', 'continuity'];
|
|
const role = (t, definition) => resolvedFor(t, 'bean', doc => {
|
|
doc.roles.bean = { definition, credentials: { gitea: { ...doc.roles.coder.credentials.gitea } }, vars: { harness: 'pi', model: 'claude-sonnet-5-5', thinking: 'high' } };
|
|
});
|
|
|
|
test('C2-02 M3: each creative role has eight tools, or ten with its web grant', t => {
|
|
for (const definition of CREATIVE) {
|
|
const resolved = role(t, definition), tools = typedTools({ resolved, library: true, tracker: true, arbiter: true });
|
|
const expected = ['send_message', ...(['companion', 'research'].includes(definition) ? ['web_search', 'web_fetch'] : []), ...LOOKUPS, ...WRITES];
|
|
assert.deepEqual(tools.map(tool => tool.name), expected, definition);
|
|
assert.equal(tools.length, ['companion', 'research'].includes(definition) ? 10 : 8);
|
|
assert.equal(untypedActions(resolved, tools).includes('library.write'), false);
|
|
}
|
|
});
|
|
|
|
test('C2-02 schemas: both write tools require universe/path/base, close keys, map verb and action', t => {
|
|
const tools = typedTools({ resolved: role(t, 'ideas'), library: true });
|
|
for (const [name, verb, required] of [
|
|
['library_doc_write', 'library.doc.write', ['universe', 'path', 'base', 'content']],
|
|
['library_doc_delete', 'library.doc.delete', ['universe', 'path', 'base']],
|
|
]) {
|
|
const tool = tools.find(tool => tool.name === name);
|
|
assert.ok(tool, name); assert.equal(tool.verb, verb); assert.equal(tool.action, 'library.write');
|
|
assert.equal(tool.parameters.type, 'object'); assert.equal(tool.parameters.additionalProperties, false);
|
|
assert.deepEqual(tool.parameters.required, required);
|
|
assert.deepEqual(Object.keys(tool.parameters.properties).sort(), [...required].sort());
|
|
assert.equal(tool.parameters.properties.universe.type, 'string');
|
|
assert.equal(tool.parameters.properties.path.type, 'string');
|
|
assert.deepEqual(tool.parameters.properties.base.type, ['string', 'null']);
|
|
assert.equal(tool.parameters.properties.base.pattern, '^[a-f0-9]{40}$');
|
|
if (name === 'library_doc_write') {
|
|
assert.equal(tool.parameters.properties.content.type, 'string');
|
|
assert.match(tool.parameters.properties.base.description, /null to create/);
|
|
} else {
|
|
assert.match(tool.parameters.properties.base.description, /existing file/);
|
|
assert.doesNotMatch(tool.parameters.properties.base.description, /create/);
|
|
}
|
|
for (const field of ['repo', 'commit', 'business', 'accept', 'project', 'identity', 'author', 'credential', 'agent', 'message'])
|
|
assert.equal(Object.hasOwn(tool.parameters.properties, field), false, `${name} ${field}`);
|
|
}
|
|
});
|
|
|
|
test('C2-02 tools: no library, noncreative or no within-role write grant exposes neither write tool', t => {
|
|
const creative = role(t, 'ideas');
|
|
assert.equal(typedTools({ resolved: creative }).some(tool => WRITES.includes(tool.name)), false);
|
|
for (const instance of ['coder', 'reviewer', 'pm', 'chief'])
|
|
assert.equal(typedTools({ resolved: resolvedFor(t, instance), library: true }).some(tool => WRITES.includes(tool.name)), false);
|
|
const narrowed = structuredClone(creative);
|
|
narrowed.limits.authority.withinRole = narrowed.limits.authority.withinRole.filter(action => action !== 'library.write');
|
|
narrowed.limits.authority.crossRole.push('library.write');
|
|
assert.deepEqual(typedTools({ resolved: narrowed, library: true }).map(tool => tool.name), ['send_message', ...LOOKUPS]);
|
|
});
|