blob: 5818e2f1d859e314a6cd7edef7c171b0943237e7 [file]
import assert from 'node:assert/strict';
import { describe, test } from 'node:test';
import { z } from 'zod';
import {
ToolAvailabilityRuntime,
LOAD_TOOLS_NAME,
toolAvailabilityHash,
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: () => {},
};
test('tool availability hash captures policy while canonicalizing group members', () => {
const full = toolAvailabilityHash({ economy: false });
const economy = toolAvailabilityHash({
economy: true,
groups: [{ id: 'docs', toolNames: ['docs_read', 'docs_edit', 'docs_read'] }],
});
const reordered = toolAvailabilityHash({
economy: true,
groups: [{ id: 'docs', toolNames: ['docs_edit', 'docs_read'] }],
});
assert.notEqual(full, economy);
assert.equal(economy, reordered);
});
// 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 — 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('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('connector rejects an unknown group', async () => {
const connector = runtime(true)
.prepare([])
.providerTools.find((t) => t.name === LOAD_TOOLS_NAME);
assert.ok(connector);
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');
});
});
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('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');
});
});