61 lines
3.4 KiB
JavaScript
61 lines
3.4 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import http from 'node:http';
|
|
|
|
const requests = [];
|
|
let responder;
|
|
const server = http.createServer(async (req, res) => {
|
|
let body = ''; for await (const chunk of req) body += chunk;
|
|
requests.push(JSON.parse(body));
|
|
res.writeHead(200, { 'Content-Type': 'text/event-stream' });
|
|
for (const event of responder(requests.length)) res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
res.end('data: [DONE]\n\n');
|
|
});
|
|
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
process.env.OPENROUTER_URL = `http://127.0.0.1:${server.address().port}`;
|
|
process.env.OPENROUTER_API_KEY = 'test';
|
|
process.env.LLM_MODE = 'openrouter';
|
|
const { streamAnswer } = await import('../server/providers.mjs');
|
|
test.after(() => server.close());
|
|
const event = (delta, finish_reason = null) => ({ choices: [{ delta, finish_reason }] });
|
|
const settings = { systemPrompt: 'test', llmModel: 'test' };
|
|
|
|
test('assembles fragmented calls before executing and streams text', async () => {
|
|
requests.length = 0;
|
|
responder = n => n === 1 ? [event({ content: 'Сейчас включу.', tool_calls: [{ index: 0, id: 'call_1', type: 'function', function: { name: 'spotify_play', arguments: '{"query":"' } }] }), event({ tool_calls: [{ index: 0, function: { arguments: 'Queen"}' } }] }, 'tool_calls')] : [event({ content: 'Готово' }, 'stop')];
|
|
const calls = [], text = [];
|
|
const answer = await streamAnswer(settings, [], 'play', new AbortController().signal, part => text.push(part), async (name, args) => { calls.push([name, args]); return { ok: true }; });
|
|
assert.equal(answer, 'Готово'); assert.deepEqual(text, ['Готово']);
|
|
assert.deepEqual(calls, [['spotify_play', { query: 'Queen' }]]);
|
|
assert.equal(requests[0].tools[0].type, 'openrouter:web_search');
|
|
assert.equal(requests[1].messages.at(-1).tool_call_id, 'call_1');
|
|
});
|
|
|
|
test('confirms Spotify play once without reading the volume or an old track', async () => {
|
|
requests.length = 0;
|
|
responder = () => [event({ content: 'Громкость 40.', tool_calls: [{ index: 0, id: 'play', function: { name: 'spotify_play', arguments: '{"query":"Aphex Twin"}' } }] }, 'tool_calls')];
|
|
const text = [];
|
|
const answer = await streamAnswer(settings, [], 'включи музыку', AbortSignal.timeout(2000), part => text.push(part), async () => ({
|
|
accepted: true, status: 'pending', selection: { name: 'Aphex Twin' }, item: { name: 'Old track' }, volume: 40,
|
|
}));
|
|
assert.equal(answer, 'Включаю «Aphex Twin».');
|
|
assert.deepEqual(text, [answer]);
|
|
assert.equal(requests.length, 1);
|
|
});
|
|
|
|
test('does not execute incomplete tool calls', async () => {
|
|
requests.length = 0;
|
|
responder = () => [event({ tool_calls: [{ index: 0, id: 'x', function: { name: 'spotify_control', arguments: '{"action":"next"}' } }] }, 'length')];
|
|
let called = false;
|
|
await assert.rejects(streamAnswer(settings, [], 'next', new AbortController().signal, () => {}, async () => { called = true; }), /llm_incomplete/);
|
|
assert.equal(called, false);
|
|
});
|
|
|
|
test('limits repeated tool calls', async () => {
|
|
requests.length = 0;
|
|
responder = n => [event({ tool_calls: [{ index: 0, id: `call_${n}`, function: { name: 'spotify_control', arguments: '{"action":"next"}' } }] }, 'tool_calls')];
|
|
let calls = 0;
|
|
await assert.rejects(streamAnswer(settings, [], 'next', new AbortController().signal, () => {}, async () => { calls++; return { ok: true }; }), /llm_limit/);
|
|
assert.equal(calls, 3);
|
|
});
|