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,75 @@
1
+ 'use strict';
2
+
3
+ /* #18 python_run altyapısı testleri — ağ çağrısı YOK */
4
+
5
+ const test = require('node:test');
6
+ const assert = require('node:assert');
7
+ const tools = require('../src/agent/tools');
8
+
9
+ test('python_run tanımı TOOLS içinde', () => {
10
+ const def = tools.definitions.find((d) => d.function.name === 'python_run');
11
+ assert.ok(def, 'python_run var');
12
+ const props = def.function.parameters.properties;
13
+ for (const k of ['code', 'script', 'args', 'timeout_ms']) assert.ok(props[k], k);
14
+ });
15
+
16
+ test('code/script yoksa net hata döner (interpreter araması bile gerekmez)', async () => {
17
+ const out = await tools.exec('python_run', {}, { cwd: process.cwd(), allowDownload: false });
18
+ const r = JSON.parse(out);
19
+ assert.strictEqual(r.ok, false);
20
+ assert.ok(/code ya da script/.test(r.error), r.error);
21
+ });
22
+
23
+ test('olmayan script: interpreter bakılmadan bulunamadı + klasör ipucu döner', async () => {
24
+ const out = await tools.exec(
25
+ 'python_run',
26
+ { script: 'kesinlikle-yok-abc123.py' },
27
+ { cwd: process.cwd(), allowDownload: false }
28
+ );
29
+ const r = JSON.parse(out);
30
+ assert.strictEqual(r.ok, false);
31
+ assert.match(String(r.error || ''), /bulunamadı/);
32
+ assert.match(String(r.hint || ''), /scripts/, 'klasör ipucu verildi');
33
+ });
34
+
35
+ test('scripts klasörü çözümlenir ve oluşturulur', () => {
36
+ const dir = tools.pythonScriptsDir();
37
+ assert.ok(typeof dir === 'string' && dir.length > 0);
38
+ });
39
+
40
+ test('paketlenmiş python scriptleri mevcut ve tohumlanır', () => {
41
+ for (const name of ['websearch.py', 'news.py']) {
42
+ assert.ok(require('fs').existsSync(tools.bundledScriptPath(name)), 'paket içi: ' + name);
43
+ const dest = tools.seedScript(name);
44
+ assert.ok(require('fs').existsSync(dest), 'tohumlandı: ' + dest);
45
+ }
46
+ });
47
+
48
+ /* canlı: python hızlı arama yolu (ağ varsa gerçek sonuç, yoksa JS fallback) */
49
+ test('webSearchFast: python yolu ya da fallback sonuç döner', async function () {
50
+ const r = await tools.webSearchFast('electron builder portable windows', { maxResults: 5 });
51
+ assert.strictEqual(r.ok, true);
52
+ if (!r.results.length) return this.skip('motorlar bu turda sonuç döndürmedi (ağ/rate-limit) — kablaj doğru');
53
+ assert.ok(r.results[0].title && r.results[0].url);
54
+ }, { timeout: 40000 });
55
+
56
+ /* canlı python varsa (BEAST_PYTHON / sistem / önceden kurulmuş gömülü):
57
+ gerçek bir hesap çalıştırılır — ağ kurulumu yapılmaz */
58
+ test('canlı interpreter ile inline code çalışır (kuruluysa)', async () => {
59
+ let py = null;
60
+ try { py = await tools.findSystemPython(); } catch {}
61
+ if (!py) {
62
+ try { fsAccess(tools.embeddedPythonExe()); } catch { return test.skip('interpreter yok — atlandı'); }
63
+ }
64
+ const out = await tools.exec(
65
+ 'python_run',
66
+ { code: 'print(21*2)' },
67
+ { cwd: process.cwd(), allowDownload: false }
68
+ );
69
+ const r = JSON.parse(out);
70
+ if (r.ok) assert.ok(/42/.test(r.output), 'çıktı: ' + r.output);
71
+ }, { timeout: 30000 });
72
+
73
+ function fsAccess(p) {
74
+ require('fs').accessSync(p);
75
+ }
@@ -0,0 +1,81 @@
1
+ {
2
+ "note": "Beast eval senaryoları (#4). Her senaryo offline doğrulanabilir olmalı: ya modül fonksiyonu çağrılır ya da prompt/sistem davranışı regex ile kontrol edilir. LLM çağıran uçtan uca testler burada DEĞİL — maliyet ve kararsızlık nedeniyle.",
3
+ "scenarios": [
4
+ {
5
+ "id": "cron-invalid-rejected",
6
+ "desc": "Bozuk cron ifadesi kabul edilmez",
7
+ "module": "cron",
8
+ "call": { "fn": "parseCron", "args": ["61 * * * *"] },
9
+ "expectNull": true
10
+ },
11
+ {
12
+ "id": "reminder-daily-schedule",
13
+ "desc": "Tekrarlı hatırlatma daily preset doğru cron üretir",
14
+ "module": "cron",
15
+ "call": { "fn": "reminderSchedule", "args": ["2026-08-27T09:00", "daily"] },
16
+ "expectEquals": { "schedule": "0 9 * * *" }
17
+ },
18
+ {
19
+ "id": "watcher-edge-trigger",
20
+ "desc": "İzleyici kenar tetiklemeli: koşul doğruyken tek atış",
21
+ "module": "watchers",
22
+ "manual": true,
23
+ "coveredBy": "watchers.test.js"
24
+ },
25
+ {
26
+ "id": "wa-multi-message-merge",
27
+ "desc": "Ard arda WA mesajları tek pakette birleşir",
28
+ "module": "main",
29
+ "manual": true,
30
+ "coveredBy": "manuel (wa.log queue→flush→perm zinciri)"
31
+ },
32
+ {
33
+ "id": "session-code-format",
34
+ "desc": "Oturum kodu 6 haneli, karıştırılan harfler yok",
35
+ "module": "engine",
36
+ "coveredBy": "sessioncode.test.js"
37
+ },
38
+ {
39
+ "id": "reflection-json-parse",
40
+ "desc": "Yansıma JSON'u fence'li/gömülü olsa da ayrışır",
41
+ "module": "engine",
42
+ "coveredBy": "v11.test.js"
43
+ },
44
+ {
45
+ "id": "kb-search-citation",
46
+ "desc": "kb_search sonucu citation taşır",
47
+ "module": "kb",
48
+ "check": "citation"
49
+ },
50
+ {
51
+ "id": "memory-hygiene-dedup",
52
+ "desc": "Duplike hafıza kayıtları tekilleşir",
53
+ "module": "memory",
54
+ "coveredBy": "eval.test.js"
55
+ },
56
+ {
57
+ "id": "system-prompt-has-local-time",
58
+ "desc": "Sistem promptu yerel tarih/saat bildirir",
59
+ "module": "engine",
60
+ "regex": "Yerel saat:"
61
+ },
62
+ {
63
+ "id": "system-prompt-rules-injected",
64
+ "desc": "Kalıcı kurallar sistem promptuna girer",
65
+ "module": "engine",
66
+ "check": "rules"
67
+ },
68
+ {
69
+ "id": "usage-cost-math",
70
+ "desc": "Maliyet hesabı (1M token fiyatıyla) doğru",
71
+ "module": "usage",
72
+ "coveredBy": "usage.test.js"
73
+ },
74
+ {
75
+ "id": "bus-price-filter",
76
+ "desc": "Fiyat olayı eşiğiyle filtrelenir",
77
+ "module": "bus",
78
+ "coveredBy": "bus.test.js"
79
+ }
80
+ ]
81
+ }
@@ -0,0 +1,50 @@
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
+ function mkEng() {
12
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'beast-code-'));
13
+ return new Engine({}, { sessionsDir: dir });
14
+ }
15
+
16
+ test('oturum kodu: benzersiz, formatta, meta\u2019dan geri yüklenir', () => {
17
+ const eng = mkEng();
18
+ const a = eng.createSession();
19
+ const b = eng.createSession();
20
+ assert.match(a.code, /^[23456789ABCDEFGHJKMNPQRSTUVWXYZ]{6}$/);
21
+ assert.notEqual(a.code, b.code);
22
+
23
+ // cache temizlenmiş gibi: yeni engine aynı dosyadan code okumalı
24
+ const eng2 = new Engine({}, { sessionsDir: eng.sessionsDir });
25
+ const view = eng2.listSessions().find((s) => s.id === a.id);
26
+ assert.equal(view.code, a.code);
27
+ });
28
+
29
+ test('findByCode: kod ile oturumu bulur, hızlı indeks kullanır', async () => {
30
+ const eng = mkEng();
31
+ const v = eng.createSession();
32
+ // gerçek bir mesaj yaz → dosya oluşsun
33
+ eng.send(v.id, { text: 'merhaba' });
34
+ await new Promise((r) => setTimeout(r, 30));
35
+ const hit = eng.findByCode(v.code);
36
+ assert.ok(hit);
37
+ assert.equal(hit.id, v.id);
38
+ assert.equal(eng.findByCode('ZZZZZZ'), null);
39
+ assert.equal(eng.findByCode(v.code.toLowerCase()).id, v.id);
40
+ fs.rmSync(eng.sessionsDir, { recursive: true, force: true });
41
+ });
42
+
43
+ test('ORTAM: yerel tarih/saat/dilim prompta girer', () => {
44
+ const eng = mkEng();
45
+ const sys = eng.buildSystem('test');
46
+ assert.match(sys, /Yerel tarih:/);
47
+ assert.match(sys, /Yerel saat:/);
48
+ assert.match(sys, /UTC varsayma/);
49
+ fs.rmSync(eng.sessionsDir, { recursive: true, force: true });
50
+ });
package/tests/setup.js ADDED
@@ -0,0 +1,9 @@
1
+ 'use strict';
2
+
3
+ /* Ortak test kurulumu: tüm modüller izole BEAST_DATA altına yazsın. */
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const path = require('path');
7
+
8
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'beast-test-'));
9
+ process.env.BEAST_DATA = path.join(tmp, 'data');
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+
3
+ require('./setup');
4
+ const test = require('node:test');
5
+ const assert = require('node:assert');
6
+ const { estTokens, estMsgTokens } = require('../src/agent/tokens');
7
+
8
+ test('estTokens boş metin 0', () => {
9
+ assert.equal(estTokens(''), 0);
10
+ assert.equal(estTokens(null), 0);
11
+ });
12
+
13
+ test('estTokens uzunlukla monoton artar', () => {
14
+ const a = estTokens('hello world');
15
+ const b = estTokens('hello world and more words here');
16
+ assert.ok(a > 0 && b > a);
17
+ });
18
+
19
+ test('geniş karakterler (CJK/emoji) daha pahalı', () => {
20
+ const ascii = estTokens('aaaa');
21
+ const wide = estTokens('\u4e00\u4e01\u4e02\u4e03'); // 4 CJK karakter
22
+ assert.ok(wide > ascii);
23
+ });
24
+
25
+ test('Türkçe karakterler ASCII\u0027den hafif pahalı', () => {
26
+ assert.ok(estTokens('ğğğğ') >= estTokens('aaaa'));
27
+ });
28
+
29
+ test('estMsgTokens mesaj zarfı ekler', () => {
30
+ const m = { role: 'user', content: 'selam' };
31
+ assert.ok(estMsgTokens(m) > estTokens('selam'));
32
+ });
33
+
34
+ test('tool_calls içeren mesaj daha ağır', () => {
35
+ const base = { role: 'assistant', content: 'x' };
36
+ const withTools = {
37
+ role: 'assistant',
38
+ content: 'x',
39
+ tool_calls: [{ id: '1', type: 'function', function: { name: 'run_command', arguments: '{"command":"dir"}' } }],
40
+ };
41
+ assert.ok(estMsgTokens(withTools) > estMsgTokens(base));
42
+ });
@@ -0,0 +1,84 @@
1
+ 'use strict';
2
+
3
+ require('./setup');
4
+ const test = require('node:test');
5
+ const assert = require('node:assert');
6
+ const tools = require('../src/agent/tools');
7
+
8
+ /* ---------- SSRF guard ---------- */
9
+
10
+ test('public URL kabul edilir', () => {
11
+ const u = tools.assertPublicHttpUrl('https://example.com/x?y=1');
12
+ assert.ok(u.startsWith('https://example.com'));
13
+ });
14
+
15
+ test('localhost ve özel ağlar engellenir', () => {
16
+ const bad = [
17
+ 'http://localhost/a',
18
+ 'http://127.0.0.1/',
19
+ 'http://10.0.0.5/',
20
+ 'http://192.168.1.1/',
21
+ 'http://172.16.0.1/',
22
+ 'http://172.31.255.255/',
23
+ 'http://169.254.169.254/meta',
24
+ 'http://pc.local/',
25
+ 'file:///C:/Windows',
26
+ 'ftp://example.com',
27
+ 'http://[::1]/',
28
+ 'http://user:pass@example.com/',
29
+ ];
30
+ for (const u of bad) assert.throws(() => tools.assertPublicHttpUrl(u));
31
+ });
32
+
33
+ test('geçersiz URL atar', () => {
34
+ assert.throws(() => tools.assertPublicHttpUrl('not a url'));
35
+ });
36
+
37
+ /* ---------- DDG parser ---------- */
38
+
39
+ test('parseDdgResults sonuçları ayrıştırır', () => {
40
+ const html = `
41
+ <div class="result results_links">
42
+ <a rel="nofollow" class="result__a" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fexample.com%2Fa&amp;rut=abc">Örnek <b>Başlık</b></a>
43
+ <a class="result__snippet" href="#">Açıklama &amp; özet</a>
44
+ </div>
45
+ <div class="result">
46
+ <a class="result__a" href="https://direct.com/b">Direkt</a>
47
+ <a class="result__snippet">snpt</a>
48
+ </div>`;
49
+ const out = tools.parseDdgResults(html);
50
+ assert.equal(out.length, 2);
51
+ assert.equal(out[0].url, 'https://example.com/a');
52
+ assert.equal(out[0].title, 'Örnek Başlık');
53
+ assert.ok(out[0].snippet.includes('özet'));
54
+ assert.equal(out[1].url, 'https://direct.com/b');
55
+ });
56
+
57
+ test('parseDdgResults boş HTML\u0027de boş döner', () => {
58
+ assert.deepEqual(tools.parseDdgResults('<html></html>'), []);
59
+ });
60
+
61
+ /* ---------- htmlToText ---------- */
62
+
63
+ test('htmlToText script/style temizler', () => {
64
+ const t = tools.htmlToText('<head><style>p{color:red}</style><script>evil()</script></head><body><p>Merhaba</p> dünya</body>');
65
+ assert.ok(t.includes('Merhaba'));
66
+ assert.ok(!t.includes('evil'));
67
+ assert.ok(!t.includes('color:red'));
68
+ });
69
+
70
+ /* ---------- dosya araçları ---------- */
71
+
72
+ const fs = require('fs');
73
+ const os = require('os');
74
+ const path = require('path');
75
+
76
+ test('exec write/read/list turu', async () => {
77
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'beast-tools-'));
78
+ const w = JSON.parse(await tools.exec('write_file', { path: 'alt/dosya.txt', content: 'içerik' }, { cwd }));
79
+ assert.ok(w.ok);
80
+ const r = JSON.parse(await tools.exec('read_file', { path: 'alt/dosya.txt' }, { cwd }));
81
+ assert.equal(r.content, 'içerik');
82
+ const l = JSON.parse(await tools.exec('list_dir', {}, { cwd }));
83
+ assert.ok(l.entries.split('\n')[0].includes('alt'));
84
+ });
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ require('./setup');
4
+ const test = require('node:test');
5
+ const assert = require('node:assert');
6
+ const usageMod = require('../src/agent/usage');
7
+
8
+ test('usage: kayıt + bugün/ay raporu + maliyet hesabı', () => {
9
+ usageMod.reset();
10
+
11
+ /* maliyetsiz çağrı */
12
+ usageMod.record({ providerId: 'p1', model: 'm1', promptTokens: 1000, completionTokens: 500 });
13
+ /* fiyatlı çağrı: $2/M giriş, $8/M çıkış → (2000*2 + 1000*8)/1e6 = $0.012 */
14
+ usageMod.record({
15
+ providerId: 'p1',
16
+ model: 'm2',
17
+ promptTokens: 2000,
18
+ completionTokens: 1000,
19
+ costIn: 2,
20
+ costOut: 8,
21
+ });
22
+ /* sıfır/eksik token — sayılmaz */
23
+ usageMod.record({});
24
+ usageMod.record({ providerId: 'pX', model: 'mY' });
25
+
26
+ const rep = usageMod.report();
27
+ assert.equal(rep.today.total.calls, 2);
28
+ assert.equal(rep.today.total.pin, 3000);
29
+ assert.equal(rep.today.total.pout, 1500);
30
+ assert.ok(Math.abs(rep.today.total.cost - 0.012) < 1e-9);
31
+
32
+ const byModel = Object.fromEntries(rep.today.models.map((m) => [m.model, m]));
33
+ assert.equal(byModel['p1::m1'].calls, 1);
34
+ assert.equal(byModel['p1::m1'].cost, 0);
35
+ assert.equal(byModel['p1::m2'].calls, 1);
36
+
37
+ /* ay raporu aynı günü kapsar */
38
+ assert.equal(rep.month.total.calls, 2);
39
+
40
+ /* reset gerçekten temizler */
41
+ usageMod.reset();
42
+ const rep2 = usageMod.report();
43
+ assert.equal(rep2.today.total.calls, 0);
44
+ });
@@ -0,0 +1,76 @@
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
+ /* parseReflectionJson: düz / fence'li / gömülü JSON + bozuk girdi */
9
+ test('reflection JSON ayrıştırıcı', () => {
10
+ const good = Engine.parseReflectionJson(
11
+ '{"create": true, "name": "rapor-al", "description": "d", "body": "# Rapor\\n- adım"}'
12
+ );
13
+ assert.equal(good.create, true);
14
+ assert.equal(good.name, 'rapor-al');
15
+
16
+ const fenced = Engine.parseReflectionJson(
17
+ 'İşte öneri:\n```json\n{"create": false}\n```\nbitti'
18
+ );
19
+ assert.equal(fenced.create, false);
20
+
21
+ const embedded = Engine.parseReflectionJson(
22
+ 'önce metin {"create": true, "name": "x", "description": "y", "body": "z"} sonra metin'
23
+ );
24
+ assert.equal(embedded.name, 'x');
25
+
26
+ assert.equal(Engine.parseReflectionJson('JSON yok burada'), null);
27
+ assert.equal(Engine.parseReflectionJson('{bozuk'), null);
28
+ });
29
+
30
+ /* skills taslak yaşam döngüsü */
31
+ test('skills draft: add → list → accept → scan\u2019de görünür', () => {
32
+ const skills = require('../src/agent/skills');
33
+ const r = skills.addDraft({
34
+ name: 'Test Yetenek',
35
+ description: 'deneme taslağı',
36
+ body: '# Test Yetenek\n- adım bir',
37
+ });
38
+ assert.ok(r.ok);
39
+
40
+ const drafts = skills.listDrafts();
41
+ const d = drafts.find((x) => x.id === 'test-yetenek');
42
+ assert.ok(d, 'taslak listede olmalı');
43
+
44
+ // scan taslağı GÖRMEMELİ
45
+ assert.ok(!skills.scan().some((s) => s.name === 'Test Yetenek'));
46
+
47
+ // kabul → kurulur, taslak silinir
48
+ const acc = skills.acceptDraft('test-yetenek');
49
+ assert.ok(acc.ok);
50
+ assert.ok(skills.scan().some((s) => s.name === 'Test Yetenek'));
51
+ assert.ok(!skills.listDrafts().some((x) => x.id === 'test-yelenek' || x.id === 'test-yetenek'));
52
+
53
+ // temizlik
54
+ const folder = require('path').join(skills.dir(), 'test-yetenek');
55
+ require('fs').rmSync(folder, { recursive: true, force: true });
56
+ });
57
+
58
+ /* kural hattı (#3) */
59
+ test('rules: ekle/liste/çıkar + skill\u2019e madde', () => {
60
+ const memory = require('../src/agent/memory');
61
+ const before = memory.listRules();
62
+ const r = memory.addRule('Terminal çıktısını asla kısaltma');
63
+ assert.ok(r.ok);
64
+ const after = memory.listRules();
65
+ assert.equal(after.length, before.length + 1);
66
+ assert.ok(after.some((x) => x.includes('Terminal çıktısını')));
67
+
68
+ // dup
69
+ const dup = memory.addRule('Terminal çıktısını asla kısaltma');
70
+ assert.ok(dup.duplicate);
71
+
72
+ // kaldır
73
+ const rm = memory.removeRule('Terminal çıktısını asla kısaltma');
74
+ assert.ok(rm.ok);
75
+ assert.ok(!memory.listRules().some((x) => x.includes('Terminal çıktısını')));
76
+ });
@@ -0,0 +1,206 @@
1
+ 'use strict';
2
+
3
+ require('./setup');
4
+ const test = require('node:test');
5
+ const assert = require('node:assert');
6
+ const watchers = require('../src/agent/watchers');
7
+
8
+ const T0 = new Date('2026-08-26T12:00:00Z').getTime();
9
+
10
+ test('normalize: web izleyicisi minimal alanlarla kurulur', () => {
11
+ const r = watchers.normalize({ name: 'BTC', kind: 'web', url: 'https://api.x.com/p', value: 100000 });
12
+ assert.ok(!r.error);
13
+ assert.equal(r.watcher.kind, 'web');
14
+ assert.equal(r.watcher.op, 'lte'); // varsayılan
15
+ assert.equal(r.watcher.everyMin, 15);
16
+ assert.equal(r.watcher.enabled, true);
17
+ assert.equal(r.watcher.armed, true);
18
+ });
19
+
20
+ test('normalize: battery için url gerekmez, web için zorunlu', () => {
21
+ const b = watchers.normalize({ name: 'Pil', kind: 'battery', value: 20 });
22
+ assert.ok(!b.error);
23
+ const w = watchers.normalize({ name: 'X', kind: 'web', value: 1 });
24
+ assert.ok(w.error);
25
+ });
26
+
27
+ test('normalize: geçersiz kind / regex / eksik eşik reddedilir', () => {
28
+ assert.ok(watchers.normalize({ name: 'x', kind: 'uzay' }).error);
29
+ assert.ok(watchers.normalize({ name: 'x', kind: 'web', url: 'https://a.b', re: '[' }).error);
30
+ assert.ok(watchers.normalize({ name: 'x', kind: 'web', url: 'https://a.b', op: 'lt' }).error); // value yok
31
+ assert.ok(watchers.normalize({ kind: 'battery', value: 1 }).error); // isim yok
32
+ assert.ok(watchers.normalize({ name: 'x', kind: 'web', url: 'ftp://a.b' }).error);
33
+ });
34
+
35
+ test('normalize: changed opsiyonu value olmadan çalışır, aralık kelepçelenir', () => {
36
+ const r = watchers.normalize({ name: 'sayfa', kind: 'web', url: 'https://a.b', op: 'changed', everyMin: 1, cooldownMin: 99999 });
37
+ assert.ok(!r.error);
38
+ assert.equal(r.watcher.everyMin, 1); // #22: 1 dakika artık geçerli
39
+ assert.equal(r.watcher.cooldownMin, 10080);
40
+ /* saniye aralığı: everySec esas alınır, everyMin uyumluluk için türetilir */
41
+ const s = watchers.normalize({ name: 'sn', kind: 'web', url: 'https://a.b', op: 'changed', everySec: 30 });
42
+ assert.ok(!s.error);
43
+ assert.equal(s.watcher.everySec, 30);
44
+ assert.equal(s.watcher.everyMin, 1);
45
+ const s2 = watchers.normalize({ name: 'sn2', kind: 'web', url: 'https://a.b', op: 'changed', everySec: 3 });
46
+ assert.equal(s2.watcher.everySec, 10); // min 10 sn
47
+ });
48
+
49
+ test('compare: sayısal zorlama ve değişmez karşılaştırma', () => {
50
+ assert.ok(watchers.compare('lte', '19.9', 20));
51
+ assert.ok(watchers.compare('gt', 30, 29));
52
+ assert.ok(watchers.compare('eq', 100, '100'));
53
+ assert.ok(watchers.compare('neq', 5, 6));
54
+ assert.ok(!watchers.compare('lt', 'abc', 10)); // sayıya dönmez
55
+ assert.ok(watchers.compare('changed', 'yeni', 'eski'));
56
+ assert.ok(!watchers.compare('changed', 'aynı', 'aynı'));
57
+ });
58
+
59
+ test('applyCheck: kenar tetiklemeli — bir kez ateşlenir, normale dönünce yeniden kurulur', () => {
60
+ const w = { op: 'lte', value: 20, armed: true, lastTriggeredAt: null };
61
+ const a = watchers.applyCheck(w, 18, T0);
62
+ assert.ok(a.triggered);
63
+ assert.equal(a.patch.armed, false);
64
+ // koşul hâlâ doğru ama armed=false → tekrar ateşlemez
65
+ const b = watchers.applyCheck({ ...w, ...a.patch }, 19, T0 + 1);
66
+ assert.ok(!b.triggered);
67
+ // normale döndü → armed=true
68
+ const c = watchers.applyCheck({ ...w, ...a.patch }, 55, T0 + 2);
69
+ assert.ok(!c.triggered);
70
+ assert.equal(c.patch.armed, true);
71
+ // tekrar eşik altına indi → yine ateşler
72
+ const d = watchers.applyCheck({ ...w, ...c.patch }, 10, T0 + 3);
73
+ assert.ok(d.triggered);
74
+ });
75
+
76
+ test('isDue: hiç kontrol edilmemişse vakti geldi, yeni kontrol edildiyse gelmedi', () => {
77
+ const fresh = { enabled: true, everyMin: 15, lastCheckAt: null };
78
+ assert.ok(watchers.isDue(fresh, T0));
79
+ const recent = { enabled: true, everyMin: 15, lastCheckAt: new Date(T0 - 5 * 60000).toISOString() };
80
+ assert.ok(!watchers.isDue(recent, T0));
81
+ const old = { enabled: true, everyMin: 15, lastCheckAt: new Date(T0 - 16 * 60000).toISOString() };
82
+ assert.ok(watchers.isDue(old, T0));
83
+ assert.ok(!watchers.isDue({ ...fresh, enabled: false }, T0));
84
+ /* #22 saniye aralığı */
85
+ const sn = { enabled: true, everySec: 30, lastCheckAt: new Date(T0 - 20000).toISOString() };
86
+ assert.ok(!watchers.isDue(sn, T0));
87
+ assert.ok(watchers.isDue({ ...sn, lastCheckAt: new Date(T0 - 31000).toISOString() }, T0));
88
+ });
89
+
90
+ test('cooldownActive: süre dolmadan aktif', () => {
91
+ const w = { cooldownMin: 60, lastTriggeredAt: new Date(T0 - 30 * 60000).toISOString() };
92
+ assert.ok(watchers.cooldownActive(w, T0));
93
+ const expired = { cooldownMin: 60, lastTriggeredAt: new Date(T0 - 61 * 60000).toISOString() };
94
+ assert.ok(!watchers.cooldownActive(expired, T0));
95
+ assert.ok(!watchers.cooldownActive({ cooldownMin: 60, lastTriggeredAt: null }, T0));
96
+ });
97
+
98
+ /* ---------- arka plan tick ---------- */
99
+
100
+ const { extractValue } = watchers;
101
+
102
+ test('extractValue: JSON path ve regex yolu', () => {
103
+ const w1 = { path: 'data.0.price.usd' };
104
+ assert.equal(extractValue('{"data":[{"price":{"usd":2377.5}}]}', w1), 2377.5);
105
+ assert.equal(extractValue('{"data":[]}', { path: 'data.0.price' }), null);
106
+
107
+ const w2 = { re: '<span id="p">([\\d.]+)</span>' };
108
+ assert.equal(extractValue('x<span id="p">99.9</span>y', w2), '99.9');
109
+
110
+ assert.throws(() => extractValue('{"a":1}', { kind: 'web' }), /path/);
111
+ });
112
+
113
+ test('tickOnce: kenar tetikleme + hata disiplini + changed semantiği', async () => {
114
+ // depoyu temizle
115
+ for (const w of watchers.list()) watchers.remove(w.id);
116
+
117
+ let fakeValue = 100;
118
+ let throws = false;
119
+
120
+ const r = watchers.add({
121
+ name: 'fiyat alarımı',
122
+ kind: 'web',
123
+ url: 'https://api.example.com/x',
124
+ path: 'price',
125
+ op: 'lte',
126
+ value: 90,
127
+ everyMin: 15,
128
+ cooldownMin: 0, // akışı gerçek zamanla bağımsız kılmak için kapalı
129
+ });
130
+ assert.ok(r.ok);
131
+
132
+ const deps = {
133
+ check: async () => {
134
+ if (throws) throw new Error('ağ koptu');
135
+ return fakeValue;
136
+ },
137
+ };
138
+ const rewDue = () =>
139
+ watchers.patch(r.watcher.id, { lastCheckAt: new Date(Date.now() - 16 * 60000).toISOString() });
140
+
141
+ // 1) eşik üstü → tetik yok
142
+ assert.equal((await watchers.tickOnce(deps)).length, 0);
143
+ assert.equal(watchers.list()[0].lastValue, 100);
144
+
145
+ // 2) henüz due değil (az önce kontrol edildi)
146
+ assert.equal((await watchers.tickOnce(deps)).length, 0);
147
+
148
+ // 3) eşiğe iner → bir kez tetik
149
+ fakeValue = 85;
150
+ rewDue();
151
+ const hit = await watchers.tickOnce(deps);
152
+ assert.equal(hit.length, 1);
153
+ assert.equal(hit[0].value, 85);
154
+
155
+ // 4) armed=false — koşul doğruyken tekrar tetiklemez
156
+ rewDue();
157
+ assert.equal((await watchers.tickOnce(deps)).length, 0);
158
+
159
+ // 5) normale döner (re-arm) ama hata modunda lastError yazılır
160
+ fakeValue = 120;
161
+ throws = true;
162
+ rewDue();
163
+ assert.equal((await watchers.tickOnce(deps)).length, 0);
164
+ assert.match(watchers.list()[0].lastError, /ağ koptu/);
165
+
166
+ // 6) hata düzelir → lastError temizlenir
167
+ throws = false;
168
+ rewDue();
169
+ assert.equal((await watchers.tickOnce(deps)).length, 0);
170
+ assert.equal(watchers.list()[0].lastError, '');
171
+
172
+ /* --- changed: sayfa değişikliği izleme --- */
173
+ const r2 = watchers.add({
174
+ name: 'sayfa değişimi',
175
+ kind: 'web',
176
+ url: 'https://a.b/c',
177
+ op: 'changed',
178
+ everyMin: 2,
179
+ cooldownMin: 0,
180
+ });
181
+ assert.ok(r2.ok, JSON.stringify(r2));
182
+ let html = '';
183
+ const deps2 = { check: async () => html };
184
+
185
+ // kuruluştaki referans boş — ilk kontrolde içerik görünür → tetik
186
+ html = '<h1>v1</h1>';
187
+ let evs = await watchers.tickOnce(deps2);
188
+ assert.equal(evs.length, 1);
189
+
190
+ // içerik sabitken tekrar tetiklenmez
191
+ await watchers.patch(r2.watcher.id, { lastCheckAt: new Date(Date.now() - 5 * 60000).toISOString() });
192
+ assert.equal((await watchers.tickOnce(deps2)).length, 0);
193
+
194
+ // içeriği eski haline getir (referans değerine dön) → re-arm
195
+ html = '';
196
+ await watchers.patch(r2.watcher.id, { lastCheckAt: new Date(Date.now() - 5 * 60000).toISOString() });
197
+ await watchers.tickOnce(deps2);
198
+
199
+ // tekrar değişir → yeniden tetik
200
+ html = '<h1>v2</h1>';
201
+ await watchers.patch(r2.watcher.id, { lastCheckAt: new Date(Date.now() - 5 * 60000).toISOString() });
202
+ evs = await watchers.tickOnce(deps2);
203
+ assert.equal(evs.length, 1);
204
+
205
+ for (const id of watchers.list().map((w) => w.id)) watchers.remove(id);
206
+ });