// 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/TI: each creative role has nine tools, or eleven 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, 'my_rails']; assert.deepEqual(tools.map(tool => tool.name), expected, definition); assert.equal(tools.length, ['companion', 'research'].includes(definition) ? 11 : 9); 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, 'my_rails']); });