beast-agent 0.30.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/package.json +8 -3
  2. package/src/agent/engine.js +463 -21
  3. package/src/agent/llm.js +52 -3
  4. package/src/agent/obscura.js +26 -10
  5. package/src/agent/skills.js +2 -2
  6. package/src/agent/tools.js +519 -7
  7. package/src/main.js +28 -13
  8. package/src/renderer/i18n.js +7 -7
  9. package/src/renderer/index.html +5 -2
  10. package/src/renderer/renderer.js +154 -5
  11. package/src/renderer/style.css +45 -4
  12. package/.env.example +0 -8
  13. package/config.example.yaml +0 -39
  14. package/eng.traineddata +0 -0
  15. package/src/agent/scripts/__pycache__/websearch.cpython-312.pyc +0 -0
  16. package/store/skills.json +0 -5
  17. package/tests/approval.test.js +0 -56
  18. package/tests/bg-jobs.test.js +0 -424
  19. package/tests/bots-name.test.js +0 -42
  20. package/tests/bus.test.js +0 -46
  21. package/tests/computeruse-context.test.js +0 -69
  22. package/tests/cron.test.js +0 -95
  23. package/tests/engine.test.js +0 -203
  24. package/tests/eval.test.js +0 -91
  25. package/tests/fallout.test.js +0 -188
  26. package/tests/llm.test.js +0 -205
  27. package/tests/memory.test.js +0 -33
  28. package/tests/memoryloop.test.js +0 -26
  29. package/tests/notegen.test.js +0 -63
  30. package/tests/obscura.test.js +0 -124
  31. package/tests/owner.test.js +0 -32
  32. package/tests/python.test.js +0 -76
  33. package/tests/research.test.js +0 -116
  34. package/tests/scenarios.json +0 -81
  35. package/tests/sessioncode.test.js +0 -50
  36. package/tests/setup.js +0 -9
  37. package/tests/tokens.test.js +0 -42
  38. package/tests/tools.test.js +0 -84
  39. package/tests/usage.test.js +0 -44
  40. package/tests/v11.test.js +0 -76
  41. package/tests/watchers.test.js +0 -206
  42. package/tests/watext.test.js +0 -53
  43. package/tests/whatsapp.test.js +0 -103
  44. package/tests/wherewasi.test.js +0 -40
