@apmantza/greedysearch-pi 1.1.7 → 1.2.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/README.md +27 -1
- package/extractors/consent.mjs +81 -3
- package/package.json +1 -1
- package/search.mjs +52 -20
- package/test.sh +221 -0
package/README.md
CHANGED
|
@@ -4,6 +4,13 @@ Pi extension that adds a `greedy_search` tool — fans out queries to Perplexity
|
|
|
4
4
|
|
|
5
5
|
Forked from [GreedySearch-claude](https://github.com/apmantza/GreedySearch-claude).
|
|
6
6
|
|
|
7
|
+
## What's New (v1.2.0)
|
|
8
|
+
|
|
9
|
+
- **Fixed parallel search race condition** — multiple `greedy_search` calls can now run concurrently without tab conflicts
|
|
10
|
+
- **Improved Bing Copilot verification** — better auto-handling of Turnstile challenges and modal dialogs
|
|
11
|
+
- **Added test suite** — run `./test.sh` to verify all modes work correctly
|
|
12
|
+
- **Atomic port file writes** — prevents corruption when multiple processes connect to Chrome
|
|
13
|
+
|
|
7
14
|
## Install
|
|
8
15
|
|
|
9
16
|
```bash
|
|
@@ -128,6 +135,22 @@ Check status:
|
|
|
128
135
|
node ~/.pi/agent/git/GreedySearch-pi/launch.mjs --status
|
|
129
136
|
```
|
|
130
137
|
|
|
138
|
+
## Testing
|
|
139
|
+
|
|
140
|
+
Run the test suite to verify everything works:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
./test.sh # full suite (~3-4 min)
|
|
144
|
+
./test.sh quick # skip parallel tests (~1 min)
|
|
145
|
+
./test.sh parallel # parallel race condition tests only
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Tests verify:
|
|
149
|
+
- Single engine mode (perplexity, bing, google)
|
|
150
|
+
- Sequential "all" mode searches
|
|
151
|
+
- Parallel "all" mode (5 concurrent searches) — detects tab race conditions
|
|
152
|
+
- Synthesis mode with Gemini
|
|
153
|
+
|
|
131
154
|
## Troubleshooting
|
|
132
155
|
|
|
133
156
|
### "Chrome not found"
|
|
@@ -144,7 +167,10 @@ node ~/.pi/agent/git/GreedySearch-pi/launch.mjs
|
|
|
144
167
|
```
|
|
145
168
|
|
|
146
169
|
### Google / Bing "verify you're human"
|
|
147
|
-
The extension auto-clicks
|
|
170
|
+
The extension auto-clicks verification buttons and Cloudflare Turnstile challenges. For hard CAPTCHAs (image puzzles), solve manually in the Chrome window that opens.
|
|
171
|
+
|
|
172
|
+
### Parallel searches failing
|
|
173
|
+
Earlier versions shared Chrome tabs between concurrent searches, causing `ERR_ABORTED` errors. Version 1.2.0+ creates fresh tabs for each search, allowing safe parallel execution.
|
|
148
174
|
|
|
149
175
|
### Search hangs
|
|
150
176
|
Chrome may be unresponsive. Restart it with `launch.mjs --kill` then `launch.mjs`.
|
package/extractors/consent.mjs
CHANGED
|
@@ -36,12 +36,42 @@ const VERIFY_DETECT_JS = `
|
|
|
36
36
|
if (msVerify) { msVerify.click(); return 'clicked-ms-verify:' + (msVerify.innerText?.trim() || msVerify.value); }
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// --- Bing Copilot / Microsoft "Verify you're human" interstitial ---
|
|
40
|
+
// Copilot sometimes shows a modal with "Continue" or "Verify" before allowing queries
|
|
41
|
+
if (url.includes('copilot.microsoft.com') || url.includes('bing.com/chat')) {
|
|
42
|
+
// Look for verification modal/dialog
|
|
43
|
+
var modal = document.querySelector('[role="dialog"], .b_modal, .bnp_hfly, [class*="verify"], [class*="challenge"]');
|
|
44
|
+
if (modal) {
|
|
45
|
+
// Find any actionable button in the modal
|
|
46
|
+
var modalBtns = Array.from(modal.querySelectorAll('button, a[role="button"], input[type="submit"]'));
|
|
47
|
+
var actionBtn = modalBtns.find(b => /^(continue|verify|submit|next|i agree|accept|got it)$/i.test(b.innerText?.trim() || b.value || ''));
|
|
48
|
+
if (actionBtn) { actionBtn.click(); return 'clicked-copilot-modal:' + actionBtn.innerText.trim(); }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Check for Turnstile iframe (Copilot uses Cloudflare Turnstile)
|
|
52
|
+
var turnstileIframe = document.querySelector('iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"], iframe[title*="challenge"], iframe[title*="Widget"]');
|
|
53
|
+
if (turnstileIframe) {
|
|
54
|
+
// Try clicking the iframe container or nearby checkbox
|
|
55
|
+
var container = turnstileIframe.closest('[class*="turnstile"], [class*="challenge"], [id*="turnstile"]') || turnstileIframe.parentElement;
|
|
56
|
+
if (container) {
|
|
57
|
+
var checkbox = container.querySelector('input[type="checkbox"]');
|
|
58
|
+
if (checkbox && !checkbox.checked) {
|
|
59
|
+
checkbox.click();
|
|
60
|
+
return 'clicked-turnstile-in-iframe';
|
|
61
|
+
}
|
|
62
|
+
// Click the container itself (Turnstile often captures clicks on parent)
|
|
63
|
+
container.click();
|
|
64
|
+
return 'clicked-turnstile-container-near-iframe';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
39
69
|
// --- Cloudflare Turnstile (used by Copilot and many sites) ---
|
|
40
70
|
// Turnstile widget in iframe
|
|
41
71
|
var turnstileIframe = document.querySelector('iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"]');
|
|
42
72
|
if (turnstileIframe) {
|
|
43
73
|
// Try to find and click the checkbox inside the iframe's container
|
|
44
|
-
var turnstileCheckbox = document.querySelector('#cf-turnstile-response, [data-turnstile-callback] input, .cf-turnstile input[type=checkbox]');
|
|
74
|
+
var turnstileCheckbox = document.querySelector('#cf-turnstile-response, [data-turnstile-callback] input, .cf-turnstile input[type="checkbox"]');
|
|
45
75
|
if (turnstileCheckbox && !turnstileCheckbox.checked) {
|
|
46
76
|
turnstileCheckbox.click();
|
|
47
77
|
return 'clicked-turnstile-checkbox';
|
|
@@ -95,19 +125,26 @@ const VERIFY_RETRY_JS = `
|
|
|
95
125
|
var isVerifyPage = url.includes('/sorry/') ||
|
|
96
126
|
url.includes('challenges.cloudflare.com') ||
|
|
97
127
|
url.includes('login.microsoftonline.com') ||
|
|
98
|
-
document.querySelector('#challenge-running, #challenge-stage, .cf-turnstile');
|
|
128
|
+
document.querySelector('#challenge-running, #challenge-stage, .cf-turnstile, [role="dialog"]');
|
|
99
129
|
|
|
100
130
|
if (!isVerifyPage) return 'cleared';
|
|
101
131
|
|
|
102
132
|
// Try clicking any verify/continue button again
|
|
103
133
|
var btns = Array.from(document.querySelectorAll('button, input[type=submit], a[role=button]'));
|
|
104
|
-
var btn = btns.find(b => /^(verify|continue|next|i am human|not a robot)$/i.test(b.innerText?.trim() || b.value || ''));
|
|
134
|
+
var btn = btns.find(b => /^(verify|continue|next|i am human|not a robot|submit)$/i.test(b.innerText?.trim() || b.value || ''));
|
|
105
135
|
if (btn) { btn.click(); return 'clicked:' + (btn.innerText?.trim() || btn.value); }
|
|
106
136
|
|
|
107
137
|
// Try Turnstile checkbox
|
|
108
138
|
var cf = document.querySelector('#cf-stage input[type="checkbox"], .cf-turnstile input');
|
|
109
139
|
if (cf && !cf.checked) { cf.click(); return 'clicked-turnstile'; }
|
|
110
140
|
|
|
141
|
+
// Check for modal dialog with continue button (Copilot interstitial)
|
|
142
|
+
var modal = document.querySelector('[role="dialog"], .b_modal, [class*="verify"]');
|
|
143
|
+
if (modal) {
|
|
144
|
+
var modalBtn = modal.querySelector('button, a[role="button"]');
|
|
145
|
+
if (modalBtn) { modalBtn.click(); return 'clicked-modal-btn:' + modalBtn.innerText.trim(); }
|
|
146
|
+
}
|
|
147
|
+
|
|
111
148
|
return 'still-verifying';
|
|
112
149
|
})()
|
|
113
150
|
`;
|
|
@@ -119,6 +156,17 @@ export async function dismissConsent(tab, cdp) {
|
|
|
119
156
|
}
|
|
120
157
|
}
|
|
121
158
|
|
|
159
|
+
// Get iframe bounding box for coordinate-based clicking (for cross-origin Turnstile)
|
|
160
|
+
const GET_IFRAME_CENTER_JS = `
|
|
161
|
+
(function() {
|
|
162
|
+
var iframe = document.querySelector('iframe[src*="challenges.cloudflare.com"], iframe[src*="turnstile"], iframe[title*="challenge"], iframe[title*="Widget"]');
|
|
163
|
+
if (!iframe) return null;
|
|
164
|
+
var rect = iframe.getBoundingClientRect();
|
|
165
|
+
// Click near the center-left where the checkbox usually is
|
|
166
|
+
return JSON.stringify({ x: rect.left + 30, y: rect.top + rect.height / 2 });
|
|
167
|
+
})()
|
|
168
|
+
`;
|
|
169
|
+
|
|
122
170
|
// Returns 'clear' | 'clicked' | 'needs-human'
|
|
123
171
|
export async function handleVerification(tab, cdp, waitMs = 60000) {
|
|
124
172
|
const result = await cdp(['eval', tab, VERIFY_DETECT_JS]).catch(() => null);
|
|
@@ -158,6 +206,17 @@ export async function handleVerification(tab, cdp, waitMs = 60000) {
|
|
|
158
206
|
await new Promise(r => setTimeout(r, 2000));
|
|
159
207
|
}
|
|
160
208
|
|
|
209
|
+
// If verification is stuck, try clicking the Turnstile iframe by coordinates
|
|
210
|
+
const iframeCenter = await cdp(['eval', tab, GET_IFRAME_CENTER_JS]).catch(() => null);
|
|
211
|
+
if (iframeCenter && iframeCenter !== 'null') {
|
|
212
|
+
try {
|
|
213
|
+
const { x, y } = JSON.parse(iframeCenter);
|
|
214
|
+
process.stderr.write(`[greedysearch] Trying coordinate click on Turnstile iframe at (${x}, ${y})...\n`);
|
|
215
|
+
await cdp(['clickxy', tab, String(x), String(y)]);
|
|
216
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
217
|
+
} catch {}
|
|
218
|
+
}
|
|
219
|
+
|
|
161
220
|
await new Promise(r => setTimeout(r, 1500));
|
|
162
221
|
}
|
|
163
222
|
|
|
@@ -166,5 +225,24 @@ export async function handleVerification(tab, cdp, waitMs = 60000) {
|
|
|
166
225
|
return 'needs-human';
|
|
167
226
|
}
|
|
168
227
|
|
|
228
|
+
// Detection didn't find anything initially, but check for Turnstile iframe with coordinates
|
|
229
|
+
if (result === 'null' || !result) {
|
|
230
|
+
const iframeCenter = await cdp(['eval', tab, GET_IFRAME_CENTER_JS]).catch(() => null);
|
|
231
|
+
if (iframeCenter && iframeCenter !== 'null') {
|
|
232
|
+
process.stderr.write(`[greedysearch] Found Turnstile iframe, attempting coordinate click...\n`);
|
|
233
|
+
try {
|
|
234
|
+
const { x, y } = JSON.parse(iframeCenter);
|
|
235
|
+
await cdp(['clickxy', tab, String(x), String(y)]);
|
|
236
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
237
|
+
|
|
238
|
+
// Check if it worked
|
|
239
|
+
const cleared = await cdp(['eval', tab, VERIFY_RETRY_JS]).catch(() => null);
|
|
240
|
+
if (cleared === 'cleared' || cleared === 'null') {
|
|
241
|
+
return 'clicked';
|
|
242
|
+
}
|
|
243
|
+
} catch {}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
169
247
|
return 'clear';
|
|
170
248
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apmantza/greedysearch-pi",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Pi extension: browser-automation tool that searches Perplexity, Bing Copilot, and Google AI in parallel, extracts answers and sources via CDP, with optional Gemini synthesis — grounded AI answers from real browser interactions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
package/search.mjs
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
import { spawn } from 'child_process';
|
|
23
23
|
import { fileURLToPath } from 'url';
|
|
24
24
|
import { join, dirname } from 'path';
|
|
25
|
-
import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'fs';
|
|
25
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync, renameSync, unlinkSync } from 'fs';
|
|
26
26
|
import { tmpdir, homedir } from 'os';
|
|
27
27
|
import http from 'http';
|
|
28
28
|
|
|
@@ -305,7 +305,39 @@ function probeGreedyChrome(timeoutMs = 3000) {
|
|
|
305
305
|
|
|
306
306
|
// Write (or refresh) the DevToolsActivePort file for the GreedySearch Chrome so
|
|
307
307
|
// cdp.mjs always connects to the right port rather than the user's main Chrome.
|
|
308
|
+
// Uses atomic write (write to temp + rename) to prevent corruption from parallel processes.
|
|
308
309
|
async function refreshPortFile() {
|
|
310
|
+
const LOCK_FILE = ACTIVE_PORT_FILE + '.lock';
|
|
311
|
+
const TEMP_FILE = ACTIVE_PORT_FILE + '.tmp';
|
|
312
|
+
|
|
313
|
+
// Simple file-based lock with timeout (prevents parallel writes from corrupting the port file)
|
|
314
|
+
const lockAcquired = await new Promise((resolve) => {
|
|
315
|
+
const start = Date.now();
|
|
316
|
+
const tryLock = () => {
|
|
317
|
+
try {
|
|
318
|
+
writeFileSync(LOCK_FILE, `${process.pid}`, 'utf8');
|
|
319
|
+
resolve(true);
|
|
320
|
+
} catch {
|
|
321
|
+
// Lock file exists - check if stale (older than 5 seconds)
|
|
322
|
+
try {
|
|
323
|
+
const lockTime = parseInt(readFileSync(LOCK_FILE, 'utf8'));
|
|
324
|
+
if (Date.now() - lockTime > 5000) {
|
|
325
|
+
// Stale lock - overwrite
|
|
326
|
+
writeFileSync(LOCK_FILE, `${process.pid}`, 'utf8');
|
|
327
|
+
resolve(true);
|
|
328
|
+
} else if (Date.now() - start < 1000) {
|
|
329
|
+
setTimeout(tryLock, 50);
|
|
330
|
+
} else {
|
|
331
|
+
resolve(false); // Give up after 1s
|
|
332
|
+
}
|
|
333
|
+
} catch {
|
|
334
|
+
setTimeout(tryLock, 50);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
tryLock();
|
|
339
|
+
});
|
|
340
|
+
|
|
309
341
|
try {
|
|
310
342
|
const body = await new Promise((res, rej) => {
|
|
311
343
|
const req = http.get(`http://localhost:${GREEDY_PORT}/json/version`, r => {
|
|
@@ -318,8 +350,19 @@ async function refreshPortFile() {
|
|
|
318
350
|
});
|
|
319
351
|
const { webSocketDebuggerUrl } = JSON.parse(body);
|
|
320
352
|
const wsPath = new URL(webSocketDebuggerUrl).pathname;
|
|
321
|
-
|
|
353
|
+
|
|
354
|
+
// Atomic write: write to temp file, then rename
|
|
355
|
+
if (lockAcquired) {
|
|
356
|
+
writeFileSync(TEMP_FILE, `${GREEDY_PORT}\n${wsPath}`, 'utf8');
|
|
357
|
+
try { unlinkSync(ACTIVE_PORT_FILE); } catch {}
|
|
358
|
+
renameSync(TEMP_FILE, ACTIVE_PORT_FILE);
|
|
359
|
+
}
|
|
322
360
|
} catch { /* best-effort — launch.mjs already wrote the file on first start */ }
|
|
361
|
+
finally {
|
|
362
|
+
if (lockAcquired) {
|
|
363
|
+
try { unlinkSync(LOCK_FILE); } catch {}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
323
366
|
}
|
|
324
367
|
|
|
325
368
|
async function ensureChrome() {
|
|
@@ -377,25 +420,14 @@ async function main() {
|
|
|
377
420
|
if (engine === 'all') {
|
|
378
421
|
await cdp(['list']); // refresh pages cache
|
|
379
422
|
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
//
|
|
423
|
+
// PARALLEL-SAFE: Always create fresh tabs for each engine to avoid race conditions
|
|
424
|
+
// when multiple "all" searches run concurrently. Previously, reusing cached tabs
|
|
425
|
+
// caused ERR_ABORTED and Uncaught errors as multiple processes fought over the same tab.
|
|
383
426
|
const tabs = [];
|
|
384
|
-
let
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
if (existing) {
|
|
389
|
-
tabs.push(existing);
|
|
390
|
-
} else if (!blankReused) {
|
|
391
|
-
const tab = await getOrReuseBlankTab();
|
|
392
|
-
tabs.push(tab);
|
|
393
|
-
blankReused = true;
|
|
394
|
-
} else {
|
|
395
|
-
await new Promise(r => setTimeout(r, 500));
|
|
396
|
-
const tab = await openNewTab();
|
|
397
|
-
tabs.push(tab);
|
|
398
|
-
}
|
|
427
|
+
for (let i = 0; i < ALL_ENGINES.length; i++) {
|
|
428
|
+
if (i > 0) await new Promise(r => setTimeout(r, 300)); // small delay between tab opens
|
|
429
|
+
const tab = await openNewTab();
|
|
430
|
+
tabs.push(tab);
|
|
399
431
|
}
|
|
400
432
|
|
|
401
433
|
// All tabs assigned — run extractors in parallel
|
package/test.sh
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test.sh — GreedySearch test suite
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# ./test.sh # run all tests
|
|
6
|
+
# ./test.sh parallel # run only parallel test
|
|
7
|
+
# ./test.sh quick # skip slow tests (parallel + stress)
|
|
8
|
+
#
|
|
9
|
+
# Tests verify:
|
|
10
|
+
# - No crashes/errors from extractors
|
|
11
|
+
# - All engines complete in "all" mode
|
|
12
|
+
# - Correct queries in results (not mixed up)
|
|
13
|
+
# - Parallel searches don't race on shared tabs
|
|
14
|
+
|
|
15
|
+
set -e
|
|
16
|
+
|
|
17
|
+
cd "$(dirname "$0")"
|
|
18
|
+
RESULTS_DIR="results/test_$(date +%Y%m%d_%H%M%S)"
|
|
19
|
+
mkdir -p "$RESULTS_DIR"
|
|
20
|
+
|
|
21
|
+
RED='\033[0;31m'
|
|
22
|
+
GREEN='\033[0;32m'
|
|
23
|
+
YELLOW='\033[1;33m'
|
|
24
|
+
NC='\033[0m'
|
|
25
|
+
|
|
26
|
+
PASS=0
|
|
27
|
+
FAIL=0
|
|
28
|
+
|
|
29
|
+
pass() { PASS=$((PASS+1)); echo -e " ${GREEN}✓${NC} $1"; }
|
|
30
|
+
fail() { FAIL=$((FAIL+1)); echo -e " ${RED}✗${NC} $1"; }
|
|
31
|
+
|
|
32
|
+
check_no_errors() {
|
|
33
|
+
local file="$1"
|
|
34
|
+
local errors=$(node -e "
|
|
35
|
+
const d = JSON.parse(require('fs').readFileSync('$file','utf8'));
|
|
36
|
+
const errs = [];
|
|
37
|
+
if (d.perplexity?.error) errs.push('perplexity: ' + d.perplexity.error);
|
|
38
|
+
if (d.bing?.error) errs.push('bing: ' + d.bing.error);
|
|
39
|
+
if (d.google?.error) errs.push('google: ' + d.google.error);
|
|
40
|
+
console.log(errs.join('; ') || '');
|
|
41
|
+
" 2>/dev/null)
|
|
42
|
+
echo "$errors"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
check_correct_queries() {
|
|
46
|
+
local file="$1"
|
|
47
|
+
local expected="$2"
|
|
48
|
+
local result=$(node -e "
|
|
49
|
+
const d = JSON.parse(require('fs').readFileSync('$file','utf8'));
|
|
50
|
+
const queries = [d.perplexity?.query, d.bing?.query, d.google?.query].filter(Boolean);
|
|
51
|
+
const allMatch = queries.every(q => q === '$expected');
|
|
52
|
+
console.log(allMatch ? 'ok' : 'queries: ' + queries.join(', '));
|
|
53
|
+
" 2>/dev/null)
|
|
54
|
+
echo "$result"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
check_all_engines_completed() {
|
|
58
|
+
local file="$1"
|
|
59
|
+
local result=$(node -e "
|
|
60
|
+
const d = JSON.parse(require('fs').readFileSync('$file','utf8'));
|
|
61
|
+
const hasAnswer = (e) => d[e]?.answer && d[e].answer.length > 10;
|
|
62
|
+
const engines = ['perplexity', 'bing', 'google'];
|
|
63
|
+
const ok = engines.every(hasAnswer);
|
|
64
|
+
console.log(ok ? 'ok' : 'missing: ' + engines.filter(e => !hasAnswer(e)).join(', '));
|
|
65
|
+
" 2>/dev/null)
|
|
66
|
+
echo "$result"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# ─────────────────────────────────────────────────────────
|
|
70
|
+
echo -e "\n${YELLOW}═══ GreedySearch Test Suite ═══${NC}\n"
|
|
71
|
+
|
|
72
|
+
# ── Test 1: Single engine mode ──────────────────────────
|
|
73
|
+
if [[ "$1" != "parallel" ]]; then
|
|
74
|
+
echo "Test 1: Single engine mode"
|
|
75
|
+
|
|
76
|
+
for engine in perplexity bing google; do
|
|
77
|
+
outfile="$RESULTS_DIR/single_${engine}.json"
|
|
78
|
+
node search.mjs "$engine" "explain $engine attention mechanism" --out "$outfile" 2>/dev/null
|
|
79
|
+
if [[ $? -eq 0 && -f "$outfile" ]]; then
|
|
80
|
+
errors=$(check_no_errors "$outfile")
|
|
81
|
+
if [[ -z "$errors" ]]; then
|
|
82
|
+
pass "$engine completed without errors"
|
|
83
|
+
else
|
|
84
|
+
fail "$engine errors: $errors"
|
|
85
|
+
fi
|
|
86
|
+
else
|
|
87
|
+
fail "$engine failed to run"
|
|
88
|
+
fi
|
|
89
|
+
done
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# ── Test 2: Sequential "all" mode ───────────────────────
|
|
93
|
+
if [[ "$1" != "parallel" ]]; then
|
|
94
|
+
echo -e "\nTest 2: Sequential 'all' mode (3 runs)"
|
|
95
|
+
|
|
96
|
+
for i in 1 2 3; do
|
|
97
|
+
outfile="$RESULTS_DIR/seq_${i}.json"
|
|
98
|
+
query="LLM inference optimization techniques $i"
|
|
99
|
+
node search.mjs all "$query" --out "$outfile" 2>/dev/null
|
|
100
|
+
|
|
101
|
+
if [[ $? -eq 0 && -f "$outfile" ]]; then
|
|
102
|
+
errors=$(check_no_errors "$outfile")
|
|
103
|
+
if [[ -z "$errors" ]]; then
|
|
104
|
+
pass "Run $i: no errors"
|
|
105
|
+
else
|
|
106
|
+
fail "Run $i errors: $errors"
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
correct=$(check_correct_queries "$outfile" "$query")
|
|
110
|
+
if [[ "$correct" == "ok" ]]; then
|
|
111
|
+
pass "Run $i: correct queries"
|
|
112
|
+
else
|
|
113
|
+
fail "Run $i: $correct"
|
|
114
|
+
fi
|
|
115
|
+
else
|
|
116
|
+
fail "Run $i: failed to run"
|
|
117
|
+
fi
|
|
118
|
+
done
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
# ── Test 3: Parallel "all" mode (race condition test) ───
|
|
122
|
+
if [[ "$1" != "quick" && "$1" != "sequential" ]]; then
|
|
123
|
+
echo -e "\nTest 3: Parallel 'all' mode (5 concurrent searches)"
|
|
124
|
+
|
|
125
|
+
PARALLEL_QUERIES=(
|
|
126
|
+
"what are transformer architectures in LLMs"
|
|
127
|
+
"explain RLHF fine-tuning process"
|
|
128
|
+
"difference between GPT and BERT models"
|
|
129
|
+
"how does chain of thought prompting work"
|
|
130
|
+
"what is retrieval augmented generation"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
PIDS=()
|
|
134
|
+
for i in "${!PARALLEL_QUERIES[@]}"; do
|
|
135
|
+
outfile="$RESULTS_DIR/parallel_${i}.json"
|
|
136
|
+
query="${PARALLEL_QUERIES[$i]}"
|
|
137
|
+
node search.mjs all "$query" --out "$outfile" 2>/dev/null &
|
|
138
|
+
PIDS+=($!)
|
|
139
|
+
done
|
|
140
|
+
|
|
141
|
+
# Wait for all to complete
|
|
142
|
+
FAILED=0
|
|
143
|
+
for i in "${!PIDS[@]}"; do
|
|
144
|
+
if ! wait "${PIDS[$i]}"; then
|
|
145
|
+
fail "Parallel $i: process exited with error"
|
|
146
|
+
((FAILED++))
|
|
147
|
+
fi
|
|
148
|
+
done
|
|
149
|
+
|
|
150
|
+
if [[ $FAILED -eq 0 ]]; then
|
|
151
|
+
# Check results
|
|
152
|
+
for i in "${!PARALLEL_QUERIES[@]}"; do
|
|
153
|
+
outfile="$RESULTS_DIR/parallel_${i}.json"
|
|
154
|
+
query="${PARALLEL_QUERIES[$i]}"
|
|
155
|
+
|
|
156
|
+
if [[ -f "$outfile" ]]; then
|
|
157
|
+
errors=$(check_no_errors "$outfile")
|
|
158
|
+
if [[ -z "$errors" ]]; then
|
|
159
|
+
pass "Parallel $i: no errors"
|
|
160
|
+
else
|
|
161
|
+
fail "Parallel $i: $errors"
|
|
162
|
+
fi
|
|
163
|
+
|
|
164
|
+
correct=$(check_correct_queries "$outfile" "$query")
|
|
165
|
+
if [[ "$correct" == "ok" ]]; then
|
|
166
|
+
pass "Parallel $i: correct query"
|
|
167
|
+
else
|
|
168
|
+
fail "Parallel $i: $correct (TAB RACE DETECTED)"
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
all_done=$(check_all_engines_completed "$outfile")
|
|
172
|
+
if [[ "$all_done" == "ok" ]]; then
|
|
173
|
+
pass "Parallel $i: all engines answered"
|
|
174
|
+
else
|
|
175
|
+
fail "Parallel $i: $all_done"
|
|
176
|
+
fi
|
|
177
|
+
else
|
|
178
|
+
fail "Parallel $i: no result file"
|
|
179
|
+
fi
|
|
180
|
+
done
|
|
181
|
+
fi
|
|
182
|
+
fi
|
|
183
|
+
|
|
184
|
+
# ── Test 4: Synthesis mode ──────────────────────────────
|
|
185
|
+
if [[ "$1" != "parallel" && "$1" != "quick" ]]; then
|
|
186
|
+
echo -e "\nTest 4: Synthesis mode"
|
|
187
|
+
|
|
188
|
+
outfile="$RESULTS_DIR/synthesis.json"
|
|
189
|
+
node search.mjs all "what is Mixture of Experts in neural networks" --synthesize --out "$outfile" 2>/dev/null
|
|
190
|
+
|
|
191
|
+
if [[ $? -eq 0 && -f "$outfile" ]]; then
|
|
192
|
+
has_synthesis=$(node -e "
|
|
193
|
+
const d = JSON.parse(require('fs').readFileSync('$outfile','utf8'));
|
|
194
|
+
console.log(d._synthesis?.answer ? 'ok' : 'missing');
|
|
195
|
+
" 2>/dev/null)
|
|
196
|
+
|
|
197
|
+
if [[ "$has_synthesis" == "ok" ]]; then
|
|
198
|
+
pass "Synthesis completed"
|
|
199
|
+
else
|
|
200
|
+
fail "Synthesis missing"
|
|
201
|
+
fi
|
|
202
|
+
|
|
203
|
+
errors=$(check_no_errors "$outfile")
|
|
204
|
+
if [[ -z "$errors" ]]; then
|
|
205
|
+
pass "Synthesis: no engine errors"
|
|
206
|
+
else
|
|
207
|
+
fail "Synthesis: $errors"
|
|
208
|
+
fi
|
|
209
|
+
else
|
|
210
|
+
fail "Synthesis failed to run"
|
|
211
|
+
fi
|
|
212
|
+
fi
|
|
213
|
+
|
|
214
|
+
# ─────────────────────────────────────────────────────────
|
|
215
|
+
echo -e "\n${YELLOW}═══ Results ═══${NC}"
|
|
216
|
+
echo -e " ${GREEN}Passed: $PASS${NC}"
|
|
217
|
+
[[ $FAIL -gt 0 ]] && echo -e " ${RED}Failed: $FAIL${NC}" || echo " Failed: 0"
|
|
218
|
+
echo " Results in: $RESULTS_DIR"
|
|
219
|
+
echo ""
|
|
220
|
+
|
|
221
|
+
[[ $FAIL -eq 0 ]] && exit 0 || exit 1
|