blob: fb6562c9a7c98d3bb8abc62f913cf883bae9d9b7 [file]
import assert from 'node:assert/strict';
import { describe, test } from 'node:test';
import { z } from 'zod';
import {
ToolAvailabilityRuntime,
LOAD_TOOLS_NAME,
type RuntimeEventLike,
type StepLike,
} from '../tool-availability.js';
import type { MakaTool } from '../tool-runtime.js';
function tool(name: string): MakaTool {
return { name, description: name, parameters: z.object({}), impl: () => ({ ok: true }) };
}
const invalid: MakaTool = {
name: 'invalid',
description: 'invalid',
parameters: z.object({}),
impl: () => ({}),
};
const ctx = {
sessionId: 's',
turnId: 't',
cwd: '/tmp',
toolCallId: 'tc',
abortSignal: new AbortController().signal,
emitOutput: () => {},
};
// rive/docs grouped; Read/Write ungrouped (always visible).
function runtime(economy: boolean) {
return new ToolAvailabilityRuntime(
[tool('Read'), tool('Write'), tool('rive_run'), tool('docs_edit'), tool('docs_read')],
{
economy,
groups: [
{ id: 'rive', toolNames: ['rive_run'], label: 'Rive' },
{ id: 'docs', toolNames: ['docs_edit', 'docs_read'], description: 'Document tools' },
],
},
invalid,
);
}
function loadStep(group: string): StepLike {
return { toolCalls: [{ toolName: LOAD_TOOLS_NAME, input: { group } }] };
}
describe('ToolAvailabilityRuntime — full mode', () => {
test('economy off advertises every tool, no connector / gating / diagnostics', () => {
const plan = runtime(false).prepare([]);
assert.ok(
plan.activeTools.includes('rive_run'),
'grouped tools are active when economy is off',
);
assert.ok(plan.activeTools.includes('docs_edit'));
assert.ok(!plan.activeTools.includes(LOAD_TOOLS_NAME), 'no connector in full mode');
assert.equal(plan.projectActiveTools, undefined);
assert.equal(plan.gating, undefined);
assert.equal(plan.diagnostics([], 0), undefined);
});
test('economy on but no hideable groups falls back to full mode', () => {
const r = new ToolAvailabilityRuntime([tool('Read')], { economy: true }, invalid);
const plan = r.prepare([]);
assert.equal(plan.gating, undefined, 'nothing to hide → no gating');
assert.ok(plan.activeTools.includes('Read'));
});
});
describe('ToolAvailabilityRuntime — economy mode', () => {
test('only ungrouped tools + connector are active at step 0; group tools hidden', () => {
const plan = runtime(true).prepare([]);
assert.ok(plan.activeTools.includes('Read'), 'ungrouped tool is visible');
assert.ok(plan.activeTools.includes('Write'), 'ungrouped defaults to visible');
assert.ok(plan.activeTools.includes(LOAD_TOOLS_NAME), 'connector is always visible');
assert.ok(!plan.activeTools.includes('rive_run'), 'grouped tool hidden until loaded');
assert.ok(!plan.activeTools.includes('docs_edit'));
});
test('a required orchestration tool stays pinned for the whole turn', () => {
const plan = runtime(true).prepare([], new Set(['rive_run']));
assert.ok(plan.activeTools.includes('rive_run'), 'required tool is visible at step 0');
assert.ok(plan.gating?.activeNames().has('rive_run'));
const next = plan.projectActiveTools!({ completedSteps: [] });
assert.ok(next.activeTools.includes('rive_run'), 'required tool remains visible later');
assert.ok(!next.activeTools.includes('docs_edit'), 'other deferred groups remain hidden');
});
test('unknown required tool names are ignored', () => {
const plan = runtime(true).prepare([], new Set(['not-a-tool']));
assert.ok(!plan.activeTools.includes('not-a-tool'));
assert.ok(!plan.activeTools.includes('rive_run'));
});
test('providerTools keeps every tool dispatchable, including hidden groups + invalid', () => {
const names = runtime(true)
.prepare([])
.providerTools.map((t) => t.name);
for (const n of [
'Read',
'Write',
'rive_run',
'docs_edit',
'docs_read',
LOAD_TOOLS_NAME,
'invalid',
]) {
assert.ok(names.includes(n), `${n} present in providerTools`);
}
});
test('the connector activates a group in the next request projection', () => {
const plan = runtime(true).prepare([]);
assert.ok(plan.projectActiveTools);
const next = plan.projectActiveTools!({ completedSteps: [loadStep('docs')] });
assert.ok(next.activeTools.includes('docs_edit'), 'docs group active after load_tools(docs)');
assert.ok(next.activeTools.includes('docs_read'));
assert.ok(!next.activeTools.includes('rive_run'), 'an unloaded group stays hidden');
});
test('gating exposes group members and tracks the current step snapshot', () => {
const plan = runtime(true).prepare([]);
assert.ok(plan.gating);
assert.deepEqual([...plan.gating!.gatedNames].sort(), ['docs_edit', 'docs_read', 'rive_run']);
assert.ok(!plan.gating!.activeNames().has('rive_run'), 'rive hidden at step 0');
plan.projectActiveTools!({ completedSteps: [loadStep('rive')] });
assert.ok(plan.gating!.activeNames().has('rive_run'), 'snapshot updated after rive load');
});
test('connector impl returns the group tool names; unknown group throws', async () => {
const connector = runtime(true)
.prepare([])
.providerTools.find((t) => t.name === LOAD_TOOLS_NAME);
assert.ok(connector);
assert.deepEqual(await connector!.impl({ group: 'docs' }, ctx), {
loaded: ['docs_edit', 'docs_read'],
});
await assert.rejects(async () => connector!.impl({ group: 'nope' }, ctx), /Unknown tool group/);
});
});
describe('ToolAvailabilityRuntime — durable ledger seed', () => {
function event(name: string, args: unknown): RuntimeEventLike {
return { content: { kind: 'function_call', name, args } };
}
test('a prior-turn load_tools call re-activates the group at step 0', () => {
const plan = runtime(true).prepare([event(LOAD_TOOLS_NAME, { group: 'rive' })]);
assert.ok(plan.activeTools.includes('rive_run'), 'seeded group active from turn start');
assert.ok(!plan.activeTools.includes('docs_edit'), 'unseeded group still hidden');
});
test('historical load_tool (PR#30) and connect_tool_source (PR#34) calls also seed', () => {
const fromDeferred = runtime(true).prepare([event('load_tool', { namespace: 'docs' })]);
assert.ok(
fromDeferred.activeTools.includes('docs_edit'),
'load_tool namespace seeds the group',
);
const fromEconomy = runtime(true).prepare([event('connect_tool_source', { source: 'rive' })]);
assert.ok(
fromEconomy.activeTools.includes('rive_run'),
'connect_tool_source source seeds the group',
);
});
test('an unknown seeded group id is ignored (forward compatible)', () => {
const plan = runtime(true).prepare([event(LOAD_TOOLS_NAME, { group: 'ghost' })]);
assert.ok(!plan.activeTools.includes('rive_run'));
assert.ok(!plan.activeTools.includes('docs_edit'));
});
});
describe('ToolAvailabilityRuntime — diagnostics', () => {
test('reports the active subset, enabled/available groups, and schema reduction', () => {
const plan = runtime(true).prepare([]);
const d = plan.diagnostics(plan.activeTools, 100);
assert.ok(d);
assert.equal(d!.mode, 'economy');
assert.equal(d!.connectorToolName, LOAD_TOOLS_NAME);
assert.deepEqual(d!.enabledSourceIds, [], 'no group loaded at step 0');
assert.deepEqual(d!.availableSourceIds, ['docs', 'rive']);
assert.deepEqual(d!.visibleToolNamesBySource, {
docs: ['docs_edit', 'docs_read'],
rive: ['rive_run'],
});
assert.ok(
(d!.fullToolSchemaChars ?? 0) > (d!.visibleToolSchemaChars ?? 0),
'hidden schemas reduce the visible chars',
);
assert.ok((d!.toolSchemaCharReduction ?? 0) > 0);
// full = visible + hidden must hold (the connector is counted on both
// sides, so it cancels — guards against the hiddenToolCount off-by-one).
assert.equal(d!.hiddenToolCount, 3, 'rive(1) + docs(2) tools are hidden at step 0');
assert.equal(d!.fullToolCount, (d!.visibleToolCount ?? 0) + (d!.hiddenToolCount ?? 0));
});
test('enabledSourceIds grows once a group is loaded', () => {
const plan = runtime(true).prepare([]);
const active = plan.projectActiveTools!({ completedSteps: [loadStep('rive')] }).activeTools;
const d = plan.diagnostics(active, 100);
assert.deepEqual(d!.enabledSourceIds, ['rive']);
assert.deepEqual(d!.availableSourceIds, ['docs']);
});
});
describe('ToolAvailabilityRuntime — connector shape', () => {
function connector() {
const found = runtime(true)
.prepare([])
.providerTools.find((t) => t.name === LOAD_TOOLS_NAME);
assert.ok(found);
return found!;
}
test('lists every group in its description and never requires permission', () => {
const c = connector();
assert.match(c.description, /rive/);
assert.match(c.description, /docs/);
assert.match(c.description, /Rive/); // rive group's label
assert.match(c.description, /Document tools/); // docs group's description
});
test('loading a group returns exactly its tool names — a thin result, no schema', async () => {
const result = await connector().impl({ group: 'docs' }, ctx);
assert.deepEqual(result, { loaded: ['docs_edit', 'docs_read'] });
const keys = Object.keys(result as object);
assert.ok(
!keys.includes('schema') && !keys.includes('parameters') && !keys.includes('inputSchema'),
);
});
});
describe('ToolAvailabilityRuntime — activation robustness', () => {
test('parses a stringified connector input, ignores malformed input', () => {
const plan = runtime(true).prepare([]);
const ok = plan.projectActiveTools!({
completedSteps: [
{ toolCalls: [{ toolName: LOAD_TOOLS_NAME, input: JSON.stringify({ group: 'rive' }) }] },
],
});
assert.ok(ok.activeTools.includes('rive_run'), 'stringified { group } is parsed');
const bad = runtime(true).prepare([]);
const after = bad.projectActiveTools!({
completedSteps: [{ toolCalls: [{ toolName: LOAD_TOOLS_NAME, input: 'not json' }] }],
});
assert.ok(!after.activeTools.includes('rive_run'), 'malformed input activates nothing');
});
test('same-turn activation honors only load_tools({group}) — historical names and other keys are inert', () => {
const cases: Array<{ toolName: string; input: unknown }> = [
{ toolName: 'load_tool', input: { namespace: 'rive' } }, // PR#30 name — ledger-only
{ toolName: 'connect_tool_source', input: { source: 'rive' } }, // PR#34 name — ledger-only
{ toolName: LOAD_TOOLS_NAME, input: { namespace: 'rive' } }, // right name, wrong key
{ toolName: LOAD_TOOLS_NAME, input: { source: 'rive' } }, // right name, wrong key
];
for (const c of cases) {
const plan = runtime(true).prepare([]);
const next = plan.projectActiveTools!({ completedSteps: [{ toolCalls: [c] }] });
assert.ok(
!next.activeTools.includes('rive_run'),
`step ${c.toolName}(${JSON.stringify(c.input)}) must NOT activate a group`,
);
}
// Only the live connector with the `group` arg activates same-turn.
const ok = runtime(true).prepare([]).projectActiveTools!({
completedSteps: [loadStep('rive')],
});
assert.ok(ok.activeTools.includes('rive_run'));
});
test('a non-function_call ledger event does not seed a group', () => {
const plan = runtime(true).prepare([
{ content: { kind: 'function_response', name: LOAD_TOOLS_NAME, args: { group: 'rive' } } },
]);
assert.ok(!plan.activeTools.includes('rive_run'), 'only committed function_call events seed');
});
});