beast-agent 0.27.0 → 0.28.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/package.json +1 -1
- package/src/agent/engine.js +91 -53
- package/src/agent/obscura.js +245 -0
- package/src/agent/skills.js +14 -10
- package/src/agent/tools.js +105 -51
- package/src/main.js +47 -22
- package/src/preload.js +5 -3
- package/src/renderer/i18n.js +46 -24
- package/src/renderer/renderer.js +106 -38
- package/src/renderer/style.css +11 -0
- package/tests/bg-jobs.test.js +71 -3
- package/tests/obscura.test.js +124 -0
package/src/renderer/renderer.js
CHANGED
|
@@ -383,6 +383,17 @@ function addErrorBubble(text) {
|
|
|
383
383
|
scrollDown(true);
|
|
384
384
|
}
|
|
385
385
|
|
|
386
|
+
/* ajan durduruldu/iptal edildi — SEBEP zorunlu, sohbete sönük not olarak düşer */
|
|
387
|
+
function addStopNote(reason) {
|
|
388
|
+
streamEl = null;
|
|
389
|
+
showEmpty(false);
|
|
390
|
+
const div = document.createElement('div');
|
|
391
|
+
div.className = 'msg msg-sys';
|
|
392
|
+
div.textContent = '\u23F9 Durduruldu — sebep: ' + String(reason || 'sebep belirtilmedi');
|
|
393
|
+
els.msgs.appendChild(div);
|
|
394
|
+
scrollDown(true);
|
|
395
|
+
}
|
|
396
|
+
|
|
386
397
|
/* ---------------- tool cards ---------------- */
|
|
387
398
|
/* Ard arda gelen araç kartları TEK kutuda toplanır (.tool-box): çalışırken
|
|
388
399
|
lacivert outline döner, gövde max 420px scroll'lu. Grup; tool_calls İÇEREN
|
|
@@ -738,12 +749,30 @@ async function refreshFalloutPane() {
|
|
|
738
749
|
renderFalloutPane();
|
|
739
750
|
}
|
|
740
751
|
|
|
741
|
-
/* Web Arama sekmesi: TinyFish (
|
|
752
|
+
/* Web Arama sekmesi: TinyFish anahtarı + Obscura (kurulum/aktiflik) + arama sırası */
|
|
742
753
|
async function renderWebSearchPane() {
|
|
743
754
|
const pane = $('#tab-websearch');
|
|
744
755
|
if (!pane) return;
|
|
745
756
|
const tf = await beast.tinyfishGet().catch(() => ({ set: false, masked: '' }));
|
|
746
757
|
pane.innerHTML =
|
|
758
|
+
'<h2>' + _t('ws_h2') + '</h2>' +
|
|
759
|
+
'<div class="sub">' + _t('ws_sub') + '</div>' +
|
|
760
|
+
/* --- Obscura --- */
|
|
761
|
+
'<div class="divider"></div>' +
|
|
762
|
+
'<h2>' + _t('oc_h2') + '</h2>' +
|
|
763
|
+
'<div class="sub">' + _t('oc_sub') + '</div>' +
|
|
764
|
+
'<div id="ocStatus" class="sub" style="text-align:left;margin-top:8px"></div>' +
|
|
765
|
+
'<label class="mem-label" style="display:flex;align-items:center;gap:8px;cursor:pointer">' +
|
|
766
|
+
'<input id="ocEnabled" type="checkbox" style="width:auto" /> <span>' + _t('oc_enabled') + '</span></label>' +
|
|
767
|
+
'<div class="form-grid" style="grid-template-columns:auto auto;gap:8px;margin-top:10px">' +
|
|
768
|
+
'<button id="ocInstall" class="btn">' + _t('oc_install') + '</button></div>' +
|
|
769
|
+
/* --- Arama sırası --- */
|
|
770
|
+
'<div class="divider"></div>' +
|
|
771
|
+
'<h2>' + _t('so_h2') + '</h2>' +
|
|
772
|
+
'<div class="sub">' + _t('so_sub') + '</div>' +
|
|
773
|
+
'<div id="soList" style="margin-top:10px"></div>' +
|
|
774
|
+
/* --- TinyFish --- */
|
|
775
|
+
'<div class="divider"></div>' +
|
|
747
776
|
'<h2>' + _t('tf_h2') + '</h2>' +
|
|
748
777
|
'<div class="sub">' + _t('tf_sub') + '</div>' +
|
|
749
778
|
'<div id="tfStatus" class="sub" style="text-align:left;margin-top:8px"></div>' +
|
|
@@ -751,26 +780,80 @@ async function renderWebSearchPane() {
|
|
|
751
780
|
'<input id="tfKeyInp" class="inp" type="password" placeholder="tf_..." autocomplete="new-password" spellcheck="false" />' +
|
|
752
781
|
'<div class="form-grid" style="grid-template-columns:auto auto;gap:8px;margin-top:8px">' +
|
|
753
782
|
'<button id="tfSave" class="btn">' + _t('ws_save') + '</button>' +
|
|
754
|
-
'<button id="tfClear" class="btn ghost">' + _t('ws_clear') + '</button></div>'
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
783
|
+
'<button id="tfClear" class="btn ghost">' + _t('ws_clear') + '</button></div>';
|
|
784
|
+
|
|
785
|
+
/* --- Obscura durumu --- */
|
|
786
|
+
const ocSt = $('#ocStatus');
|
|
787
|
+
const ocChk = $('#ocEnabled');
|
|
788
|
+
const ocBtn = $('#ocInstall');
|
|
789
|
+
const oc = await beast.obscuraGet().catch(() => ({ installed: false, enabled: true, dir: '' }));
|
|
790
|
+
ocChk.checked = oc.enabled !== false;
|
|
791
|
+
ocSt.textContent = oc.installed ? _t('oc_status_ok') : _t('oc_status_missing');
|
|
792
|
+
ocChk.addEventListener('change', async () => {
|
|
793
|
+
await beast.obscuraSetEnabled(ocChk.checked).catch(() => {});
|
|
794
|
+
toast(ocChk.checked ? _t('oc_on_toast') : _t('oc_off_toast'));
|
|
795
|
+
});
|
|
796
|
+
ocBtn.addEventListener('click', async () => {
|
|
797
|
+
ocBtn.disabled = true;
|
|
798
|
+
ocBtn.textContent = _t('oc_installing');
|
|
799
|
+
ocSt.textContent = _t('oc_installing');
|
|
800
|
+
const r = await beast.obscuraInstall().catch(() => ({ ok: false, error: 'hata' }));
|
|
801
|
+
ocBtn.disabled = false;
|
|
802
|
+
ocBtn.textContent = _t('oc_install');
|
|
803
|
+
ocSt.textContent = r && r.ok ? _t('oc_status_ok') : _t('oc_install_fail') + (r && r.error ? r.error : '?');
|
|
804
|
+
toast(r && r.ok ? _t('oc_installed_toast') : _t('oc_install_fail') + (r && r.error ? r.error : '?'));
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
/* --- Arama sırası --- */
|
|
808
|
+
const soBox = $('#soList');
|
|
809
|
+
let rows = [];
|
|
810
|
+
const soRes = await beast.searchOrderGet().catch(() => ({ chain: [] }));
|
|
811
|
+
rows = (soRes && Array.isArray(soRes.chain) && soRes.chain.length) ? soRes.chain.map((x) => ({ id: x.id, on: x.on !== false })) : [];
|
|
812
|
+
const engName = (id) => _t('so_engine_' + id) || id;
|
|
813
|
+
function renderRows() {
|
|
814
|
+
soBox.innerHTML = '';
|
|
815
|
+
rows.forEach((row, i) => {
|
|
816
|
+
const el = document.createElement('div');
|
|
817
|
+
el.className = 'so-row' + (row.on ? '' : ' off');
|
|
818
|
+
el.style.cssText = 'display:flex;align-items:center;gap:8px;padding:6px 8px;border:1px solid var(--border);border-radius:8px;margin-bottom:6px';
|
|
819
|
+
el.innerHTML =
|
|
820
|
+
'<span class="so-idx" style="min-width:18px;color:var(--muted);font-size:12px">' + (i + 1) + '.</span>' +
|
|
821
|
+
'<input type="checkbox" class="so-chk" style="width:auto" ' + (row.on ? 'checked' : '') + ' />' +
|
|
822
|
+
'<span style="flex:1">' + escapeHtml(engName(row.id)) + '</span>' +
|
|
823
|
+
'<button class="so-up" title="Yukarı" style="width:auto;padding:2px 8px">↑</button>' +
|
|
824
|
+
'<button class="so-down" title="Aşağı" style="width:auto;padding:2px 8px">↓</button>';
|
|
825
|
+
el.querySelector('.so-chk').addEventListener('change', async (e) => {
|
|
826
|
+
row.on = e.target.checked;
|
|
827
|
+
el.classList.toggle('off', !row.on);
|
|
828
|
+
await saveOrder();
|
|
829
|
+
});
|
|
830
|
+
el.querySelector('.so-up').addEventListener('click', async () => {
|
|
831
|
+
if (i === 0) return;
|
|
832
|
+
[rows[i - 1], rows[i]] = [rows[i], rows[i - 1]];
|
|
833
|
+
renderRows();
|
|
834
|
+
await saveOrder();
|
|
835
|
+
});
|
|
836
|
+
el.querySelector('.so-down').addEventListener('click', async () => {
|
|
837
|
+
if (i >= rows.length - 1) return;
|
|
838
|
+
[rows[i + 1], rows[i]] = [rows[i], rows[i + 1]];
|
|
839
|
+
renderRows();
|
|
840
|
+
await saveOrder();
|
|
841
|
+
});
|
|
842
|
+
soBox.appendChild(el);
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
async function saveOrder() {
|
|
846
|
+
const r = await beast.searchOrderSet(rows).catch(() => null);
|
|
847
|
+
if (r && r.ok) toast(_t('so_saved_toast'));
|
|
848
|
+
else toast(_t('ws_fail_toast'));
|
|
849
|
+
}
|
|
850
|
+
if (rows.length) renderRows();
|
|
851
|
+
|
|
852
|
+
/* --- TinyFish --- */
|
|
769
853
|
const tfSt = $('#tfStatus');
|
|
770
854
|
const setTfSt = (r) => {
|
|
771
855
|
tfSt.textContent = r.set ? _t('tf_status_set') + r.masked : _t('tf_status_unset');
|
|
772
856
|
};
|
|
773
|
-
setExaSt(await beast.exaGet().catch(() => ({ set: false, masked: '' })));
|
|
774
857
|
setTfSt(tf);
|
|
775
858
|
$('#tfSave').addEventListener('click', async () => {
|
|
776
859
|
const v = $('#tfKeyInp').value.trim();
|
|
@@ -790,24 +873,6 @@ async function renderWebSearchPane() {
|
|
|
790
873
|
setTfSt({ set: false, masked: '' });
|
|
791
874
|
toast(_t('tf_cleared_toast'));
|
|
792
875
|
});
|
|
793
|
-
$('#exaSave').addEventListener('click', async () => {
|
|
794
|
-
const v = $('#exaKeyInp').value.trim();
|
|
795
|
-
if (!v) { toast(_t('ws_empty_toast')); return; }
|
|
796
|
-
const rr = await beast.exaSet(v).catch(() => null);
|
|
797
|
-
$('#exaKeyInp').value = '';
|
|
798
|
-
if (rr && rr.set) {
|
|
799
|
-
setExaSt(rr);
|
|
800
|
-
toast(_t('ws_saved_toast'));
|
|
801
|
-
} else {
|
|
802
|
-
toast(_t('ws_fail_toast'));
|
|
803
|
-
}
|
|
804
|
-
});
|
|
805
|
-
$('#exaClear').addEventListener('click', async () => {
|
|
806
|
-
await beast.exaClear().catch(() => {});
|
|
807
|
-
$('#exaKeyInp').value = '';
|
|
808
|
-
setExaSt({ set: false, masked: '' });
|
|
809
|
-
toast(_t('ws_cleared_toast'));
|
|
810
|
-
});
|
|
811
876
|
}
|
|
812
877
|
|
|
813
878
|
async function renderProviderPane() {
|
|
@@ -3447,7 +3512,7 @@ function renderAgentRail() {
|
|
|
3447
3512
|
`<span class="ag-dot"></span>` +
|
|
3448
3513
|
`<span class="sess-title">${escapeHtml(j.title)}</span>` +
|
|
3449
3514
|
(j.code ? `<span class="sess-code" title="Oturum kodu">${escapeHtml(j.code)}</span>` : ``) +
|
|
3450
|
-
`<span class="rj-time">${j.status === 'running' ? when + ' · ' + agentTimerHtml(j.startedAt) + ' · ' + _t('ag_working') : (agStText(j.status) === _t('ag_st_done') ? '\u2713' : (agStText(j.status) || j.status))}</span>` +
|
|
3515
|
+
`<span class="rj-time">${j.status === 'running' ? when + ' · ' + agentTimerHtml(j.startedAt) + ' · ' + _t('ag_working') : (agStText(j.status) === _t('ag_st_done') ? '\u2713' : (agStText(j.status) || j.status) + (j.status === 'aborted' && j.error ? ' — ' + escapeHtml(String(j.error).slice(0, 70)) : ''))}</span>` +
|
|
3451
3516
|
(j.status === 'running'
|
|
3452
3517
|
? `<button class="rj-cancel" title="${_t('ag_cancel')}">×</button>`
|
|
3453
3518
|
: `<button class="rj-cancel" title="${_t('ag_delete')}"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M10 11v6M14 11v6"/></svg></button>`) +
|
|
@@ -3577,7 +3642,7 @@ function renderAgentsPane() {
|
|
|
3577
3642
|
`<div class="ag-head">` +
|
|
3578
3643
|
`<span class="ag-dot"></span>` +
|
|
3579
3644
|
`<span class="ag-title">${escapeHtml(j.title)}</span>` +
|
|
3580
|
-
`<span class="ag-st">${agStText(j.status) || j.status}</span>` +
|
|
3645
|
+
`<span class="ag-st" title="${j.error ? escapeHtml(String(j.error)) : ''}">${agStText(j.status) || j.status}</span>` +
|
|
3581
3646
|
`<span class="ag-time">${when}${j.status === 'running' ? ' · ' + agentTimerHtml(j.startedAt) : j.endedAt ? ` · ${fmtAgo(j.endedAt)}` : ''}</span>` +
|
|
3582
3647
|
(j.status === 'running'
|
|
3583
3648
|
? `<button class="ag-btn cancel" title="${_t('ag_cancel')}">${_t('ag_cancel')}</button>`
|
|
@@ -3764,6 +3829,8 @@ function onEvent(ev) {
|
|
|
3764
3829
|
break;
|
|
3765
3830
|
case 'done':
|
|
3766
3831
|
closeChatToolGroup();
|
|
3832
|
+
/* iptal/durdurma sebebini SOHBETE yaz — sebepsiz durdurma yok */
|
|
3833
|
+
if (ev.aborted) addStopNote(ev.reason);
|
|
3767
3834
|
setBusy(false);
|
|
3768
3835
|
setStatus(''); /* son durum balonu ("düşünüyor…" vb.) yapışmasın */
|
|
3769
3836
|
/* cevabın SONU görünsün: markdown son render sonrası iki kez en alta kilitle */
|
|
@@ -6103,7 +6170,8 @@ function bcIngest(ev) {
|
|
|
6103
6170
|
bcFlushStream();
|
|
6104
6171
|
bcCloseToolGroup();
|
|
6105
6172
|
bcSetBusy(false);
|
|
6106
|
-
|
|
6173
|
+
/* iptal/durdurma SEBEBİ panelde de görünür */
|
|
6174
|
+
bcLine(ev.aborted ? 't-err' : 't-dim', ev.aborted ? '[durduruldu — sebep: ' + (ev.reason || 'sebep belirtilmedi') + ']' : '(tamamlandı)');
|
|
6107
6175
|
bcRefreshPreview();
|
|
6108
6176
|
if (ideModeOn() && els.bcInput) els.bcInput.focus();
|
|
6109
6177
|
break;
|
package/src/renderer/style.css
CHANGED
|
@@ -759,6 +759,17 @@ body.term-open #settingsOverlay { right: var(--tw, 520px); }
|
|
|
759
759
|
padding: 10px 14px;
|
|
760
760
|
}
|
|
761
761
|
|
|
762
|
+
/* durdurma/iptal notu: sebep zorunlu — nötr, sönük ton */
|
|
763
|
+
.msg-sys {
|
|
764
|
+
border: 1px dashed var(--border);
|
|
765
|
+
background: var(--panel);
|
|
766
|
+
color: var(--muted);
|
|
767
|
+
border-radius: 10px;
|
|
768
|
+
padding: 8px 12px;
|
|
769
|
+
font-size: 12.5px;
|
|
770
|
+
word-break: break-word;
|
|
771
|
+
}
|
|
772
|
+
|
|
762
773
|
/* markdown */
|
|
763
774
|
.md p { margin: 0 0 8px; }
|
|
764
775
|
.md p:last-child { margin-bottom: 0; }
|
package/tests/bg-jobs.test.js
CHANGED
|
@@ -271,18 +271,86 @@ test('#5 eşzamanlılık limiti: fazlası kuyrukta bekler, slot boşalınca FIFO
|
|
|
271
271
|
assert.deepEqual(st, ['queued', 'queued', 'running', 'running']);
|
|
272
272
|
assert.strictEqual(sent.length, 2);
|
|
273
273
|
|
|
274
|
-
/* slot açılır → sıradaki queued iş running olur ve gönderilir
|
|
274
|
+
/* slot açılır → sıradaki queued iş running olur ve gönderilir.
|
|
275
|
+
_bgFinish('aborted') artık üst sohbete [ARKA PLAN İPTAL + SEBEP] raporu da düşürür. */
|
|
275
276
|
eng._bgFinish(ids[0], 'aborted');
|
|
276
277
|
await sleep(30);
|
|
277
278
|
const queued = [...eng._bgJobs.values()].filter((j) => j.status === 'queued').length;
|
|
278
279
|
const running = [...eng._bgJobs.values()].filter((j) => j.status === 'running').length;
|
|
279
280
|
assert.strictEqual(queued, 1);
|
|
280
281
|
assert.strictEqual(running, 2);
|
|
281
|
-
|
|
282
|
+
/* 3 gönderim = kuyruktan başlayan iş + 1 rapor = üst sohbete düşen iptal sebebi */
|
|
283
|
+
assert.strictEqual(sent.length, 4);
|
|
284
|
+
assert.ok(eng._pendingReports.length === 0, 'iptal raporu üst sohbete düştü');
|
|
282
285
|
});
|
|
283
286
|
|
|
284
|
-
/* ----------
|
|
287
|
+
/* ---------- İPTAL SEBEBİ DİSİPLİNİ ---------- */
|
|
288
|
+
|
|
289
|
+
test('iptal SEBEBİ zorunlu: interrupt sebebi iş kaydına ve üst sohbete yazar', async () => {
|
|
285
290
|
const { eng } = tmpEngine();
|
|
291
|
+
const sent = [];
|
|
292
|
+
eng.send = (sid, payload) => { sent.push({ sid, text: String((payload && payload.text) || '') }); return true; };
|
|
293
|
+
const r = eng.runBackground('pr', 'koşan iş', 'Sebep İş');
|
|
294
|
+
const id = r.backgroundId;
|
|
295
|
+
const c = new AbortController();
|
|
296
|
+
eng.ctrls.set(id, c);
|
|
297
|
+
|
|
298
|
+
eng.interrupt(id, 'kullanıcı panelden iptal etti');
|
|
299
|
+
assert.ok(c.signal.aborted, 'ctrl kesildi');
|
|
300
|
+
assert.strictEqual(eng._abortReasons.get(id), 'kullanıcı panelden iptal etti', 'sebep haritada');
|
|
301
|
+
|
|
302
|
+
/* _run catch'inin yaptığı iş: haritadan sebebi okuyup _bgFinish'e verir */
|
|
303
|
+
const why = eng._abortReasons.get(id);
|
|
304
|
+
eng._abortReasons.delete(id);
|
|
305
|
+
eng._bgFinish(id, 'aborted', why);
|
|
306
|
+
|
|
307
|
+
const j = eng._bgJobs.get(id);
|
|
308
|
+
assert.strictEqual(j.status, 'aborted');
|
|
309
|
+
assert.strictEqual(j.error, 'kullanıcı panelden iptal etti', 'sebep iş kaydına yazıldı');
|
|
310
|
+
const rep = sent.find((x) => x.sid === 'pr' && x.text.includes('[ARKA PLAN İPTAL'));
|
|
311
|
+
assert.ok(rep, 'üst sohbete İPTAL raporu düştü');
|
|
312
|
+
assert.ok(/Sebep: kullanıcı panelden/.test(rep.text), 'rapor SEBEPİ içerir');
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test('kuyruktaki ajan interrupt ile SEBEPİYLE kesilir', async () => {
|
|
316
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'beast-qc-'));
|
|
317
|
+
const eng = new Engine({}, { sessionsDir: dir, emit: () => {} });
|
|
318
|
+
eng._bgLimit = 0; // yapıcı min 1 kelepçeler; sıfırla → her iş queued kalır
|
|
319
|
+
eng.send = () => true;
|
|
320
|
+
const r = eng.runBackground('pq2', 'kuyruk işi', 'Kuyruk İş');
|
|
321
|
+
await sleep(80); // admit → bgLimit 0 → queued
|
|
322
|
+
assert.strictEqual(eng._bgJobs.get(r.backgroundId).status, 'queued');
|
|
323
|
+
|
|
324
|
+
assert.strictEqual(eng.interrupt(r.backgroundId, 'sıra iptal edildi'), true);
|
|
325
|
+
const j = eng._bgJobs.get(r.backgroundId);
|
|
326
|
+
assert.strictEqual(j.status, 'aborted');
|
|
327
|
+
assert.strictEqual(j.error, 'sıra iptal edildi', 'kuyruktaki işin iptal sebebi kayda geçti');
|
|
328
|
+
eng.deleteSession(r.backgroundId);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test('task_cancel reason ZORUNLU: sebep iş kaydına ve note mesajına düşer', async () => {
|
|
332
|
+
const { eng } = tmpEngine();
|
|
333
|
+
const r = eng.runBackground('pc', 'iş', 'İptal Edilen');
|
|
334
|
+
const id = r.backgroundId;
|
|
335
|
+
const c = new AbortController();
|
|
336
|
+
eng.ctrls.set(id, c);
|
|
337
|
+
|
|
338
|
+
const out = JSON.parse(await eng._execTool('task_cancel', { id, reason: 'yanlış kaynağa baktı' }, {}));
|
|
339
|
+
assert.ok(out.ok, 'cancel başarılı');
|
|
340
|
+
assert.ok(/yanlış kaynağa baktı/.test(out.note), 'note ceza sebep içerir');
|
|
341
|
+
assert.strictEqual(eng._abortReasons.get(id), 'yanlış kaynağa baktı');
|
|
342
|
+
assert.ok(c.signal.aborted);
|
|
343
|
+
|
|
344
|
+
/* reason verilmezse varsayılan sebep yine kayda düşer */
|
|
345
|
+
const id2 = eng.runBackground('pc', 'iş 2', 'İptal 2').backgroundId;
|
|
346
|
+
eng.ctrls.set(id2, new AbortController());
|
|
347
|
+
const out2 = JSON.parse(await eng._execTool('task_cancel', { id: id2 }, {}));
|
|
348
|
+
assert.ok(out2.ok);
|
|
349
|
+
assert.ok(eng._abortReasons.get(id2).includes('task_cancel'), 'varsayılan sebep CEO kaynağını söyler');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
/* ---------- /stop anahtarı ---------- */
|
|
353
|
+
test('stopAll: koşan turları keser, paralel ajanı aborted işaretler', async () => { const { eng } = tmpEngine();
|
|
286
354
|
const r = eng.runBackground('pS', 'koşan iş', 'Koşu');
|
|
287
355
|
const id = r.backgroundId;
|
|
288
356
|
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* Obscura entegrasyonu + sıralı web arama zinciri 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
|
+
const obscura = require('../src/agent/obscura');
|
|
9
|
+
|
|
10
|
+
test('varsayılan zincir: 4 motor, hepsi açık, obscura AKTİF', () => {
|
|
11
|
+
const c = tools.getSearchChain();
|
|
12
|
+
assert.deepEqual(c.map((x) => x.id), ['browser', 'obscura', 'tinyfish', 'python']);
|
|
13
|
+
for (const r of c) assert.strictEqual(r.on, true, r.id + ' varsayılan açık');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('setSearchChain: sıra yeniden düzenlenir, eksik motorlar sona eklenir (açık)', () => {
|
|
17
|
+
const c = tools.setSearchChain([
|
|
18
|
+
{ id: 'obscura', on: true },
|
|
19
|
+
{ id: 'python', on: true },
|
|
20
|
+
{ id: 'browser', on: false },
|
|
21
|
+
]);
|
|
22
|
+
assert.deepEqual(c.map((x) => x.id), ['obscura', 'python', 'browser', 'tinyfish']);
|
|
23
|
+
assert.strictEqual(c.find((x) => x.id === 'browser').on, false);
|
|
24
|
+
assert.strictEqual(c.find((x) => x.id === 'tinyfish').on, true, 'listede olmayan motor varsayılan açık');
|
|
25
|
+
tools.setSearchChain(tools.DEFAULT_SEARCH_CHAIN); /* testler arası temizlik */
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('setSearchChain: hepsi kapalıysa tarayıcı zorunlu açılır (zincir boş kalmasın)', () => {
|
|
29
|
+
const c = tools.setSearchChain(['obscura', 'tinyfish', 'python', 'browser'].map((id) => ({ id, on: false })));
|
|
30
|
+
assert.strictEqual(c.find((x) => x.id === 'browser').on, true);
|
|
31
|
+
tools.setSearchChain(tools.DEFAULT_SEARCH_CHAIN);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('setSearchChain: bilinmeyen id ve tekrar elenir', () => {
|
|
35
|
+
const c = tools.setSearchChain([{ id: 'exa', on: true }, { id: 'obscura' }, { id: 'obscura', on: false }, 'python']);
|
|
36
|
+
assert.deepEqual(c.map((x) => x.id), ['obscura', 'python', 'browser', 'tinyfish']);
|
|
37
|
+
tools.setSearchChain(tools.DEFAULT_SEARCH_CHAIN);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('searchChainWeb: sırayı uygular, dolu motorda durur, boş tarayıcı banlanır', async () => {
|
|
41
|
+
tools.setSearchChain([
|
|
42
|
+
{ id: 'browser', on: true },
|
|
43
|
+
{ id: 'obscura', on: false },
|
|
44
|
+
{ id: 'tinyfish', on: false },
|
|
45
|
+
{ id: 'python', on: false },
|
|
46
|
+
]);
|
|
47
|
+
const calls = [];
|
|
48
|
+
const r = await tools.searchChainWeb('test sorgu', 5, {
|
|
49
|
+
browser: async () => {
|
|
50
|
+
calls.push('browser');
|
|
51
|
+
return { ok: true, results: [{ title: 'T', url: 'https://x.com', snippet: 's', engine: 'browser' }] };
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
assert.ok(r.ok, 'sonuç geldi');
|
|
55
|
+
assert.strictEqual(calls.length, 1, 'zincir ilk dolu motorda durdu');
|
|
56
|
+
assert.ok(!tools.browserBanned(), 'başarılı tarayıcı banlanmaz');
|
|
57
|
+
|
|
58
|
+
/* tarayıcı boş dönerse 10 dk ban + zincir başarısız (diğer motorlar kapalı) */
|
|
59
|
+
const r2 = await tools.searchChainWeb('test 2', 5, {
|
|
60
|
+
browser: async () => {
|
|
61
|
+
calls.push('browser');
|
|
62
|
+
return { ok: true, results: [] };
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
assert.strictEqual(r2.ok, false);
|
|
66
|
+
assert.ok(tools.browserBanned(), 'boş sonuç tarayıcıyı banladı');
|
|
67
|
+
tools.banBrowser(0); /* banı temizle */
|
|
68
|
+
const r3 = await tools.searchChainWeb('test 3', 5, {
|
|
69
|
+
browser: async () => {
|
|
70
|
+
calls.push('browser');
|
|
71
|
+
return { ok: true, results: [{ title: 'T3', url: 'https://y.com' }] };
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
assert.ok(r3.ok && calls.length === 3, 'ban süresi dolunca tarayıcı geri döner');
|
|
75
|
+
tools.setSearchChain(tools.DEFAULT_SEARCH_CHAIN);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('searchChainWeb: browser hook yoksa obscura/tinyfish/python sırası izlenir', async () => {
|
|
79
|
+
tools.setSearchChain([{ id: 'browser', on: true }, { id: 'obscura', on: false }, { id: 'tinyfish', on: false }, { id: 'python', on: false }]);
|
|
80
|
+
const r = await tools.searchChainWeb('test', 5, {});
|
|
81
|
+
/* tüm motorlar kapalı/atlanmış → normalize browser'ı zorunlu açtı ama hook yok → başarısız */
|
|
82
|
+
assert.strictEqual(r.ok, false);
|
|
83
|
+
tools.setSearchChain(tools.DEFAULT_SEARCH_CHAIN);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('searchChainWeb: boş sorgu hata döner', async () => {
|
|
87
|
+
const r = await tools.searchChainWeb(' ', 5, {});
|
|
88
|
+
assert.strictEqual(r.ok, false);
|
|
89
|
+
assert.ok(/boş sorgu/.test(r.error));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('obscura parseDdgLite: lite tablosundan sonuç çıkarır', () => {
|
|
93
|
+
const html =
|
|
94
|
+
'<table><tr><td><a rel="nofollow" class="result-link" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fornek.com%2Fa&rut=1">Örnek A</a></td></tr>' +
|
|
95
|
+
'<tr><td><a rel="nofollow" class="result-link" href="https://ornek.com/b">Örnek B</a></td></tr></table>';
|
|
96
|
+
const rows = obscura.parseDdgLite(html, 5);
|
|
97
|
+
assert.strictEqual(rows.length, 2);
|
|
98
|
+
assert.strictEqual(rows[0].url, 'https://ornek.com/a');
|
|
99
|
+
assert.strictEqual(rows[0].title, 'Örnek A');
|
|
100
|
+
assert.strictEqual(rows[1].url, 'https://ornek.com/b');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('obscura yolları ve kurulum durumu', () => {
|
|
104
|
+
assert.ok(/beast[\\/]obscura$/.test(obscura.obscuraDir()));
|
|
105
|
+
assert.ok(/obscura\.exe$/.test(obscura.obscuraExe()));
|
|
106
|
+
/* test makinesinde kurulu olabilir — sadece tipleri doğrula */
|
|
107
|
+
assert.strictEqual(typeof obscura.obscuraInstalled(), 'boolean');
|
|
108
|
+
assert.strictEqual(typeof obscura.obscuraSearch, 'function');
|
|
109
|
+
assert.strictEqual(typeof obscura.installObscura, 'function');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('obscuraSearch: kurulu değilse null döner (zincir atlar)', async () => {
|
|
113
|
+
const wasInstalled = obscura.obscuraInstalled();
|
|
114
|
+
if (wasInstalled) return; /* kurulu makinede atla */
|
|
115
|
+
const r = await obscura.obscuraSearch('sorgu', {});
|
|
116
|
+
assert.strictEqual(r, null);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('Exa sistemden tamamen kalktı', () => {
|
|
120
|
+
for (const k of ['setExaKey', 'exaSearch']) assert.ok(!(k in tools), k + ' export yok');
|
|
121
|
+
const desc = JSON.stringify(tools.definitions);
|
|
122
|
+
assert.ok(!/exa\.ai/i.test(desc), 'araç tanımlarında exa yok');
|
|
123
|
+
assert.ok(!/api\.exa\.ai/i.test(desc));
|
|
124
|
+
});
|