Files
llmorbiter/test/orbiter.test.js
2026-10-05 22:27:20 +02:00

156 lines
4.5 KiB
JavaScript

'use strict';
import test from 'node:test';
import assert from 'node:assert/strict';
import { LLMOrbiter, defineTool } from '../src/index.js';
class MockProvider {
constructor(responses) {
this.responses = [...responses];
this.calls = [];
}
async chatCompletions(args) {
this.calls.push(args);
const next = this.responses.shift();
if (!next) throw new Error('MockProvider ran out of mock responses');
return next;
}
}
test('LLMOrbiter loop continues when provider returns no response (no tool calls and no content)', async () => {
let toolInvoked = false;
const testTool = defineTool({
name: 'test_tool',
description: 'Test tool',
parameters: { type: 'object', properties: {} },
invoke: async () => {
toolInvoked = true;
return { ok: true };
},
});
// Sequence:
// 1. Tool call
// 2. No response (no tool calls, empty content) -> should continue loop!
// 3. Final content
const mock = new MockProvider([
{
tool_calls: [
{
id: 'call_1',
type: 'function',
function: { name: 'test_tool', arguments: '{}' },
},
],
content: null,
},
{
tool_calls: undefined,
content: '',
},
{
tool_calls: undefined,
content: 'Task completed successfully.',
},
]);
const orbiter = new LLMOrbiter(mock);
const result = await orbiter.run({
prompt: 'Do something',
tools: [testTool],
maxIterations: 10,
});
assert.equal(toolInvoked, true, 'Tool should have been invoked');
assert.equal(result.iterations, 3, 'Should have taken 3 iterations (tool call, no response, final answer)');
assert.equal(result.content, 'Task completed successfully.');
assert.equal(mock.calls.length, 3, 'Provider should have been called 3 times');
});
test('LLMOrbiter stops at maxIterations if provider continually returns no response', async () => {
const mock = new MockProvider([
{ tool_calls: undefined, content: '' },
{ tool_calls: undefined, content: null },
{ tool_calls: undefined, content: ' ' },
]);
const orbiter = new LLMOrbiter(mock);
const result = await orbiter.run({
prompt: 'Hello',
tools: [],
maxIterations: 3,
});
assert.equal(result.iterations, 3, 'Should have iterated up to maxIterations');
assert.equal(result.content, null, 'Should return content null when no response was ever given');
});
test('LLMOrbiter finishes immediately in 1 iteration when content is returned with no tool calls', async () => {
const mock = new MockProvider([
{ tool_calls: undefined, content: 'Hello there!' },
]);
const orbiter = new LLMOrbiter(mock);
const result = await orbiter.run({
prompt: 'Hi',
tools: [],
maxIterations: 10,
});
assert.equal(result.iterations, 1);
assert.equal(result.content, 'Hello there!');
});
test('LLMOrbiter calls onToolResult before onUsage in a tool-calling iteration', async () => {
const events = [];
const testTool = defineTool({
name: 'test_tool',
description: 'Test tool',
parameters: { type: 'object', properties: {} },
invoke: async () => {
return { ok: true, data: 42 };
},
});
const mock = new MockProvider([
{
tool_calls: [
{
id: 'call_1',
type: 'function',
function: { name: 'test_tool', arguments: '{}' },
},
],
content: null,
usage: { prompt_tokens: 10, completion_tokens: 5 },
},
{
tool_calls: undefined,
content: 'All done.',
usage: { prompt_tokens: 20, completion_tokens: 8 },
},
]);
const orbiter = new LLMOrbiter(mock, {
onToolCall: ({ name }) => events.push(`tool_call:${name}`),
onToolResult: ({ name }) => events.push(`tool_result:${name}`),
onUsage: (u) => events.push(`usage:${u.input}:${u.output}`),
});
const result = await orbiter.run({
prompt: 'Run tool',
tools: [testTool],
});
assert.equal(result.content, 'All done.');
assert.deepEqual(events, [
'tool_call:test_tool',
'tool_result:test_tool',
'usage:10:5',
'usage:20:8',
]);
});