Files
stack/packages/harness/tests/library-tools.test.mjs

83 lines
5.2 KiB
JavaScript

// Row224 M3: exact creative tool sets preserve the existing C2-08 web grants.
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdirSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { typedTools } from '../src/tools.mjs';
import { buildBundle } from '../src/bundle.mjs';
import { TYPES } from '../../library/src/mapping.mjs';
import { HARNESS, REPO_SKILLS, resolvedFor, scratch } from './helpers.mjs';
const LOOKUPS = ['library_find', 'library_get', 'library_search', 'library_manuscript', 'library_universe'];
const CREATIVE = ['companion', 'research', 'ideas', 'proofreader', 'fact-checker', 'continuity'];
const role = (t, definition, harness = 'pi') => resolvedFor(t, 'bean', d => {
d.roles.bean = { definition, credentials: { gitea: { ...d.roles.coder.credentials.gitea } }, vars: { harness, model: 'claude-sonnet-5-5', thinking: 'high' } };
});
const expected = definition => ['send_message', ...(['companion', 'research'].includes(definition)
? ['web_search', 'web_fetch'] : []), ...LOOKUPS, 'library_doc_write', 'library_doc_delete'];
test('M3: per-role snapshots contain eight names, or ten with the C2-08 web grant', t => {
for (const definition of CREATIVE) {
const tools = typedTools({ resolved: role(t, definition), library: true, tracker: true, arbiter: true });
assert.deepEqual(tools.map(t => t.name), expected(definition), definition);
assert.ok(tools.every(t => t.parameters.additionalProperties === false));
}
});
test('every noncreative role gets lookups when a library is configured, not otherwise', t => {
for (const instance of ['coder', 'reviewer', 'pm', 'chief']) {
const resolved = resolvedFor(t, instance);
const ordinary = typedTools({ resolved }).map(t => t.name);
assert.ok(!ordinary.some(name => name.startsWith('library_')));
const tools = typedTools({ resolved, library: true });
assert.deepEqual(tools.filter(t => t.name.startsWith('library_')).map(t => t.name), LOOKUPS);
assert.deepEqual(tools.filter(t => !t.name.startsWith('library_')).map(t => t.name), ordinary);
}
});
test('lookup schemas require a universe even for get, include paging, and cannot select repo or commit', t => {
const tools = typedTools({ resolved: role(t, 'ideas'), library: true });
const required = [ ['universe', 'query'], ['universe', 'path'], ['universe', 'text'], ['universe', 'book'], ['universe'] ];
for (const [i, tool] of tools.filter(t => LOOKUPS.includes(t.name)).entries()) {
assert.deepEqual(tool.parameters.required, required[i]);
assert.equal(tool.verb, LOOKUPS[i].replace('_', '.'));
assert.equal(tool.action, null);
assert.equal(tool.parameters.additionalProperties, false);
for (const key of ['repo', 'commit', 'business', 'accept']) assert.equal(Object.hasOwn(tool.parameters.properties, key), false);
}
const byName = Object.fromEntries(tools.map(t => [t.name, t]));
assert.deepEqual(byName.library_find.parameters.properties.scope.enum, ['canon', 'playground', 'supporting']);
assert.deepEqual(byName.library_find.parameters.properties.type.enum, [...TYPES]);
assert.equal(byName.library_find.parameters.properties.offset.minimum, 0);
assert.equal(byName.library_get.parameters.properties.from.minimum, 1);
assert.equal(byName.library_get.parameters.properties.links_from.minimum, 0);
assert.equal(byName.library_manuscript.parameters.properties.chapter.type, 'string');
});
test('bundle flag reaches both harness tool manifests and creative prompt without generic tools', t => {
for (const definition of CREATIVE) for (const harness of ['pi', 'claude-code']) {
const dir = scratch(t), workspace = join(dir, 'ws'); mkdirSync(workspace);
const b = buildBundle({ dir: join(dir, 'bundle'), resolved: role(t, definition, harness), library: true,
contract: '# Creative\n', tracker: true, arbiter: true, skillsDir: REPO_SKILLS,
session: { run: 'r224', launchedBy: 'chief', workspace, toolSocket: join(dir, 't.sock'), turnMarker: join(dir, 'turn.done') },
harness: HARNESS[harness] });
assert.deepEqual(b.tools.map(t => t.name), expected(definition), `${definition} ${harness}`);
const serialized = JSON.parse(readFileSync(b.files.tools, 'utf8'));
assert.deepEqual(serialized.map(t => t.name), expected(definition));
const prompt = readFileSync(b.files.prompt, 'utf8');
assert.match(prompt, /library_find/);
assert.doesNotMatch(prompt, /Bus tools: [^\n]*(read_spec|list_agents|inbox|trail|write_record|raise_decision|list_tasks)/);
}
});
test('C2-05c scope schemas expand only find/search/get, keeping manuscript and tool names unchanged', t => {
const tools = typedTools({ resolved: role(t, 'ideas'), library: true });
assert.deepEqual(tools.map(t => t.name), ['send_message', ...LOOKUPS, 'library_doc_write', 'library_doc_delete']);
for (const name of ['library_find', 'library_search', 'library_get']) {
const tool = tools.find(t => t.name === name);
assert.deepEqual(tool.parameters.properties.scope.enum, ['canon', 'playground', 'supporting']);
assert.ok(!tool.parameters.required.includes('scope'));
}
assert.deepEqual(tools.find(t => t.name === 'library_manuscript').parameters.properties.scope.enum, ['canon', 'playground']);
});