@@ -1,203 +0,0 @@
1
- 'use strict';
2
-
3
- require('./setup');
4
- const test = require('node:test');
5
- const assert = require('node:assert');
6
- const fs = require('fs');
7
- const os = require('os');
8
- const path = require('path');
9
-
10
- const { Engine, sanitizeTodoItems } = require('../src/agent/engine');
11
-
12
- function makeEngine(extra = {}) {
13
- const sessionsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'beast-sess-'));
14
- const workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'beast-ws-'));
15
- return new Engine(
16
- {},
17
- { sessionsDir, workspace, customProviders: [], emit: () => {}, ...extra }
18
- );
19
- }
20
-
21
- /* ---------- sanitizeTodoItems ---------- */
22
-
23
- test('todo: boş/geçersiz girdiler elenir', () => {
24
- const out = sanitizeTodoItems([
25
- { title: 'Planla', status: 'active' },
26
- { title: '' },
27
- null,
28
- 'Sadece string başlık',
29
- { title: 'Planla', status: 'done' }, // duplicate title
30
- { title: 'Bitir', status: 'saçma' },
31
- ]);
32
- assert.deepEqual(out, [
33
- { title: 'Planla', status: 'active' },
34
- { title: 'Sadece string başlık', status: 'pending' },
35
- { title: 'Bitir', status: 'pending' },
36
- ]);
37
- });
38
-
39
- test('todo: 20 ile sınırlı', () => {
40
- const items = Array.from({ length: 50 }, (_, i) => ({ title: 'görev ' + i }));
41
- assert.equal(sanitizeTodoItems(items).length, 20);
42
- });
43
-
44
- /* ---------- payload builder ---------- */
45
-
46
- test('tool_call/tool mesajları birlikte tutulur', () => {
47
- const eng = makeEngine();
48
- const msgs = [
49
- { role: 'user', content: 'merhaba' },
50
- { role: 'assistant', content: '', tool_calls: [{ id: '1', type: 'function', function: { name: 'list_dir', arguments: '{}' } }] },
51
- { role: 'tool', tool_call_id: '1', name: 'list_dir', content: '{"ok":true}' },
52
- { role: 'assistant', content: 'sonuç şu' },
53
- { role: 'user', content: 'tekrar' },
54
- ];
55
- const payload = eng._buildPayload('sys', msgs);
56
- assert.equal(payload[0].role, 'system');
57
- const idxAssistant = payload.findIndex((m) => m.role === 'assistant');
58
- assert.ok(idxAssistant >= 0);
59
- assert.equal(payload[idxAssistant + 1].role, 'tool');
60
- });
61
-
62
- test('token bütçesine uyar: eski mesajlar düşer, son mesaj kalır', () => {
63
- const eng = makeEngine({ historyTokenBudget: 120 });
64
- const big = 'kelime '.repeat(400); // ~700 token
65
- const msgs = [];
66
- for (let i = 0; i < 10; i++) msgs.push({ role: 'user', content: big + ' #' + i });
67
- const payload = eng._buildPayload('sys', msgs);
68
- // system + en az son kullanıcı mesajı
69
- assert.equal(payload[0].role, 'system');
70
- const last = payload[payload.length - 1];
71
- assert.ok(last.role === 'user' && last.content.includes('#9'));
72
- assert.ok(eng._payloadTokens(payload.slice(1)) <= 120 + estSlack(last));
73
- function estSlack(m) {
74
- return Math.ceil((m.content || '').length / 4) * 3; // tek mesaj taşabilir
75
- }
76
- });
77
-
78
- test('browser_open/browser_read kancaları çağrılır', async () => {
79
- const calls = [];
80
- const eng = makeEngine({
81
- browser: {
82
- openUrl: async (u) => { calls.push(['open', u]); return { ok: true, url: u, title: 'T' }; },
83
- readText: async () => { calls.push(['read']); return { ok: true, content: 'sayfa metni' }; },
84
- },
85
- });
86
- const r1 = JSON.parse(await eng._execTool('browser_open', { url: 'https://example.com' }, null, 's1'));
87
- const r2 = JSON.parse(await eng._execTool('browser_read', {}, null, 's1'));
88
- assert.ok(r1.ok && r1.title === 'T');
89
- assert.ok(r2.ok && r2.content.includes('metni'));
90
- assert.deepEqual(calls, [['open', 'https://example.com'], ['read']]);
91
- });
92
-
93
- test('browser kancası yoksa zarif hata döner', async () => {
94
- const eng = makeEngine();
95
- const r = JSON.parse(await eng._execTool('browser_open', { url: 'https://x.com' }, null, 's1'));
96
- assert.equal(r.ok, false);
97
- assert.match(r.error, /tarayıcı/);
98
- });
99
-
100
- test('eski görseller metne indirilir, son mesajın görseli korunur', () => {
101
- const eng = makeEngine();
102
- const img = (u) => [{ type: 'text', text: 'bak' }, { type: 'image_url', image_url: { url: u } }];
103
- const msgs = [
104
- { role: 'user', content: img('data:image/png;base64,AAAA') },
105
- { role: 'assistant', content: 'ok' },
106
- { role: 'user', content: img('data:image/png;base64,BBBB') },
107
- ];
108
- const payload = eng._buildPayload('sys', msgs);
109
- const firstUser = payload.find((m) => m.role === 'user');
110
- assert.equal(typeof firstUser.content, 'string');
111
- assert.ok(firstUser.content.includes('bağlam dışı'));
112
- const lastUser = payload[payload.length - 1];
113
- assert.ok(Array.isArray(lastUser.content));
114
- });
115
-
116
- /* ---------- oturum notlar� (ge�ici haf�za) ---------- */
117
-
118
- test('oturum notlar�: dosyadan y�klenir, mesaj listesine kar��maz', () => {
119
- const eng = makeEngine();
120
- const id = eng.createSession().id;
121
- for (let i = 0; i < 5; i++) {
122
- fs.appendFileSync(eng._file(id), JSON.stringify({ t: 'msg', role: 'user', content: 'mesaj ' + i }) + '\n');
123
- }
124
- fs.appendFileSync(eng._file(id), JSON.stringify({ t: 'notes', text: 'hedef: v7 �al�smas�', at: 3 }) + '\n');
125
- eng.cache.delete(id); // diskten taze y�kle
126
- const s = eng._load(id);
127
- assert.equal(s.notes, 'hedef: v7 �al�smas�');
128
- assert.equal(s.notesAt, 3);
129
- assert.equal(s.messages.length, 5);
130
- });
131
-
132
- test('oturum notlar� varken payload s�k�la��r', () => {
133
- const eng = makeEngine({ historyTokenBudget: 100000 });
134
- const msgs = [];
135
- for (let i = 0; i < 30; i++) msgs.push({ role: 'user', content: 'kisa mesaj #' + i });
136
- const withoutNotes = eng._buildPayload('sys', msgs, null);
137
- const withNotes = eng._buildPayload('sys', msgs, '�zet: test');
138
- assert.ok(withNotes.length < withoutNotes.length);
139
- assert.equal(withNotes[withNotes.length - 1].content, 'kisa mesaj #29'); // son mesaj hep kal�r
140
- });
141
-
142
- /* ---------- observe(): sessiz bağlam enjeksiyonu ---------- */
143
-
144
- test('observe: mesaj geçmişe düşer ama run tetiklenmez', async () => {
145
- const events = [];
146
- const eng = makeEngine({ emit: (ev) => events.push(ev) });
147
- const id = eng.createSession().id;
148
- const ok = eng.observe(id, '[BAĞLAM — grup] ali: selam');
149
- assert.ok(ok);
150
- const s = eng._load(id);
151
- assert.equal(s.messages.length, 1);
152
- assert.equal(s.messages[0].role, 'user');
153
- assert.ok(s.messages[0].content.startsWith('[BAĞLAM'));
154
- // _run tetiklenmedi: LLM hatası/done olayı yok
155
- await new Promise((r) => setTimeout(r, 60));
156
- assert.ok(!events.some((e) => e.type === 'error' || e.type === 'done'));
157
- // observe olayı yayıldı
158
- assert.ok(events.some((e) => e.type === 'observe' && e.sessionId === id));
159
- // boş metin false
160
- assert.equal(eng.observe(id, ' '), false);
161
- });
162
-
163
- test('observe: ardışık bağlam mesajları tek mesajda birleşir', () => {
164
- const eng = makeEngine();
165
- const id = eng.createSession().id;
166
- eng.observe(id, '[BAĞLAM — grup] ali: selam');
167
- eng.observe(id, '[BAĞLAM — grup] ayşe: nasılsın');
168
- const s = eng._load(id);
169
- assert.equal(s.messages.length, 1);
170
- assert.ok(s.messages[0].content.includes('ali: selam'));
171
- assert.ok(s.messages[0].content.includes('ayşe: nasılsın'));
172
- // dosyaya da tek satır yazıldı
173
- const lines = fs.readFileSync(eng._file(id), 'utf8').split('\n').filter((l) => l.trim());
174
- const msgLines = lines.filter((l) => { try { return JSON.parse(l).t === 'msg'; } catch { return false; } });
175
- assert.equal(msgLines.length, 1);
176
- });
177
-
178
- test('observe: normal user mesajıyla birleşmez', () => {
179
- const eng = makeEngine();
180
- const id = eng.createSession().id;
181
- const s = eng._load(id);
182
- s.messages.push({ role: 'user', content: 'gerçek soru' });
183
- fs.appendFileSync(eng._file(id), JSON.stringify({ t: 'msg', role: 'user', content: 'gerçek soru' }) + '\n');
184
- eng.observe(id, '[BAĞLAM — grup] ali: selam');
185
- assert.equal(s.messages.length, 2);
186
- assert.equal(s.messages[0].content, 'gerçek soru');
187
- assert.ok(s.messages[1].content.startsWith('[BAĞLAM'));
188
- });
189
-
190
- test('send: mention mesajı bekleyen bağlam mesajıyla birleşir ve run başlar', async () => {
191
- const events = [];
192
- const eng = makeEngine({ emit: (ev) => events.push(ev) });
193
- const id = eng.createSession().id;
194
- eng.observe(id, '[BAĞLAM — grup] ali: selam\nayşe: nasılsın');
195
- eng.send(id, { text: '@beast bana bugün hava nasıl?' });
196
- const s = eng._load(id);
197
- assert.equal(s.messages.length, 1);
198
- assert.ok(s.messages[0].content.includes('ayşe: nasılsın'));
199
- assert.ok(s.messages[0].content.includes('@beast bana bugün hava nasıl?'));
200
- // run başladı → sağlayıcı yoksa hata olayı düşer (tetiklenme kanıtı)
201
- await new Promise((r) => setTimeout(r, 120));
202
- assert.ok(events.some((e) => e.type === 'error' || e.type === 'done'));
203
- });
@@ -1,91 +0,0 @@
1
- 'use strict';
2
-
3
- /* #4 Eval düzeni: scenarios.json'daki senaryoları koşar.
4
- Yeni modül eklerken buraya senaryo düşür — regresyon kalkanı olur. */
5
-
6
- require('./setup');
7
- const test = require('node:test');
8
- const assert = require('node:assert');
9
- const path = require('path');
10
- const fs = require('fs');
11
-
12
- const SCEN = JSON.parse(fs.readFileSync(path.join(__dirname, 'scenarios.json'), 'utf8'));
13
- const cron = require('../src/cron');
14
- const Engine = require('../src/agent/engine');
15
- const memory = require('../src/agent/memory');
16
- const kb = require('../src/agent/kb');
17
- const bus = require('../src/agent/bus');
18
- const usageMod = require('../src/agent/usage');
19
-
20
- function scenario(id) {
21
- return SCEN.scenarios.find((s) => s.id === id);
22
- }
23
-
24
- test('eval: cron-invalid-rejected', () => {
25
- const s = scenario('cron-invalid-rejected');
26
- assert.equal(cron[s.call.fn](...s.call.args), null);
27
- });
28
-
29
- test('eval: reminder-daily-schedule', () => {
30
- const s = scenario('reminder-daily-schedule');
31
- const r = cron[s.call.fn](...s.call.args);
32
- assert.equal(r.schedule, s.expectEquals.schedule);
33
- });
34
-
35
- test('eval: kb-search-citation', () => {
36
- const s = scenario('kb-search-citation');
37
- kb.add('FxPro GOLD seansları', 'GOLD sembolünde seans 01:00-23:00 arası açıktır', { source: 'test' });
38
- const rows = kb.search('gold seans');
39
- assert.ok(rows.length >= 1);
40
- for (const r of rows) {
41
- assert.ok(r.citation && r.citation.includes('['), 'citation formatı');
42
- assert.ok(r.title && r.snippet !== undefined);
43
- }
44
- });
45
-
46
- test('eval: memory-hygiene-dedup', () => {
47
- const s = scenario('memory-hygiene-dedup');
48
- assert.equal(s.module, 'memory');
49
- memory.append('Kullanıcının adı Batuhan');
50
- memory.append('kullanıcının adı batuhan'); // dup (fold edilmiş)
51
- const r = memory.hygiene({ maxAgeDays: 0 }); // yalnız dedup, yaş silmesi yok
52
- assert.ok(r.ok);
53
- const kept = memory.entries().filter((l) => /batuhan/i.test(l));
54
- assert.ok(kept.length <= 1, 'duplike tekilleşmeli');
55
- // temizlik
56
- for (const l of memory.entries()) if (/batuhan/i.test(l)) memory.removeRule('__none__'); // no-op koruması
57
- });
58
-
59
- test('eval: system-prompt-has-local-time', () => {
60
- const os = require('os');
61
- const eng = new Engine({}, { sessionsDir: fs.mkdtempSync(path.join(os.tmpdir(), 'beast-eval-')) });
62
- const sys = eng.buildSystem('x');
63
- assert.match(sys, new RegExp(scenario('system-prompt-has-local-time').regex));
64
- });
65
-
66
- test('eval: system-prompt-rules-injected', () => {
67
- const os = require('os');
68
- const eng = new Engine({}, { sessionsDir: fs.mkdtempSync(path.join(os.tmpdir(), 'beast-eval2-')) });
69
- memory.addRule('Eval test kuralı ABCXYZ');
70
- const sys = eng.buildSystem('x');
71
- assert.match(sys, /KURALLAR/);
72
- assert.match(sys, /Eval test kuralı ABCXYZ/);
73
- memory.removeRule('Eval test kuralı ABCXYZ');
74
- });
75
-
76
- test('eval: usage-cost-math kısa doğrulama', () => {
77
- usageMod.reset();
78
- usageMod.record({ providerId: 'e', model: 'm', promptTokens: 1000000, completionTokens: 0, costIn: 1, costOut: 0 });
79
- assert.ok(Math.abs(usageMod.report().today.total.cost - 1) < 1e-6);
80
- });
81
-
82
- test('eval: bus-price-filter', () => {
83
- for (const s of bus.listSubs()) bus.removeSub(s.id);
84
- const r = bus.addSub({ type: 'price:tick', sessionId: 'evalsestest', op: 'gte', value: 100, cooldownMin: 0 });
85
- assert.ok(r.ok);
86
- const fired = bus.emitEvent('price:tick', { value: 150 }, Date.now());
87
- assert.equal(fired, 1); // sadece bu abonelik
88
- const notFired = bus.emitEvent('price:tick', { value: 50 }, Date.now());
89
- assert.equal(notFired, 0);
90
- for (const s of bus.listSubs()) bus.removeSub(s.id);
91
- });
@@ -1,188 +0,0 @@
1
- 'use strict';
2
-
3
- require('./setup');
4
- const test = require('node:test');
5
- const assert = require('node:assert');
6
- const fs = require('fs');
7
- const os = require('os');
8
- const path = require('path');
9
-
10
- const { Engine } = require('../src/agent/engine');
11
-
12
- function tmpDir(prefix) {
13
- return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
14
- }
15
-
16
- /* pA::m1 tek sağlayıcı + çökme kayıt dosyası olan motor */
17
- function makeFalloutEngine({ fallout, emitEvents } = {}) {
18
- const sessionsDir = tmpDir('beast-fall-sess-');
19
- const workspace = tmpDir('beast-fall-ws-');
20
- const crashFile = path.join(tmpDir('beast-fall-crash-'), 'fallout-crash.json');
21
- const chainEntry = {
22
- providerId: 'pA',
23
- providerName: 'ProvA',
24
- model: 'm1',
25
- url: 'https://a.test/v1/chat/completions',
26
- key: 'keyA',
27
- };
28
- const cfg = { chain: [chainEntry], defaultSelection: { ...chainEntry } };
29
- const events = [];
30
- const eng = new Engine(cfg, {
31
- sessionsDir,
32
- workspace,
33
- crashFile,
34
- modelOverride: 'pA::m1',
35
- fallout,
36
- emit: (ev) => events.push(ev),
37
- });
38
- if (emitEvents) emitEvents(events);
39
- return { eng, crashFile, events };
40
- }
41
-
42
- test('fallout: bozuk slotlar elenir, en fazla 10 kalır', () => {
43
- const eng = makeFalloutEngine().eng;
44
- const norm = eng._normalizeFallout({
45
- enabled: true,
46
- slots: [
47
- null,
48
- { providerId: '', model: 'x', key: 'k' },
49
- { providerId: 'p', model: '', key: 'k' },
50
- { providerId: 'p', model: 'm', key: '' },
51
- { providerId: 'pA', providerName: 'ProvA', model: 'm1', key: 'k1' },
52
- ...Array.from({ length: 12 }, (_, i) => ({
53
- providerId: 'p' + i,
54
- providerName: 'P' + i,
55
- model: 'm',
56
- key: 'k',
57
- })),
58
- ],
59
- });
60
- assert.equal(norm.enabled, true);
61
- assert.equal(norm.chain.length, 10); // 1 geçerli + 11 aday ama sınır 10
62
- assert.deepEqual(norm.chain[0], { providerId: 'pA', providerName: 'ProvA', model: 'm1', key: 'k1' });
63
- });
64
-
65
- test('fallout: kapalıyken zincir boş döner', () => {
66
- const { eng } = makeFalloutEngine({
67
- fallout: { enabled: false, slots: [{ providerId: 'pA', model: 'm1', key: 'other' }] },
68
- });
69
- assert.deepEqual(eng._falloutSelections(eng.sel), []);
70
- });
71
-
72
- test('fallout: aynı provider+model farklı key ile listelenir, birebir aynı olan atlanır', () => {
73
- const { eng } = makeFalloutEngine({
74
- fallout: {
75
- enabled: true,
76
- slots: [
77
- { providerId: 'pA', providerName: 'ProvA', model: 'm1', key: 'keyA' }, // aktif ile aynı -> atla
78
- { providerId: 'pA', providerName: 'ProvA', model: 'm1', key: 'keyA2' }, // farklı key -> kalsın
79
- { providerId: 'yok', model: 'm9', key: 'k' }, // kayıtlı değil -> atla
80
- ],
81
- },
82
- });
83
- const sels = eng._falloutSelections(eng.sel);
84
- assert.equal(sels.length, 1);
85
- assert.equal(sels[0].url, 'https://a.test/v1/chat/completions'); // URL zincirden gelir
86
- assert.equal(sels[0].key, 'keyA2');
87
- });
88
-
89
- test('fallout: çökme kaydı yazılır ve silinir', () => {
90
- const { eng, crashFile } = makeFalloutEngine({});
91
- eng._saveCrash('sess1', new Error('patladı'), 'provider');
92
- assert.ok(fs.existsSync(crashFile));
93
- const st = JSON.parse(fs.readFileSync(crashFile, 'utf8'));
94
- assert.equal(st.sessionId, 'sess1');
95
- assert.match(st.error, /patladı/);
96
- assert.equal(st.phase, 'provider');
97
- eng._clearCrash();
98
- assert.ok(!fs.existsSync(crashFile));
99
- });
100
-
101
- function sseResponse(sseText) {
102
- const enc = new TextEncoder();
103
- const stream = new ReadableStream({
104
- start(c) {
105
- c.enqueue(enc.encode(sseText));
106
- c.close();
107
- },
108
- });
109
- return new Response(stream, { status: 200, headers: { 'Content-Type': 'text/event-stream' } });
110
- }
111
-
112
- const SSE_OK =
113
- 'data: {"choices":[{"delta":{"content":"merhaba"}}]}\n\n' +
114
- 'data: {"choices":[{"delta":{},"finish_reason":"stop"}]}\n\n' +
115
- 'data: [DONE]\n\n';
116
-
117
- test('fallout: ilk sağlayıcı 401 çöker → zincirdeki sıradaki key ile devam eder', async () => {
118
- const { eng, crashFile, events } = makeFalloutEngine({
119
- fallout: {
120
- enabled: true,
121
- slots: [{ providerId: 'pA', providerName: 'ProvA', model: 'm1', key: 'keyYedek' }],
122
- },
123
- });
124
-
125
- const calls = [];
126
- const realFetch = globalThis.fetch;
127
- globalThis.fetch = async (url, opts = {}) => {
128
- calls.push({ url: String(url), auth: opts.headers && opts.headers.Authorization });
129
- if (calls.length === 1) {
130
- return new Response(JSON.stringify({ error: { message: 'bad key' } }), {
131
- status: 401,
132
- statusText: 'Unauthorized',
133
- });
134
- }
135
- return sseResponse(SSE_OK);
136
- };
137
- try {
138
- const res = await eng._streamWithFallbacks(
139
- { id: 's1', messages: [{ role: 'user', content: 'selam' }] },
140
- [{ role: 'user', content: 'selam' }],
141
- [],
142
- null,
143
- () => {},
144
- eng.sel,
145
- false
146
- );
147
- assert.equal(res.content, 'merhaba');
148
- } finally {
149
- globalThis.fetch = realFetch;
150
- }
151
-
152
- assert.equal(calls.length, 2);
153
- assert.match(calls[0].auth, /keyA$/);
154
- assert.match(calls[1].auth, /keyYedek$/);
155
- // durumu kaydet adımı çalıştı
156
- assert.ok(fs.existsSync(crashFile), 'çökme kaydı yazılmalı');
157
- // durum mesajı Fallout geçişini haber verdi
158
- const st = events.find((e) => e.type === 'status' && /FALLOUT/.test(e.status));
159
- assert.ok(st, 'FALLOUT durum mesajı beklenirdi');
160
- });
161
-
162
- test('fallout: kapalıysa tek denemede hatayı fırlatır', async () => {
163
- const { eng } = makeFalloutEngine({ fallout: { enabled: false, slots: [] } });
164
- const realFetch = globalThis.fetch;
165
- let n = 0;
166
- globalThis.fetch = async () => {
167
- n++;
168
- return new Response('{}', { status: 401, statusText: 'Unauthorized' });
169
- };
170
- try {
171
- await assert.rejects(
172
- () =>
173
- eng._streamWithFallbacks(
174
- { id: 's1', messages: [] },
175
- [{ role: 'user', content: 'x' }],
176
- [],
177
- null,
178
- () => {},
179
- eng.sel,
180
- false
181
- ),
182
- /HTTP 401/
183
- );
184
- } finally {
185
- globalThis.fetch = realFetch;
186
- }
187
- assert.equal(n, 1);
188
- });
package/tests/llm.test.js DELETED
@@ -1,205 +0,0 @@
1
- 'use strict';
2
-
3
- require('./setup');
4
- const test = require('node:test');
5
- const assert = require('node:assert');
6
- const { withRetries, friendlyError, sumUsage, chatStreamAuto, setNetProbe, isNetworkError } = require('../src/agent/llm');
7
-
8
- test('withRetries: 503 sonrası başarılı isteği tekrarlar', async () => {
9
- let calls = 0;
10
- const r = await withRetries(
11
- async () => {
12
- calls++;
13
- if (calls < 3) {
14
- const e = new Error('HTTP 503');
15
- e.status = 503;
16
- throw e;
17
- }
18
- return 'ok';
19
- },
20
- { onRetry: () => {} }
21
- );
22
- assert.equal(r, 'ok');
23
- assert.equal(calls, 3);
24
- });
25
-
26
- test('withRetries: 400 gibi kalıcı hata beklemeksizin fırlatır', async () => {
27
- let calls = 0;
28
- await assert.rejects(
29
- () =>
30
- withRetries(async () => {
31
- calls++;
32
- const e = new Error('HTTP 400');
33
- e.status = 400;
34
- throw e;
35
- }),
36
- /400/
37
- );
38
- assert.equal(calls, 1);
39
- });
40
-
41
- test('withRetries: ağ hatası (status yok) tekrarlanır', async () => {
42
- let calls = 0;
43
- const r = await withRetries(async () => {
44
- calls++;
45
- if (calls === 1) throw new Error('fetch failed');
46
- return 'ok';
47
- });
48
- assert.equal(r, 'ok');
49
- assert.equal(calls, 2);
50
- });
51
-
52
- test('friendlyError ipuçları içerir', () => {
53
- const m = friendlyError(503, 'Service Unavailable', '{"error":"x"}');
54
- assert.match(m, /kullanılamıyor/);
55
- assert.match(m, /model seçici/);
56
- });
57
-
58
- test('sumUsage: sayısal alanları toplar (devam turu faturalaması)', () => {
59
- const u = sumUsage(
60
- { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
61
- { prompt_tokens: 8, completion_tokens: 3, total_tokens: 11 }
62
- );
63
- assert.deepEqual(u, { prompt_tokens: 18, completion_tokens: 8, total_tokens: 26 });
64
- assert.equal(sumUsage(null, { total_tokens: 3 }).total_tokens, 3);
65
- assert.equal(sumUsage({ total_tokens: 7 }, null).total_tokens, 7);
66
- });
67
-
68
- /* SSE mock sunucusu: finish_reason=length → otomatik devam turu açılır */
69
- function sseServer(script) {
70
- const http = require('node:http');
71
- const calls = [];
72
- const srv = http.createServer((req, res) => {
73
- let body = '';
74
- req.on('data', (c) => (body += c));
75
- req.on('end', () => {
76
- calls.push(JSON.parse(body).messages);
77
- res.writeHead(200, { 'Content-Type': 'text/event-stream' });
78
- for (const ev of script(calls.length, calls[calls.length - 1])) res.write(ev);
79
- res.end();
80
- });
81
- });
82
- return { srv, calls };
83
- }
84
-
85
- test('chatStreamAuto: length kesintisinde kaldığı yerden devam eder', async () => {
86
- const { srv, calls } = sseServer((n) => {
87
- if (n === 1) {
88
- return [
89
- 'data: ' + JSON.stringify({ choices: [{ delta: { content: 'Merhaba dun' } }] }) + '\n\n',
90
- 'data: ' + JSON.stringify({ choices: [{ delta: { content: 'ya basladi' }, finish_reason: 'length' }] }) + '\n\n',
91
- 'data: [DONE]\n\n',
92
- ];
93
- }
94
- return [
95
- 'data: ' + JSON.stringify({ choices: [{ delta: { content: 'yor devam' } }] }) + '\n\n',
96
- 'data: ' + JSON.stringify({ choices: [{ delta: {}, finish_reason: 'stop' }] }) + '\n\n',
97
- 'data: [DONE]\n\n',
98
- ];
99
- });
100
- await new Promise((r) => srv.listen(0, '127.0.0.1', r));
101
- try {
102
- const port = srv.address().port;
103
- const sel = { url: `http://127.0.0.1:${port}/v1/chat/completions`, key: 'k', model: 'm' };
104
- const deltas = [];
105
- const r = await chatStreamAuto(sel, { messages: [{ role: 'user', content: 'selam' }] }, { onDelta: (d) => deltas.push(d) });
106
- assert.equal(r.content, 'Merhaba dunya basladiyor devam');
107
- assert.equal(r.finishReason, 'stop');
108
- assert.equal(calls.length, 2);
109
- // 2. turda devam mesajları eklenmiş olmalı (orijinal dizi kopyalanır)
110
- const m2 = calls[1];
111
- assert.equal(m2[m2.length - 2].role, 'assistant');
112
- assert.equal(m2[m2.length - 1].role, 'user');
113
- assert.match(m2[m2.length - 1].content, /DEVAM/);
114
- // streaming kesintisiz: 3 delta
115
- assert.deepEqual(deltas.join(''), r.content);
116
- } finally {
117
- srv.close();
118
- }
119
- });
120
-
121
- test('chatStreamAuto: normal (stop) yanıtta hiç devam turu açılmaz', async () => {
122
- const { srv, calls } = sseServer(() => [
123
- 'data: ' + JSON.stringify({ choices: [{ delta: { content: 'tam cevap' }, finish_reason: 'stop' }] }) + '\n\n',
124
- 'data: [DONE]\n\n',
125
- ]);
126
- await new Promise((r) => srv.listen(0, '127.0.0.1', r));
127
- try {
128
- const port = srv.address().port;
129
- const sel = { url: `http://127.0.0.1:${port}/v1/chat/completions`, key: 'k', model: 'm' };
130
- const r = await chatStreamAuto(sel, { messages: [{ role: 'user', content: 'selam' }] });
131
- assert.equal(r.content, 'tam cevap');
132
- assert.equal(calls.length, 1);
133
- } finally {
134
- srv.close();
135
- }
136
- });
137
-
138
- /* ---------- ağ kopması dayanıklılığı (#retry) ---------- */
139
-
140
- test('isNetworkError sınıflandırması', () => {
141
- const net = new Error('fetch failed');
142
- assert.equal(isNetworkError(net), true, 'status yok → ağ hatası');
143
- const http = new Error('HTTP 503');
144
- http.status = 503;
145
- assert.equal(isNetworkError(http), false, 'status var → HTTP hatası');
146
- const abort = new Error('iptal');
147
- abort.name = 'AbortError';
148
- assert.equal(isNetworkError(abort), false, 'abort ağ hatası sayılmaz');
149
- assert.equal(isNetworkError(null), false);
150
- });
151
-
152
- test('withRetries: internet kapalıyken probe bekler, dönünce başarır', async () => {
153
- let online = false;
154
- setNetProbe(() => online);
155
- setTimeout(() => { online = true; }, 30);
156
- let calls = 0;
157
- const t0 = Date.now();
158
- const r = await withRetries(async () => {
159
- calls++;
160
- if (calls === 1) throw new Error('fetch failed');
161
- return 'ok';
162
- });
163
- assert.equal(r, 'ok');
164
- assert.equal(calls, 2);
165
- assert.ok(Date.now() - t0 > 2000, 'probe çevrimdışı dediği için bekledi');
166
- setNetProbe(null);
167
- });
168
-
169
- test('akış ortasında bağlantı koparsa: kısmi metinle devam turu açılır (hata balonu yok)', async () => {
170
- const http = require('node:http');
171
- const calls = [];
172
- const srv = http.createServer((req, res) => {
173
- let body = '';
174
- req.on('data', (c) => (body += c));
175
- req.on('end', () => {
176
- calls.push(JSON.parse(body).messages);
177
- if (calls.length === 1) {
178
- /* birkaç delta yaz, sonra soketi ORTADAN kopar */
179
- res.writeHead(200, { 'Content-Type': 'text/event-stream' });
180
- res.write('data: ' + JSON.stringify({ choices: [{ delta: { content: 'Merhaba du' } }] }) + '\n\n');
181
- setTimeout(() => res.destroy(), 30);
182
- return;
183
- }
184
- res.writeHead(200, { 'Content-Type': 'text/event-stream' });
185
- res.write('data: ' + JSON.stringify({ choices: [{ delta: { content: 'nya devam' } }] }) + '\n\n');
186
- res.write('data: ' + JSON.stringify({ choices: [{ delta: {}, finish_reason: 'stop' }] }) + '\n\n');
187
- res.write('data: [DONE]\n\n');
188
- res.end();
189
- });
190
- });
191
- await new Promise((r) => srv.listen(0, '127.0.0.1', r));
192
- try {
193
- const port = srv.address().port;
194
- const sel = { url: `http://127.0.0.1:${port}/v1/chat/completions`, key: 'k', model: 'm' };
195
- const r = await chatStreamAuto(sel, { messages: [{ role: 'user', content: 'selam' }] });
196
- assert.equal(r.content, 'Merhaba dunya devam', 'kısmi + devam birleşti');
197
- assert.equal(r.finishReason, 'stop');
198
- assert.equal(calls.length, 2, 'devam turu açıldı');
199
- const m2 = calls[1];
200
- assert.equal(m2[m2.length - 2].role, 'assistant');
201
- assert.match(m2[m2.length - 1].content, /DEVAM/);
202
- } finally {
203
- srv.close();
204
- }
205
- });