TODO Next: derive TOOL_SETS and MUTATING_TOOLS from tool definitions
Wrap every builtin tool with withMeta({set, mutating}) at its definition
site (src/tool-utils.ts). TOOL_SETS and MUTATING_TOOLS are now derived
via setsFrom/mutatingNames rather than hand-lists, so a new write cannot
be added ungated by forgetting a parallel list. Permission defaults now
cover the 5 extra mutating line-edit tools. Test suite covers coverage,
derived-equality, and mutating consistency (test/tool-derive.test.ts).
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
import { expect, test } from 'bun:test';
|
||||
import { DEFAULT_PERMISSIONS } from '../src/permission';
|
||||
import { tools, TOOL_SETS, MUTATING_TOOLS, TOOL_SET_NAMES } from '../src/tools';
|
||||
import { assertAllToolsInSets, mutatingNames, setsFrom, type ToolMeta } from '../src/tool-utils';
|
||||
|
||||
test('every builtin tool carries _meta at its definition site', () => {
|
||||
for (const [name, t] of Object.entries(tools as Record<string, { _meta?: ToolMeta }>)) {
|
||||
const meta = t._meta;
|
||||
expect(meta, `${name} has no _meta — wrap it with withMeta at the definition site`).toBeDefined();
|
||||
expect(TOOL_SET_NAMES as readonly string[]).toContain(meta!.set);
|
||||
expect(typeof meta!.mutating).toBe('boolean');
|
||||
}
|
||||
});
|
||||
|
||||
test('TOOL_SETS is derived from _meta and covers every builtin tool exactly once', () => {
|
||||
const derived = setsFrom(tools as Record<string, { _meta?: ToolMeta }>);
|
||||
expect(TOOL_SETS).toEqual(derived);
|
||||
|
||||
const missing = assertAllToolsInSets(tools, TOOL_SETS);
|
||||
expect(missing).toEqual([]);
|
||||
|
||||
// no tool belongs to two sets
|
||||
const all = Object.values(TOOL_SETS).flat();
|
||||
expect(new Set(all).size).toBe(all.length);
|
||||
expect(all.length).toBe(Object.keys(tools).length);
|
||||
|
||||
// every name in a set is a real tool
|
||||
for (const name of all) expect(tools).toHaveProperty(name);
|
||||
});
|
||||
|
||||
test('MUTATING_TOOLS is exactly the set of tools with _meta.mutating', () => {
|
||||
const derived = mutatingNames(tools as Record<string, { _meta?: ToolMeta }>);
|
||||
expect([...MUTATING_TOOLS].sort()).toEqual(derived);
|
||||
});
|
||||
|
||||
test('every mutating tool has a DEFAULT_PERMISSIONS entry (otherwise it would run ungated)', () => {
|
||||
for (const name of MUTATING_TOOLS) {
|
||||
expect(DEFAULT_PERMISSIONS[name], `${name} mutates but has no DEFAULT_PERMISSIONS entry`).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
test('a mutating tool outside MUTATING_TOOLS would be caught: the derive is the single source of truth', () => {
|
||||
// This is the invariant the hand-list used to break: a tool with mutating:true
|
||||
// must appear in MUTATING_TOOLS, and a tool without it must not.
|
||||
for (const [name, t] of Object.entries(tools as Record<string, { _meta?: ToolMeta }>)) {
|
||||
const isMutating = (t._meta?.mutating ?? false);
|
||||
const inList = (MUTATING_TOOLS as readonly string[]).includes(name);
|
||||
expect(inList, `${name}: _meta.mutating=${isMutating} but MUTATING_TOOLS includes=${inList}`).toBe(isMutating);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user