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.
Files changed (62) hide show
  1. package/.env.example +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +94 -0
  4. package/assets/app.ico +0 -0
  5. package/assets/tray.png +0 -0
  6. package/bin/beast-agent.js +23 -0
  7. package/config.example.yaml +26 -0
  8. package/eng.traineddata +0 -0
  9. package/package.json +101 -0
  10. package/scripts/swap-electron.js +40 -0
  11. package/src/agent/bus.js +384 -0
  12. package/src/agent/computeruse.js +200 -0
  13. package/src/agent/config.js +284 -0
  14. package/src/agent/engine.js +3118 -0
  15. package/src/agent/kb.js +123 -0
  16. package/src/agent/llm.js +219 -0
  17. package/src/agent/logger.js +90 -0
  18. package/src/agent/memory.js +383 -0
  19. package/src/agent/pdf.js +20 -0
  20. package/src/agent/scripts/__pycache__/websearch.cpython-312.pyc +0 -0
  21. package/src/agent/scripts/news.py +113 -0
  22. package/src/agent/scripts/websearch.py +225 -0
  23. package/src/agent/skills.js +522 -0
  24. package/src/agent/tokens.js +39 -0
  25. package/src/agent/tools.js +911 -0
  26. package/src/agent/usage.js +125 -0
  27. package/src/agent/watchers.js +312 -0
  28. package/src/agent/watext.js +75 -0
  29. package/src/agent/whatsapp.js +494 -0
  30. package/src/cron.js +245 -0
  31. package/src/main.js +3622 -0
  32. package/src/preload.js +122 -0
  33. package/src/renderer/browserPreload.js +73 -0
  34. package/src/renderer/i18n.js +757 -0
  35. package/src/renderer/index.html +227 -0
  36. package/src/renderer/renderer.js +3250 -0
  37. package/src/renderer/style.css +1559 -0
  38. package/tests/approval.test.js +56 -0
  39. package/tests/bg-jobs.test.js +315 -0
  40. package/tests/bus.test.js +46 -0
  41. package/tests/computeruse-context.test.js +69 -0
  42. package/tests/cron.test.js +95 -0
  43. package/tests/engine.test.js +140 -0
  44. package/tests/eval.test.js +91 -0
  45. package/tests/fallout.test.js +188 -0
  46. package/tests/llm.test.js +56 -0
  47. package/tests/memory.test.js +33 -0
  48. package/tests/memoryloop.test.js +26 -0
  49. package/tests/notegen.test.js +63 -0
  50. package/tests/owner.test.js +32 -0
  51. package/tests/python.test.js +75 -0
  52. package/tests/scenarios.json +81 -0
  53. package/tests/sessioncode.test.js +50 -0
  54. package/tests/setup.js +9 -0
  55. package/tests/tokens.test.js +42 -0
  56. package/tests/tools.test.js +84 -0
  57. package/tests/usage.test.js +44 -0
  58. package/tests/v11.test.js +76 -0
  59. package/tests/watchers.test.js +206 -0
  60. package/tests/watext.test.js +53 -0
  61. package/tests/whatsapp.test.js +103 -0
  62. package/tests/wherewasi.test.js +40 -0
