beast-agent 0.15.0
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.
- package/.env.example +7 -0
- package/LICENSE +21 -0
- package/README.md +94 -0
- package/assets/app.ico +0 -0
- package/assets/tray.png +0 -0
- package/bin/beast-agent.js +23 -0
- package/config.example.yaml +26 -0
- package/eng.traineddata +0 -0
- package/package.json +101 -0
- package/scripts/swap-electron.js +40 -0
- package/src/agent/bus.js +384 -0
- package/src/agent/computeruse.js +200 -0
- package/src/agent/config.js +284 -0
- package/src/agent/engine.js +3118 -0
- package/src/agent/kb.js +123 -0
- package/src/agent/llm.js +219 -0
- package/src/agent/logger.js +90 -0
- package/src/agent/memory.js +383 -0
- package/src/agent/pdf.js +20 -0
- package/src/agent/scripts/__pycache__/websearch.cpython-312.pyc +0 -0
- package/src/agent/scripts/news.py +113 -0
- package/src/agent/scripts/websearch.py +225 -0
- package/src/agent/skills.js +522 -0
- package/src/agent/tokens.js +39 -0
- package/src/agent/tools.js +911 -0
- package/src/agent/usage.js +125 -0
- package/src/agent/watchers.js +312 -0
- package/src/agent/watext.js +75 -0
- package/src/agent/whatsapp.js +494 -0
- package/src/cron.js +245 -0
- package/src/main.js +3622 -0
- package/src/preload.js +122 -0
- package/src/renderer/browserPreload.js +73 -0
- package/src/renderer/i18n.js +757 -0
- package/src/renderer/index.html +227 -0
- package/src/renderer/renderer.js +3250 -0
- package/src/renderer/style.css +1559 -0
- package/tests/approval.test.js +56 -0
- package/tests/bg-jobs.test.js +315 -0
- package/tests/bus.test.js +46 -0
- package/tests/computeruse-context.test.js +69 -0
- package/tests/cron.test.js +95 -0
- package/tests/engine.test.js +140 -0
- package/tests/eval.test.js +91 -0
- package/tests/fallout.test.js +188 -0
- package/tests/llm.test.js +56 -0
- package/tests/memory.test.js +33 -0
- package/tests/memoryloop.test.js +26 -0
- package/tests/notegen.test.js +63 -0
- package/tests/owner.test.js +32 -0
- package/tests/python.test.js +75 -0
- package/tests/scenarios.json +81 -0
- package/tests/sessioncode.test.js +50 -0
- package/tests/setup.js +9 -0
- package/tests/tokens.test.js +42 -0
- package/tests/tools.test.js +84 -0
- package/tests/usage.test.js +44 -0
- package/tests/v11.test.js +76 -0
- package/tests/watchers.test.js +206 -0
- package/tests/watext.test.js +53 -0
- package/tests/whatsapp.test.js +103 -0
- package/tests/wherewasi.test.js +40 -0
|
@@ -0,0 +1,140 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,188 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
require('./setup');
|
|
4
|
+
const test = require('node:test');
|
|
5
|
+
const assert = require('node:assert');
|
|
6
|
+
const { withRetries, friendlyError } = 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
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
require('./setup');
|
|
4
|
+
const test = require('node:test');
|
|
5
|
+
const assert = require('node:assert');
|
|
6
|
+
const memory = require('../src/agent/memory');
|
|
7
|
+
|
|
8
|
+
test('append + entries turu', () => {
|
|
9
|
+
memory.append('Kullanıcı FxPro MT5 kullanıyor');
|
|
10
|
+
memory.append('Beast hızlı olmalı, gereksiz sormaz');
|
|
11
|
+
const list = memory.entries();
|
|
12
|
+
assert.ok(list.length >= 2);
|
|
13
|
+
assert.ok(list.some((l) => l.includes('FxPro')));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('search ilgili kaydı bulur (Türkçe büyük/küçük + diakritik duyarsız)', () => {
|
|
17
|
+
memory.append('Kullanıcının favori editörü VS Code');
|
|
18
|
+
const hits = memory.search('favori editör nedir?');
|
|
19
|
+
assert.ok(hits.length >= 1);
|
|
20
|
+
assert.ok(hits[0].text.toLowerCase().includes('editör') || hits[0].text.toLowerCase().includes('edıtor'));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('relevantFor alakasız sorguda bile son kayıtları döndürür', () => {
|
|
24
|
+
const out = memory.relevantFor('zzz bilinmeyen kelime xyzq');
|
|
25
|
+
assert.ok(out.length > 0);
|
|
26
|
+
assert.ok(out.startsWith('- '));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('relevantFor charCap\u0027e uyar', () => {
|
|
30
|
+
for (let i = 0; i < 30; i++) memory.append('uzun kayıt numara ' + i + ' ' + 'x'.repeat(200));
|
|
31
|
+
const out = memory.relevantFor('kayıt');
|
|
32
|
+
assert.ok(out.length <= 2600);
|
|
33
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
require('./setup');
|
|
4
|
+
const test = require('node:test');
|
|
5
|
+
const assert = require('node:assert');
|
|
6
|
+
const memory = require('../src/agent/memory');
|
|
7
|
+
|
|
8
|
+
test('#v13 memory döngüsü: append dedup + cap hygiene', () => {
|
|
9
|
+
// temiz başlangıç
|
|
10
|
+
const f = require('path').join(memory.memDir(), 'MEMORY.md');
|
|
11
|
+
require('fs').rmSync(f, { force: true });
|
|
12
|
+
|
|
13
|
+
const r1 = memory.append('Kullanıcının projeleri: Beast Agent, GOLD trading');
|
|
14
|
+
assert.ok(r1.ok);
|
|
15
|
+
// aynı bilgi farklı ifadeyle — dup yakalanmalı (fold+normalize)
|
|
16
|
+
const r2 = memory.append('kullanıcının PROJELERİ: beast agent, gold trading!');
|
|
17
|
+
assert.ok(r2.ok);
|
|
18
|
+
assert.equal(r2.duplicate, true);
|
|
19
|
+
|
|
20
|
+
const list = memory.entries().filter((l) => /beast agent/i.test(l));
|
|
21
|
+
assert.equal(list.length, 1, 'dup tekilleşmeli');
|
|
22
|
+
|
|
23
|
+
// hygiene çalışıyor mu
|
|
24
|
+
const h = memory.hygiene({ maxAgeDays: 0 });
|
|
25
|
+
assert.ok(h.ok);
|
|
26
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
/* chatOnce'ı stub'la: engine yüklenmeden önce llm modülünü patch'le
|
|
11
|
+
(engine destructure anında referansı alır) */
|
|
12
|
+
const llm = require('../src/agent/llm');
|
|
13
|
+
let stubResponse = 'Not: kullanıcı Beast Agent geliştiriyor. Karar: not sistemi çalışmalı.';
|
|
14
|
+
let stubCalls = 0;
|
|
15
|
+
llm.chatOnce = async () => {
|
|
16
|
+
stubCalls++;
|
|
17
|
+
return { content: stubResponse };
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const { Engine } = require('../src/agent/engine');
|
|
21
|
+
|
|
22
|
+
function makeEngine(extra = {}) {
|
|
23
|
+
const sessionsDir = fs.mkdtempSync(path.join(os.tmpdir(), 'beast-sess-'));
|
|
24
|
+
const workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'beast-ws-'));
|
|
25
|
+
const eng = new Engine({}, { sessionsDir, workspace, customProviders: [], emit: () => {}, ...extra });
|
|
26
|
+
eng.sel = { provider: 'test', providerName: 'Test', model: 'test-model', sel: 'test::test-model' };
|
|
27
|
+
return eng;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
test('oturum notları: 14+ mesajda not üretilir ve listNotes görünür', async () => {
|
|
31
|
+
const eng = makeEngine();
|
|
32
|
+
const s = eng.createSession();
|
|
33
|
+
const full = eng._load(s.id);
|
|
34
|
+
for (let i = 0; i < 20; i++) {
|
|
35
|
+
full.messages.push({ role: 'user', content: 'mesaj ' + i });
|
|
36
|
+
full.messages.push({ role: 'assistant', content: 'cevap ' + i });
|
|
37
|
+
}
|
|
38
|
+
const r = await eng._updateSessionNotes(full, undefined);
|
|
39
|
+
assert.equal(r.ok, true, 'not üretimi başarılı olmalı, hata: ' + (r && r.reason));
|
|
40
|
+
assert.equal(stubCalls, 1);
|
|
41
|
+
assert.ok(full.notes && full.notes.length, 'notes yazıldı');
|
|
42
|
+
|
|
43
|
+
const list = eng.listNotes();
|
|
44
|
+
assert.equal(list.length, 1, 'listNotes notu dönmeli');
|
|
45
|
+
assert.equal(list[0].id, s.id);
|
|
46
|
+
assert.ok(list[0].notes.includes('Not:'));
|
|
47
|
+
|
|
48
|
+
/* diskte notes satırı da olmalı */
|
|
49
|
+
const raw = fs.readFileSync(eng._file(s.id), 'utf8');
|
|
50
|
+
assert.ok(raw.includes('"t":"notes"'), 'notes satırı dosyada');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('oturum notları: eşiği doldurmayan oturum bekleme döner, hata değil', async () => {
|
|
54
|
+
const eng = makeEngine();
|
|
55
|
+
const s = eng.createSession();
|
|
56
|
+
const full = eng._load(s.id);
|
|
57
|
+
full.messages.push({ role: 'user', content: 'selam' });
|
|
58
|
+
full.messages.push({ role: 'assistant', content: 'merhaba' });
|
|
59
|
+
const r = await eng._updateSessionNotes(full, undefined);
|
|
60
|
+
assert.equal(r.ok, false);
|
|
61
|
+
assert.ok(/^bekleme/.test(r.reason));
|
|
62
|
+
assert.equal(stubCalls, 1, 'stub çağrısı artmadı');
|
|
63
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
require('./setup');
|
|
4
|
+
const test = require('node:test');
|
|
5
|
+
const assert = require('node:assert');
|
|
6
|
+
const memory = require('../src/agent/memory');
|
|
7
|
+
|
|
8
|
+
/* owner normalize mantığı main'in wa:allow:set handler'ında; burada saf
|
|
9
|
+
davranışı doğruluyoruz: yalnız ilk owner true kalır, diğerleri söner. */
|
|
10
|
+
test('#v13.1 owner: birden fazla owner adayı olsa da yalnız biri kazanır', () => {
|
|
11
|
+
const items = [
|
|
12
|
+
{ num: '905511111111', name: 'Aday1', owner: true, perm: 'all' },
|
|
13
|
+
{ num: '905522222222', name: 'Aday2', owner: true, perm: 'all' },
|
|
14
|
+
{ num: '905533333333', name: 'Misafir', perm: 'web' },
|
|
15
|
+
];
|
|
16
|
+
const out = [];
|
|
17
|
+
let ownerCount = 0;
|
|
18
|
+
for (const item of items) {
|
|
19
|
+
const wantsOwner = !!item.owner;
|
|
20
|
+
const isOwner = wantsOwner && ownerCount === 0;
|
|
21
|
+
if (isOwner) ownerCount++;
|
|
22
|
+
out.push({ num: item.num.replace(/\D/g, ''), name: item.name, owner: isOwner });
|
|
23
|
+
}
|
|
24
|
+
assert.equal(out.filter((o) => o.owner).length, 1);
|
|
25
|
+
assert.equal(out[0].owner, true); // ilk aday kazanır
|
|
26
|
+
assert.equal(out[1].owner, false); // ikinci aday sahipsiz
|
|
27
|
+
|
|
28
|
+
/* memory tarafı: sahip kuralı + USER.md */
|
|
29
|
+
const r = memory.addRule("Beast'in SAHİBİ Aday1 (+905511111111). Onun talepleri önceliklidir.");
|
|
30
|
+
assert.ok(r.ok || r.duplicate);
|
|
31
|
+
assert.ok(memory.listRules().some((x) => x.includes('SAHİBİ')));
|
|
32
|
+
});
|