@@ -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 Engine = require('../src/agent/engine');
7
+
8
+ function mkEngine(approvals) {
9
+ const path = require('path');
10
+ const os = require('os');
11
+ return new Engine(
12
+ {},
13
+ {
14
+ sessionsDir: path.join(os.tmpdir(), 'beast-appr-test-' + Date.now()),
15
+ approvals,
16
+ }
17
+ );
18
+ }
19
+
20
+ test('onay kapısı: onay gelmezse riskli araç çalışmaz, onay gelirse çalışır', async () => {
21
+ let asked = null;
22
+ const eng = mkEngine({
23
+ request: async (q) => {
24
+ asked = q;
25
+ return false; // reddet
26
+ },
27
+ });
28
+
29
+ // GATE: run_command dıştan görünür şekilde kapıda durmalı
30
+ assert.ok(Engine.RISKY_TOOLS.has('run_command'));
31
+ assert.ok(Engine.RISKY_TOOLS.has('write_file'));
32
+ assert.ok(Engine.RISKY_TOOLS.has('email_send'));
33
+ assert.ok(!Engine.RISKY_TOOLS.has('web_search')); // güvenli araç kapıya takılmaz
34
+
35
+ const out = await eng._execTool('run_command', { command: 'echo hi' }, null, 's1');
36
+ const obj = JSON.parse(out);
37
+ assert.equal(obj.ok, false);
38
+ assert.match(obj.error, /ONAYLAMADI/);
39
+ assert.equal(asked.tool, 'run_command');
40
+ assert.equal(asked.sessionId, 's1');
41
+
42
+ // izin verilirse araç gerçekten çalışır (echo)
43
+ const eng2 = mkEngine({ request: async () => true });
44
+ const out2 = await eng2._execTool('run_command', { command: 'echo ok' }, null, 's2');
45
+ const obj2 = JSON.parse(out2);
46
+ if (obj2.ok === false && /yok/.test(String(obj2.error))) {
47
+ assert.ok(true); // ortamda shell yoksa kayıtlı davranış
48
+ } else {
49
+ assert.ok(JSON.stringify(obj2).includes('ok'));
50
+ }
51
+
52
+ // kapı hiç verilmediyse eski davranış — doğrudan çalışır
53
+ const eng3 = mkEngine(null);
54
+ const out3 = await eng3._execTool('run_command', { command: 'echo three' }, null, 's3');
55
+ assert.ok(out3.length > 0);
56
+ });
@@ -0,0 +1,315 @@
1
+ 'use strict';
2
+
3
+ /* #14 paralel ajanlar + CEO modu testleri (ağ çağrısı yok) */
4
+
5
+ const test = require('node:test');
6
+ const assert = require('node:assert');
7
+ const fs = require('fs');
8
+ const os = require('os');
9
+ const path = require('path');
10
+ const Engine = require('../src/agent/engine');
11
+
12
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
13
+
14
+ function tmpEngine() {
15
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'beast-bg-'));
16
+ const events = [];
17
+ const eng = new Engine({}, {
18
+ sessionsDir: dir,
19
+ emit: (ev) => events.push(ev),
20
+ });
21
+ return { eng, events, dir };
22
+ }
23
+
24
+ test('CEO modu varsayılan açık; setCeoMode çalışır', () => {
25
+ const { eng } = tmpEngine();
26
+ assert.strictEqual(eng.ceoMode, true);
27
+ eng.setCeoMode(false);
28
+ assert.strictEqual(eng.ceoMode, false);
29
+ eng.setCeoMode(true);
30
+ assert.strictEqual(eng.ceoMode, true);
31
+ });
32
+
33
+ test('CEO yasaklı araç seti uygulayıcıları kapsar, yönetici araçlarını değil', () => {
34
+ const E = Engine;
35
+ for (const t of ['run_command', 'write_file', 'read_file', 'web_search', 'browser_click', 'computer_act', 'delegate_task']) {
36
+ assert.ok(E.CEO_EXEC_TOOLS.has(t), t);
37
+ }
38
+ for (const t of ['memory_write', 'run_background', 'tasks_list', 'task_status', 'set_reminder']) {
39
+ assert.ok(!E.CEO_EXEC_TOOLS.has(t), t);
40
+ }
41
+ /* arka plan ajanı yönetim araçlarını görmemeli — sonsuz alt dal zinciri olmasın */
42
+ assert.ok(E.BG_HIDDEN_TOOLS.has('run_background'));
43
+ });
44
+
45
+ test('buildSystem CEO modunda CEO bloğu içerir; kapalıyken içermez', () => {
46
+ const { eng } = tmpEngine();
47
+ const onSys = eng.buildSystem('merhaba');
48
+ assert.ok(onSys.includes('# CEO MODU'), 'ceo bloğu');
49
+ assert.ok(onSys.includes('PARALEL ajana devret'));
50
+ eng.setCeoMode(false);
51
+ const offSys = eng.buildSystem('merhaba');
52
+ assert.ok(!offSys.includes('# CEO MODU'));
53
+ assert.ok(offSys.includes('delegate_task ile devret'));
54
+ });
55
+
56
+ test('runBackground kayıt açar ve iş hata durumunda error olarak kapanır', async () => {
57
+ const { eng, events } = tmpEngine();
58
+ const r = eng.runBackground('oturum-1', 'klasördeki pdfleri listele', 'PDF Taraması');
59
+ assert.ok(r.ok);
60
+ assert.ok(r.backgroundId);
61
+
62
+ let jobs = eng.listBgJobs();
63
+ assert.strictEqual(jobs.length, 1);
64
+ assert.strictEqual(jobs[0].status, 'running');
65
+ assert.strictEqual(jobs[0].title, 'PDF Taraması');
66
+ assert.ok(jobs[0].task.includes('pdf'));
67
+ assert.strictEqual(jobs[0].parentId, 'oturum-1');
68
+
69
+ await sleep(200); // send tetiklenir → model yok → _run error yakalar
70
+
71
+ jobs = eng.listBgJobs();
72
+ assert.strictEqual(jobs[0].status, 'error');
73
+ assert.ok(/Model yapılandırılmadı/.test(jobs[0].error || ''), 'hata metni yakalandı: ' + jobs[0].error);
74
+ assert.ok(events.some((e) => e.type === 'agents' && Array.isArray(e.jobs)));
75
+ });
76
+
77
+ test('_execTool tasks_list + task_status + task_cancel yanıt verir', async () => {
78
+ const { eng } = tmpEngine();
79
+ eng.runBackground('p2', 'araştırma görevi', 'Araştırma');
80
+ await sleep(150); // iş biter (error)
81
+
82
+ const lst = JSON.parse(await eng._execTool('tasks_list', {}, {}));
83
+ assert.ok(lst.ok && lst.jobs.length >= 1);
84
+ const id = lst.jobs[0].id;
85
+
86
+ const det = JSON.parse(await eng._execTool('task_status', { id }, {}));
87
+ assert.ok(det.ok);
88
+ assert.strictEqual(det.job.id, id);
89
+ assert.ok(Array.isArray(det.messages));
90
+
91
+ /* çalışan ajan olmadığından cancel false döner */
92
+ const c = JSON.parse(await eng._execTool('task_cancel', { id }, {}));
93
+ assert.strictEqual(c.ok, false);
94
+ });
95
+
96
+ test('bgDetail kod ile de bulunur; mesaj özeti GÖREV/AJAN etiketlidir', async () => {
97
+ const { eng } = tmpEngine();
98
+ const r = eng.runBackground('p3', 'görev metni', 'Kodlu İş');
99
+ await sleep(150);
100
+ const job = eng.listBgJobs()[0];
101
+ const byId = eng.bgDetail(job.id);
102
+ assert.ok(byId.ok);
103
+ const byCode = eng.bgDetail(job.code);
104
+ assert.ok(byCode.ok && byCode.job.id === job.id);
105
+ const missing = eng.bgDetail('XXXXXX99');
106
+ assert.strictEqual(missing.ok, false);
107
+ });
108
+
109
+ /* ---------- superyorizyon (#15) ---------- */
110
+
111
+ const MIN = 60000;
112
+
113
+ function fakeJob(over) {
114
+ return {
115
+ status: 'running',
116
+ startedAt: new Date(Date.now() - 10 * MIN).toISOString(),
117
+ lastActivityAt: new Date(Date.now() - 10 * MIN).toISOString(),
118
+ lastNudgeAt: null,
119
+ ...over,
120
+ };
121
+ }
122
+
123
+ test('superviseReason: takılan stuck, çalışan uzun iş once wrapup (wrapAt sonrası long), yeni uyarılan null', () => {
124
+ assert.strictEqual(Engine.superviseReason(fakeJob(), Date.now()), 'stuck');
125
+
126
+ assert.strictEqual(
127
+ Engine.superviseReason(fakeJob({ lastActivityAt: new Date(Date.now() - 30 * 1000).toISOString() }), Date.now()),
128
+ 'wrapup'
129
+ ); // aktivite sürüyor ama 10 dk koştu — önce zarif bitirme uyarısı
130
+
131
+ assert.strictEqual(
132
+ Engine.superviseReason(fakeJob({ wrapAt: new Date().toISOString(), lastActivityAt: new Date(Date.now() - 30 * 1000).toISOString() }), Date.now()),
133
+ 'long'
134
+ ); // wrap-up uyarısı verildi, hâlâ bitmedi → CEO ara kontrolü
135
+
136
+ assert.strictEqual(
137
+ Engine.superviseReason(fakeJob({ startedAt: new Date(Date.now() - 1 * MIN).toISOString() }), Date.now()),
138
+ null
139
+ ); // henüz çok yeni
140
+
141
+ assert.strictEqual(
142
+ Engine.superviseReason(fakeJob({ lastNudgeAt: new Date(Date.now() - 1 * MIN).toISOString() }), Date.now()),
143
+ null
144
+ ); // az önce uyarıldı — cooldown
145
+
146
+ assert.strictEqual(Engine.superviseReason(fakeJob({ status: 'done' }), Date.now()), null);
147
+ });
148
+
149
+ test('emit sarmalayıcı ajan olayında lastActivityAt günceller', () => {
150
+ const { eng } = tmpEngine();
151
+ eng.runBackground('pX', 'izleme', 'İz İş');
152
+ const job = eng.listBgJobs()[0];
153
+ /* zaman damgasını yapay eskilt — ms çözünürlüğü eşitliğe düşmesin */
154
+ eng._bgJobs.get(job.id).lastActivityAt = new Date(Date.now() - 5 * MIN).toISOString();
155
+ const eski = eng._bgJobs.get(job.id).lastActivityAt;
156
+ eng.emit({ type: 'tool-start', sessionId: job.id, name: 'run_command', args: {} });
157
+ const sonra = eng._bgJobs.get(job.id).lastActivityAt;
158
+ assert.ok(new Date(sonra) > new Date(eski), 'aktivite tazelendi');
159
+ /* yabancı oturum etkilenmez */
160
+ eng.emit({ type: 'token', sessionId: 'baskasi', delta: 'x' });
161
+ assert.ok(true);
162
+ });
163
+
164
+ test('_supervise önce ajana ÖZ-KURTARMA verir, haklar bitince CEO\'ya uyarır', async () => {
165
+ const { eng } = tmpEngine();
166
+ const r = eng.runBackground('parentOk', 'uzun araştırma', 'Uzun İş');
167
+ const job = eng._bgJobs.get(r.backgroundId);
168
+ job.startedAt = new Date(Date.now() - 10 * MIN).toISOString();
169
+ job.lastActivityAt = job.startedAt;
170
+ eng._supervise();
171
+
172
+ /* 1. aşama: kurtarma emri ajanın KENDİ oturumuna düşer — CEO meşgul edilmez */
173
+ const bgS = eng._load(r.backgroundId);
174
+ assert.ok(bgS.messages.some((m) => String(m.content || '').includes('[ÖZ-KURTARMA 1/')), 'ajana öz-kurtarma düştü');
175
+ assert.strictEqual(job.checks, 1);
176
+ assert.strictEqual(job.fixes, 1);
177
+ const s0 = eng._load('parentOk');
178
+ assert.ok(!s0.messages.some((m) => String(m.content || '').includes('[SUPERYORIZON]')), 'CEO daha devreye girmedi');
179
+
180
+ /* cooldown: ikinci çağrı hemen tekrar müdahale etmez */
181
+ eng._supervise();
182
+ assert.strictEqual(job.checks, 1);
183
+
184
+ /* 2. aşama: öz-kurtarma hakları bitmiş + iş hâlâ koşuyor (wrap-up zaten
185
+ verildi, aktif) → CEO uyarısı ('long') */
186
+ job.status = 'running';
187
+ job.endedAt = null;
188
+ job.fixes = 2;
189
+ job.wrapAt = new Date().toISOString();
190
+ job.lastActivityAt = new Date(Date.now() - 30 * 1000).toISOString();
191
+ job.lastNudgeAt = new Date(Date.now() - 5 * MIN).toISOString();
192
+ eng._supervise();
193
+ await sleep(250);
194
+
195
+ const s = eng._load('parentOk');
196
+ const got = s.messages.some((m) => String(m.content || '').includes('[SUPERYORIZON] "Uzun İş"'));
197
+ assert.ok(got, 'üst oturuma uyarı düştü');
198
+ assert.strictEqual(job.checks, 2);
199
+ });
200
+
201
+ /* ---------- #17 kalıcı ajan geçmişi ---------- */
202
+
203
+ test('ajan geçmişi restart sonrası durur; sol listede GİZLİ, kayıt diske yazar; elle silinir', () => {
204
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'beast-persist-'));
205
+ const eng1 = new Engine({}, { sessionsDir: dir, emit: () => {} });
206
+ const r = eng1.runBackground('p1', 'masaüstündeki dosyaları listele', 'Tarama İş');
207
+
208
+ /* ajan oturumu sol sohbet listesine karışmaz */
209
+ assert.ok(!eng1.listSessions().some((s) => s.id === r.backgroundId), 'sol liste ajan sohbetini göstermez');
210
+
211
+ /* uygulama yeniden başlaması: yeni motor örneği aynı dizinden */
212
+ const eng2 = new Engine({}, { sessionsDir: dir, emit: () => {} });
213
+ const jobs = eng2.listBgJobs();
214
+ assert.strictEqual(jobs.length, 1);
215
+ assert.strictEqual(jobs[0].status, 'aborted', 'running → aborted migrasyonu');
216
+ assert.ok(String(jobs[0].error || '').includes('kapatıldı'));
217
+
218
+ /* hâlâ sol listede yok */
219
+ assert.ok(!eng2.listSessions().some((s) => s.id === r.backgroundId), 'restart sonrası da gizli');
220
+
221
+ /* elle silme: oturum dosyası + iş kaydı birlikte gider, hayalet yazılmaz */
222
+ eng2.deleteSession(r.backgroundId);
223
+ assert.strictEqual(eng2.listBgJobs().length, 0);
224
+ assert.ok(!fs.existsSync(path.join(dir, r.backgroundId + '.jsonl')));
225
+ const raw = JSON.parse(fs.readFileSync(path.join(dir, 'bg-jobs.json'), 'utf8'));
226
+ assert.ok(!raw.jobs.some((j) => j.id === r.backgroundId), 'silinen iş diske geri yazılmaz');
227
+
228
+ /* gerçek uygulamada tek motor olur; burada eski örneği de uyumlaştır */
229
+ eng1._bgJobs.delete(r.backgroundId);
230
+
231
+ /* eski motorun geç tetiklenen send'i kaydı diriltmesin */
232
+ return new Promise((resolve) => setTimeout(resolve, 300)).then(() => {
233
+ const raw2 = JSON.parse(fs.readFileSync(path.join(dir, 'bg-jobs.json'), 'utf8'));
234
+ assert.ok(!raw2.jobs.some((j) => j.id === r.backgroundId), 'geç gelen _bgEmit hayır demedi');
235
+ });
236
+ });
237
+
238
+ /* ---------- #18 fan-out + #5 eşzamanlılık kuyruğu ---------- */
239
+
240
+ test('fan-out: adımlar aynı grupta açılır, hepsi bitince TEK birleşik rapor düşer', async () => {
241
+ const { eng } = tmpEngine();
242
+ const r = eng.runBackgroundMany(
243
+ 'pg',
244
+ [
245
+ { title: 'TR', task: 'tr haberlerini topla' },
246
+ { title: 'DÜNYA', task: 'dünya haberlerini topla' },
247
+ { title: 'EKONOMİ', task: 'ekonomi haberlerini topla' },
248
+ ],
249
+ 'Haber Taraması'
250
+ );
251
+ assert.ok(r.ok, 'fan-out başladı');
252
+ assert.strictEqual(r.ids.length, 3);
253
+ const groups = new Set([...eng._bgJobs.values()].map((j) => j.groupId));
254
+ assert.strictEqual(groups.size, 1);
255
+ for (const id of r.ids) await eng.reportBackgroundDone(id);
256
+
257
+ const s = eng._load('pg');
258
+ const got = s.messages.some((m) => String(m.content || '').includes('[ARKA PLAN GRUP BİTTİ: 3/3]'));
259
+ assert.ok(got, 'birleşik rapor düştü');
260
+ });
261
+
262
+ test('#5 eşzamanlılık limiti: fazlası kuyrukta bekler, slot boşalınca FIFO başlar', async () => {
263
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'beast-q-'));
264
+ const eng = new Engine({}, { sessionsDir: dir, emit: () => {}, bgLimit: 2 });
265
+ /* model yok → gerçek _run hata ile derhal bitmesin: gönderimi izlemeye al */
266
+ const sent = [];
267
+ eng.send = (sid, payload) => { sent.push(String(sid)); return true; };
268
+ const ids = [];
269
+ for (let i = 0; i < 4; i++) {
270
+ const r = eng.runBackground('pq', 'iş ' + i, 'İ' + i);
271
+ ids.push(r.backgroundId);
272
+ }
273
+ await sleep(120); // admit zamanlayıcıları
274
+ let st = [...eng._bgJobs.values()].map((j) => j.status).sort();
275
+ assert.deepEqual(st, ['queued', 'queued', 'running', 'running']);
276
+ assert.strictEqual(sent.length, 2);
277
+
278
+ /* slot açılır → sıradaki queued iş running olur ve gönderilir */
279
+ eng._bgFinish(ids[0], 'aborted');
280
+ await sleep(30);
281
+ const queued = [...eng._bgJobs.values()].filter((j) => j.status === 'queued').length;
282
+ const running = [...eng._bgJobs.values()].filter((j) => j.status === 'running').length;
283
+ assert.strictEqual(queued, 1);
284
+ assert.strictEqual(running, 2);
285
+ assert.strictEqual(sent.length, 3);
286
+ });
287
+
288
+ /* ---------- /stop anahtarı ---------- */test('stopAll: koşan turları keser, paralel ajanı aborted işaretler', async () => {
289
+ const { eng } = tmpEngine();
290
+ const r = eng.runBackground('pS', 'koşan iş', 'Koşu');
291
+ const id = r.backgroundId;
292
+
293
+ /* sahte koşan tur: ctrls'e AbortController koy */
294
+ const c1 = new AbortController();
295
+ const c2 = new AbortController();
296
+ eng.ctrls.set(id, c1); // paralel ajan
297
+ eng.ctrls.set('main-sid', c2); // masaüstü oturumu
298
+ /* bekleyen rapor kuyruğu da dolu olsun */
299
+ eng._pendingReports = [{ parentId: 'pS', text: 'eski rapor' }];
300
+
301
+ const n = eng.stopAll();
302
+
303
+ assert.strictEqual(n, 2);
304
+ assert.ok(c1.signal.aborted && c2.signal.aborted);
305
+ assert.strictEqual(eng._bgJobs.get(id).status, 'aborted');
306
+ assert.ok(eng._bgJobs.get(id).endedAt);
307
+ assert.strictEqual(eng._pendingReports.length, 0);
308
+ /* events'te agents yayını var */
309
+ assert.ok(eng.listBgJobs()[0].status === 'aborted');
310
+
311
+ /* ikinci çağrı idempotent: gerçek akışta _run finally ctrl'i siler */
312
+ eng.ctrls.delete(id);
313
+ eng.ctrls.delete('main-sid');
314
+ assert.strictEqual(eng.stopAll(), 0);
315
+ });
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+
3
+ require('./setup');
4
+ const test = require('node:test');
5
+ const assert = require('node:assert');
6
+
7
+ /* bus saf parçaları: decide/compareOp/emitEvent (hooks.notify stub'lı) */
8
+ const bus = require('../src/agent/bus');
9
+
10
+ test('bus: sayısal filtre op\u2019ları', () => {
11
+ assert.ok(bus.compareOp('lt', 2500, 2600));
12
+ assert.ok(bus.compareOp('gte', 2600, 2600));
13
+ assert.ok(!bus.compareOp('gt', 2599, 2600));
14
+ });
15
+
16
+ test('bus: decide — filtre + cooldown mantığı', () => {
17
+ const sub = { type: 'price:tick', op: 'lte', value: 2600, cooldownMin: 10, lastNotifiedAt: 0 };
18
+ const now = Date.now();
19
+ // koşul dışı
20
+ assert.equal(bus.decide(sub, { type: 'price:tick', value: 2700 }, now).hit, false);
21
+ // yanlış tip
22
+ assert.equal(bus.decide(sub, { type: 'mail:new' }, now).hit, false);
23
+ // koşul içi ilk kez → tetik
24
+ const d1 = bus.decide(sub, { type: 'price:tick', value: 2550 }, now);
25
+ assert.ok(d1.hit && !d1.cooled);
26
+ // hemen ardından → cooldown'a takılır ama hit'tir
27
+ sub.lastNotifiedAt = now;
28
+ const d2 = bus.decide(sub, { type: 'price:tick', value: 2500 }, now + 60000);
29
+ assert.ok(d2.hit && d2.cooled);
30
+ });
31
+
32
+ test('bus: addSub doğrulama + emitEvent dağıtımı', () => {
33
+ assert.equal(bus.addSub({ type: 'yok', sessionId: 's1' }).ok, false);
34
+ assert.equal(bus.addSub({ type: 'price:tick', sessionId: 's1' }).ok, true); // filtresiz genel abonelik
35
+
36
+ let notified = 0;
37
+ // notify hook'u start ile gelir; emitEvent hooks.notify'i kullanır
38
+ const before = bus.listSubs().length;
39
+ assert.ok(before >= 1);
40
+ // fiyat tick yolla — cooldown zaten 10dk varsayılan; ikinci çağrı cooled olmalı
41
+ const fired1 = bus.emitEvent('price:tick', { symbol: 'PAXGUSDT', price: 100 }, Date.now());
42
+ const fired2 = bus.emitEvent('price:tick', { symbol: 'PAXGUSDT', price: 101 }, Date.now() + 1000);
43
+ assert.ok(fired1 >= fired2); // en az ilk seferde tetiklenmiş olmalı
44
+ // temizle
45
+ for (const s of bus.listSubs()) bus.removeSub(s.id);
46
+ });
@@ -0,0 +1,69 @@
1
+ 'use strict';
2
+
3
+ require('./setup');
4
+ const test = require('node:test');
5
+ const assert = require('node:assert');
6
+ const path = require('path');
7
+ const os = require('os');
8
+ const fs = require('fs');
9
+ const Engine = require('../src/agent/engine');
10
+
11
+ /* computeruse: modül surface + koordinat/girdi kırpma (network'süz) */
12
+ test('#4 computer use: op doğrulama ve sınırlar', async () => {
13
+ const cu = require('../src/agent/computeruse');
14
+ const r1 = await cu.act('yokboyleop', {});
15
+ assert.equal(r1.ok, false);
16
+ const r2 = await cu.act('type', { text: '' });
17
+ assert.equal(r2.ok, false);
18
+ const r3 = await cu.act('key', { combo: '' });
19
+ assert.equal(r3.ok, false);
20
+ const r4 = await cu.act('scroll', { dy: 0 });
21
+ assert.equal(r4.ok, false);
22
+ /* bilinmeyen op dışındaki gerçek aksiyonlar PowerShell ister; çağrılmaz */
23
+ });
24
+
25
+ /* #6 bağlam sıkıştırma: notlar öze dönüşünce eski mesajlar diskten de düşer */
26
+ test('#6 compaction: _compactToNotes dosyayı metaya+notlara+son pencereye indirger', () => {
27
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'beast-cx-'));
28
+ const eng = new Engine({}, { sessionsDir: dir });
29
+ const s = eng.createSession();
30
+ /* createSession bir meta satırı yazdı; cache'i atla — ham dosyaya mesaj ekleyip
31
+ YENİ engine ile yüklüyoruz (cache trick yok) */
32
+ const file = path.join(dir, s.id + '.jsonl');
33
+ const lines = [JSON.stringify({ t: 'meta', id: s.id, code: 'ABC234', createdAt: new Date().toISOString() })];
34
+ for (let i = 0; i < 20; i++) {
35
+ lines.push(JSON.stringify({ t: 'msg', role: i % 2 ? 'assistant' : 'user', content: 'mesaj-' + i }));
36
+ }
37
+ fs.writeFileSync(file, lines.join('\n') + '\n');
38
+
39
+ const eng2 = new Engine({}, { sessionsDir: dir }); // taze cache — dosyadan okur
40
+ const sess = eng2._load(s.id);
41
+ assert.equal(sess.messages.length, 20);
42
+ sess.notes = 'eski önemli noktalar:\n- karar A\n- karar B';
43
+ sess.code = sess.code || 'ABC234';
44
+ eng2._compactToNotes(sess, 14);
45
+
46
+ // dosyayı yeniden oku
47
+ const raw = fs.readFileSync(file, 'utf8').split('\n').filter(Boolean).map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
48
+ const metas = raw.filter((r) => r.t === 'meta');
49
+ const notesRows = raw.filter((r) => r.t === 'notes');
50
+ const msgRows = raw.filter((r) => r.t === 'msg');
51
+ const compacted = raw.filter((r) => r.t === 'compacted');
52
+ assert.equal(metas.length, 1);
53
+ assert.ok(metas[0].code, 'kod korunmalı');
54
+ assert.equal(notesRows.length, 1);
55
+ assert.match(notesRows[0].text, /karar A/);
56
+ assert.equal(notesRows[0].at, 0); // notesAt sıfırlandı
57
+ assert.ok(msgRows.length <= 6, `korunan pencere ≤6=KEEP_RECENT*2 (gerçek ${msgRows.length})`);
58
+ assert.ok(msgRows.every((m) => Number(m.content.split('-')[1]) >= 14), 'yalnız cut sonrası kaldı');
59
+ assert.equal(compacted.length, 1);
60
+
61
+ // yeniden yükleme tutarlı
62
+ const reloaded = new Engine({}, { sessionsDir: dir });
63
+ const again = reloaded._load(s.id);
64
+ assert.equal(again.code, 'ABC234');
65
+ assert.ok(again.messages.length <= 12);
66
+ assert.match(String(again.notes || ''), /karar A/);
67
+
68
+ fs.rmSync(dir, { recursive: true, force: true });
69
+ });
@@ -0,0 +1,95 @@
1
+ 'use strict';
2
+
3
+ require('./setup');
4
+ const test = require('node:test');
5
+ const assert = require('node:assert');
6
+ const cron = require('../src/cron');
7
+
8
+ test('geçersiz cron ifadeleri reddedilir', () => {
9
+ assert.equal(cron.parseCron('* * * *'), null);
10
+ assert.equal(cron.parseCron('61 * * * *'), null);
11
+ assert.equal(cron.parseCron('* 25 * * *'), null);
12
+ assert.equal(cron.parseCron('a b c d e'), null);
13
+ });
14
+
15
+ test('geçerli cron ifadeleri ayrışır', () => {
16
+ assert.ok(cron.parseCron('* * * * *'));
17
+ assert.ok(cron.parseCron('*/15 * * * *'));
18
+ assert.ok(cron.parseCron('0 9 * * 1-5'));
19
+ assert.ok(cron.parseCron('30 21 1,15 * *'));
20
+ });
21
+
22
+ /* Sabit tarih: 2026-08-25 Salı 10:00 */
23
+ function at(min, h, day, mon) {
24
+ return new Date(2026, mon - 1, day, h, min, 0);
25
+ }
26
+
27
+ test('nextRunFrom sıradaki uyarın anını bulur', () => {
28
+ // Her saat başı: 10:00'dan sonraki = 11:00
29
+ const n = new Date(cron.nextRunFrom('0 * * * *', at(0, 10, 25, 8)));
30
+ assert.equal(n.getHours(), 11);
31
+ assert.equal(n.getMinutes(), 0);
32
+
33
+ // Günlük 09:00: salı 10:00'dan sonra → çarşamba 09:00
34
+ const d = new Date(cron.nextRunFrom('0 9 * * *', at(0, 10, 25, 8)));
35
+ assert.equal(d.getDate(), 26);
36
+ assert.equal(d.getHours(), 9);
37
+
38
+ // Hafta içi (Pzt-Cum): salıdan sonraki aynı gün 21:00
39
+ const w = new Date(cron.nextRunFrom('0 21 * * 1-5', at(30, 20, 25, 8)));
40
+ assert.equal(w.getDay(), 2); // salı
41
+ assert.equal(w.getHours(), 21);
42
+ });
43
+
44
+ test('add: bozuk schedule reddedilir, düzgün olan nextRun alır', () => {
45
+ const bad = cron.add({ name: 'x', schedule: 'yok', prompt: 'p' });
46
+ assert.equal(bad.ok, false);
47
+ const good = cron.add({ name: 'deneme', schedule: '*/5 * * * *', prompt: 'saati söyle' });
48
+ assert.equal(good.ok, true);
49
+ assert.ok(good.job.nextRunAt > Date.now());
50
+ assert.equal(cron.list().some((j) => j.id === good.job.id), true);
51
+ });
52
+
53
+ test('toggle + remove çalışır', () => {
54
+ const r = cron.add({ name: 't2', schedule: '0 12 * * *', prompt: 'p' });
55
+ const id = r.job.id;
56
+ const t = cron.toggle(id);
57
+ assert.equal(t.job.enabled, false);
58
+ assert.equal(t.job.nextRunAt, null);
59
+ assert.equal(cron.remove(id).ok, true);
60
+ });
61
+
62
+ /* ---------- reminderSchedule: tekrarlı hatırlatma ---------- */
63
+
64
+ test('reminderSchedule: presetler when\u0027den saat üretir', () => {
65
+ // 2026-08-26 Çarşamba 09:00
66
+ const w = '2026-08-26T09:00';
67
+
68
+ const daily = cron.reminderSchedule(w, 'daily');
69
+ assert.equal(daily.ok, true);
70
+ assert.equal(daily.schedule, '0 9 * * *');
71
+
72
+ const weekdays = cron.reminderSchedule(w, 'weekdays');
73
+ assert.equal(weekdays.schedule, '0 9 * * 1-5');
74
+
75
+ const weekly = cron.reminderSchedule(w, 'weekly');
76
+ assert.equal(weekly.schedule, '0 9 * * 3'); // çarşamba
77
+
78
+ const monthly = cron.reminderSchedule(w, 'monthly');
79
+ assert.equal(monthly.schedule, '0 9 26 * *');
80
+
81
+ // dakika korunmalı
82
+ const dm = cron.reminderSchedule('2026-08-26T09:30', 'daily');
83
+ assert.equal(dm.schedule, '30 9 * * *');
84
+ });
85
+
86
+ test('reminderSchedule: doğrudan cron kabul, bozuk ve bilinmeyen reddedilir', () => {
87
+ const raw = cron.reminderSchedule(null, '*/30 * * * *');
88
+ assert.equal(raw.ok, true);
89
+ assert.equal(raw.schedule, '*/30 * * * *');
90
+
91
+ assert.equal(cron.reminderSchedule('2026-08-26T09:00', '').ok, false);
92
+ assert.equal(cron.reminderSchedule('2026-08-26T09:00', 'hergun').ok, false);
93
+ assert.equal(cron.reminderSchedule('zaman değil', 'daily').ok, false);
94
+ assert.equal(cron.reminderSchedule(null, '61 * * * *').ok, false);
95
+ });