@apmantza/greedysearch-pi 1.8.0 → 1.8.2
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/CHANGELOG.md +10 -0
- package/README.md +17 -1
- package/bin/launch.mjs +366 -288
- package/bin/search.mjs +148 -20
- package/extractors/common.mjs +291 -279
- package/extractors/gemini.mjs +146 -145
- package/extractors/google-ai.mjs +125 -124
- package/extractors/perplexity.mjs +145 -141
- package/extractors/selectors.mjs +54 -52
- package/index.ts +179 -35
- package/package.json +53 -46
- package/src/github.mjs +237 -237
- package/src/search/chrome.mjs +222 -222
- package/src/search/constants.mjs +37 -37
- package/src/search/defaults.mjs +14 -14
- package/src/search/engines.mjs +6 -2
- package/src/search/fetch-source.mjs +229 -229
- package/src/search/output.mjs +58 -58
- package/src/search/sources.mjs +445 -445
- package/src/search/synthesis-runner.mjs +63 -63
- package/src/search/synthesis.mjs +51 -40
- package/src/tools/deep-research-handler.ts +36 -36
- package/src/tools/greedy-search-handler.ts +57 -57
- package/src/tools/shared.ts +130 -130
- package/src/types.ts +103 -103
- package/test.mjs +377 -0
package/test.mjs
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// test.mjs — Cross-platform test runner for GreedySearch (Windows + Unix)
|
|
3
|
+
//
|
|
4
|
+
// Usage:
|
|
5
|
+
// node test.mjs # run all tests (~8-12 min)
|
|
6
|
+
// node test.mjs quick # skip slow tests (~3 min)
|
|
7
|
+
// node test.mjs smoke # basic health check (~60s)
|
|
8
|
+
// node test.mjs parallel # race condition tests only
|
|
9
|
+
// node test.mjs flags # flag/option tests only
|
|
10
|
+
// node test.mjs edge # edge case tests only
|
|
11
|
+
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
|
|
19
|
+
// ANSI colors
|
|
20
|
+
const C = {
|
|
21
|
+
red: "\x1b[31m",
|
|
22
|
+
green: "\x1b[32m",
|
|
23
|
+
yellow: "\x1b[33m",
|
|
24
|
+
blue: "\x1b[34m",
|
|
25
|
+
cyan: "\x1b[36m",
|
|
26
|
+
reset: "\x1b[0m",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const mode = process.argv[2] || "all";
|
|
30
|
+
const resultsDir = join(__dir, "results", `test_${Date.now()}`);
|
|
31
|
+
mkdirSync(resultsDir, { recursive: true });
|
|
32
|
+
|
|
33
|
+
let pass = 0,
|
|
34
|
+
fail = 0,
|
|
35
|
+
warn = 0,
|
|
36
|
+
skip = 0;
|
|
37
|
+
const failures = [],
|
|
38
|
+
warnings = [],
|
|
39
|
+
skipped = [];
|
|
40
|
+
const startTime = Date.now();
|
|
41
|
+
|
|
42
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
43
|
+
// Helpers
|
|
44
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
function passMsg(msg) {
|
|
47
|
+
pass++;
|
|
48
|
+
console.log(` ${C.green}✓${C.reset} ${msg}`);
|
|
49
|
+
}
|
|
50
|
+
function failMsg(msg) {
|
|
51
|
+
fail++;
|
|
52
|
+
console.log(` ${C.red}✗${C.reset} ${msg}`);
|
|
53
|
+
failures.push(msg);
|
|
54
|
+
}
|
|
55
|
+
function warnMsg(msg) {
|
|
56
|
+
warn++;
|
|
57
|
+
console.log(` ${C.yellow}⚠${C.reset} ${msg}`);
|
|
58
|
+
warnings.push(msg);
|
|
59
|
+
}
|
|
60
|
+
function skipMsg(msg) {
|
|
61
|
+
skip++;
|
|
62
|
+
console.log(` ${C.cyan}⊘${C.reset} ${msg}`);
|
|
63
|
+
skipped.push(msg);
|
|
64
|
+
}
|
|
65
|
+
function info(msg) {
|
|
66
|
+
console.log(` ${C.cyan}ℹ${C.reset} ${msg}`);
|
|
67
|
+
}
|
|
68
|
+
function section(title) {
|
|
69
|
+
console.log(`\n${C.blue}${title}${C.reset}`);
|
|
70
|
+
}
|
|
71
|
+
function subsection(title) {
|
|
72
|
+
console.log(`\n${C.yellow}${title}${C.reset}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function runNode(args, timeoutSec = 60) {
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
const proc = spawn(process.execPath, args, {
|
|
78
|
+
cwd: __dir,
|
|
79
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
80
|
+
timeout: timeoutSec * 1000,
|
|
81
|
+
});
|
|
82
|
+
let out = "",
|
|
83
|
+
err = "";
|
|
84
|
+
proc.stdout.on("data", (d) => (out += d));
|
|
85
|
+
proc.stderr.on("data", (d) => (err += d));
|
|
86
|
+
proc.on("close", (code) => resolve({ code, out, err }));
|
|
87
|
+
proc.on("error", reject);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function checkJson(file, checkFn) {
|
|
92
|
+
try {
|
|
93
|
+
const data = JSON.parse(readFileSync(file, "utf8"));
|
|
94
|
+
return checkFn(data);
|
|
95
|
+
} catch (e) {
|
|
96
|
+
return `PARSE_ERROR: ${e.message}`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
101
|
+
// Pre-flight Checks
|
|
102
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
section("🔧 Pre-flight Checks");
|
|
105
|
+
|
|
106
|
+
// Check CDP module
|
|
107
|
+
if (!existsSync(join(__dir, "bin", "cdp.mjs"))) {
|
|
108
|
+
failMsg("bin/cdp.mjs missing - extension not properly installed");
|
|
109
|
+
process.exit(1);
|
|
110
|
+
} else {
|
|
111
|
+
passMsg("CDP module present");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Check Node version
|
|
115
|
+
const nodeVersion = process.version.match(/v(\d+)/)?.[1];
|
|
116
|
+
if (nodeVersion && parseInt(nodeVersion) >= 22) {
|
|
117
|
+
passMsg(`Node.js 22+ (${process.version})`);
|
|
118
|
+
} else {
|
|
119
|
+
warnMsg(`Node.js ${process.version} (22+ recommended)`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check Chrome launcher
|
|
123
|
+
if (!existsSync(join(__dir, "bin", "launch.mjs"))) {
|
|
124
|
+
warnMsg("bin/launch.mjs missing - Chrome auto-launch may fail");
|
|
125
|
+
} else {
|
|
126
|
+
passMsg("Chrome launcher present");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
130
|
+
// Flag & Option Tests
|
|
131
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
if (["", "flags", "quick", "smoke"].includes(mode)) {
|
|
134
|
+
section("🏷️ Flag & Option Tests");
|
|
135
|
+
|
|
136
|
+
subsection("Testing --inline flag (stdout output)...");
|
|
137
|
+
const inlineFile = join(resultsDir, "flag_inline.json");
|
|
138
|
+
const { code: inlineCode, out: inlineOut } = await runNode(
|
|
139
|
+
[join(__dir, "bin", "search.mjs"), "perplexity", "what is AI", "--inline"],
|
|
140
|
+
90,
|
|
141
|
+
);
|
|
142
|
+
if (inlineOut) {
|
|
143
|
+
writeFileSync(inlineFile, inlineOut, "utf8");
|
|
144
|
+
const hasAnswer = checkJson(
|
|
145
|
+
inlineFile,
|
|
146
|
+
(d) => d.answer || d.perplexity?.answer,
|
|
147
|
+
);
|
|
148
|
+
if (hasAnswer) {
|
|
149
|
+
passMsg("--inline: JSON output to stdout");
|
|
150
|
+
} else {
|
|
151
|
+
warnMsg(`--inline: ${hasAnswer}`);
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
failMsg("--inline: timeout or no output");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
subsection("Testing engine aliases...");
|
|
158
|
+
for (const alias of ["p", "g", "b"]) {
|
|
159
|
+
const aliasFile = join(resultsDir, `alias_${alias}.json`);
|
|
160
|
+
const { out: aliasOut } = await runNode(
|
|
161
|
+
[
|
|
162
|
+
join(__dir, "bin", "search.mjs"),
|
|
163
|
+
alias,
|
|
164
|
+
"test query",
|
|
165
|
+
"--out",
|
|
166
|
+
aliasFile,
|
|
167
|
+
],
|
|
168
|
+
60,
|
|
169
|
+
);
|
|
170
|
+
if (existsSync(aliasFile) && aliasFile.length > 0) {
|
|
171
|
+
passMsg(`alias '${alias}': search completed`);
|
|
172
|
+
} else {
|
|
173
|
+
warnMsg(`alias '${alias}': failed (may be expected for some engines)`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
179
|
+
// Edge Case Tests
|
|
180
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
if (["", "edge", "quick"].includes(mode)) {
|
|
183
|
+
section("🔍 Edge Case Tests");
|
|
184
|
+
|
|
185
|
+
subsection("Test 1: Special characters in query...");
|
|
186
|
+
const specialFile = join(resultsDir, "edge_special.json");
|
|
187
|
+
await runNode(
|
|
188
|
+
[
|
|
189
|
+
join(__dir, "bin", "search.mjs"),
|
|
190
|
+
"perplexity",
|
|
191
|
+
"C++ memory management & pointers",
|
|
192
|
+
"--out",
|
|
193
|
+
specialFile,
|
|
194
|
+
],
|
|
195
|
+
90,
|
|
196
|
+
);
|
|
197
|
+
if (existsSync(specialFile)) {
|
|
198
|
+
const queryCheck = checkJson(
|
|
199
|
+
specialFile,
|
|
200
|
+
(d) => d.query?.includes("C++") && d.query?.includes("&"),
|
|
201
|
+
);
|
|
202
|
+
if (queryCheck) {
|
|
203
|
+
passMsg("Edge1: special chars preserved");
|
|
204
|
+
} else {
|
|
205
|
+
warnMsg("Edge1: query mangled");
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
warnMsg("Edge1: search failed");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
subsection("Test 2: Very short query...");
|
|
212
|
+
const shortFile = join(resultsDir, "edge_short.json");
|
|
213
|
+
await runNode(
|
|
214
|
+
[
|
|
215
|
+
join(__dir, "bin", "search.mjs"),
|
|
216
|
+
"perplexity",
|
|
217
|
+
"Docker",
|
|
218
|
+
"--out",
|
|
219
|
+
shortFile,
|
|
220
|
+
],
|
|
221
|
+
90,
|
|
222
|
+
);
|
|
223
|
+
if (existsSync(shortFile)) {
|
|
224
|
+
const hasAnswer = checkJson(shortFile, (d) => d.answer?.length > 10);
|
|
225
|
+
if (hasAnswer) {
|
|
226
|
+
passMsg("Edge2: short query handled");
|
|
227
|
+
} else {
|
|
228
|
+
warnMsg("Edge2: no answer");
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
warnMsg("Edge2: timeout");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
subsection("Test 3: Unicode/international characters...");
|
|
235
|
+
const unicodeFile = join(resultsDir, "edge_unicode.json");
|
|
236
|
+
await runNode(
|
|
237
|
+
[
|
|
238
|
+
join(__dir, "bin", "search.mjs"),
|
|
239
|
+
"google",
|
|
240
|
+
"日本のAI技術について教えて",
|
|
241
|
+
"--out",
|
|
242
|
+
unicodeFile,
|
|
243
|
+
],
|
|
244
|
+
120,
|
|
245
|
+
);
|
|
246
|
+
if (existsSync(unicodeFile)) {
|
|
247
|
+
const unicodeCheck = checkJson(unicodeFile, (d) =>
|
|
248
|
+
d.query?.includes("日本"),
|
|
249
|
+
);
|
|
250
|
+
if (unicodeCheck) {
|
|
251
|
+
passMsg("Edge3: unicode preserved");
|
|
252
|
+
} else {
|
|
253
|
+
warnMsg("Edge3: unicode mangled");
|
|
254
|
+
}
|
|
255
|
+
} else {
|
|
256
|
+
warnMsg("Edge3: timeout");
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
261
|
+
// GitHub Fetch Tests
|
|
262
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
if (["", "edge", "quick", "smoke"].includes(mode)) {
|
|
265
|
+
section("🐙 GitHub Fetch Tests");
|
|
266
|
+
|
|
267
|
+
subsection("Test 1: Blob file fetch (raw URL)...");
|
|
268
|
+
const ghBlobFile = join(resultsDir, "gh_blob.json");
|
|
269
|
+
const blobScript = `
|
|
270
|
+
import { fetchGitHubContent } from '${join(__dir, "src", "github.mjs").replace(/\\/g, "/")}';
|
|
271
|
+
import { writeFileSync } from 'fs';
|
|
272
|
+
try {
|
|
273
|
+
const r = await fetchGitHubContent('https://github.com/expressjs/express/blob/master/Readme.md');
|
|
274
|
+
writeFileSync('${ghBlobFile.replace(/\\/g, "\\\\")}', JSON.stringify(r));
|
|
275
|
+
} catch(e) {
|
|
276
|
+
writeFileSync('${ghBlobFile.replace(/\\/g, "\\\\")}', JSON.stringify({ ok: false, error: e.message }));
|
|
277
|
+
}
|
|
278
|
+
`;
|
|
279
|
+
const blobTmp = join(resultsDir, "_gh_blob_test.mjs");
|
|
280
|
+
writeFileSync(blobTmp, blobScript, "utf8");
|
|
281
|
+
await runNode([blobTmp], 20);
|
|
282
|
+
|
|
283
|
+
if (existsSync(ghBlobFile)) {
|
|
284
|
+
const result = checkJson(
|
|
285
|
+
ghBlobFile,
|
|
286
|
+
(r) => r.ok && r.content?.length > 100,
|
|
287
|
+
);
|
|
288
|
+
if (result) {
|
|
289
|
+
passMsg("GitHub blob: content fetched");
|
|
290
|
+
} else {
|
|
291
|
+
failMsg("GitHub blob: failed");
|
|
292
|
+
}
|
|
293
|
+
} else {
|
|
294
|
+
failMsg("GitHub blob: no output");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
subsection("Test 2: HTTP fetcher pipeline...");
|
|
298
|
+
const ghFetchFile = join(resultsDir, "gh_fetcher.json");
|
|
299
|
+
const fetcherScript = `
|
|
300
|
+
import { fetchSourceHttp } from '${join(__dir, "src", "fetcher.mjs").replace(/\\/g, "/")}';
|
|
301
|
+
import { writeFileSync } from 'fs';
|
|
302
|
+
try {
|
|
303
|
+
const r = await fetchSourceHttp('https://github.com/expressjs/express/blob/master/Readme.md');
|
|
304
|
+
writeFileSync('${ghFetchFile.replace(/\\/g, "\\\\")}', JSON.stringify({ ok: r.ok, length: r.markdown?.length, error: r.error }));
|
|
305
|
+
} catch(e) {
|
|
306
|
+
writeFileSync('${ghFetchFile.replace(/\\/g, "\\\\")}', JSON.stringify({ ok: false, error: e.message }));
|
|
307
|
+
}
|
|
308
|
+
`;
|
|
309
|
+
const fetcherTmp = join(resultsDir, "_gh_fetcher_test.mjs");
|
|
310
|
+
writeFileSync(fetcherTmp, fetcherScript, "utf8");
|
|
311
|
+
await runNode([fetcherTmp], 20);
|
|
312
|
+
|
|
313
|
+
if (existsSync(ghFetchFile)) {
|
|
314
|
+
const result = checkJson(ghFetchFile, (r) => r.ok && r.length > 100);
|
|
315
|
+
if (result) {
|
|
316
|
+
passMsg("GitHub via fetcher: content fetched");
|
|
317
|
+
} else {
|
|
318
|
+
failMsg("GitHub via fetcher: failed");
|
|
319
|
+
}
|
|
320
|
+
} else {
|
|
321
|
+
failMsg("GitHub via fetcher: no output");
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
326
|
+
// Summary
|
|
327
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
328
|
+
|
|
329
|
+
section("📊 Test Summary");
|
|
330
|
+
|
|
331
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
332
|
+
const reportFile = join(resultsDir, "REPORT.md");
|
|
333
|
+
|
|
334
|
+
const report = `# GreedySearch Test Report
|
|
335
|
+
|
|
336
|
+
**Date:** ${new Date().toISOString()}
|
|
337
|
+
**Duration:** ${duration}s
|
|
338
|
+
**Results Directory:** ${resultsDir}
|
|
339
|
+
**Test Mode:** ${mode}
|
|
340
|
+
|
|
341
|
+
## Summary
|
|
342
|
+
|
|
343
|
+
| Metric | Count |
|
|
344
|
+
|--------|-------|
|
|
345
|
+
| ✅ Passed | ${pass} |
|
|
346
|
+
| ❌ Failed | ${fail} |
|
|
347
|
+
| ⚠️ Warnings | ${warn} |
|
|
348
|
+
| ⊘ Skipped | ${skip} |
|
|
349
|
+
| **Total** | ${pass + fail + warn + skip} |
|
|
350
|
+
|
|
351
|
+
${failures.length ? `### Failures\n${failures.map((f, i) => `${i + 1}. ${f}`).join("\n")}` : ""}
|
|
352
|
+
${warnings.length ? `### Warnings\n${warnings.map((w, i) => `${i + 1}. ${w}`).join("\n")}` : ""}
|
|
353
|
+
`;
|
|
354
|
+
|
|
355
|
+
writeFileSync(reportFile, report, "utf8");
|
|
356
|
+
|
|
357
|
+
console.log(`\n${C.yellow}═══ Results ═══${C.reset}`);
|
|
358
|
+
console.log(` ${C.green}Passed: ${pass}${C.reset}`);
|
|
359
|
+
console.log(` ${C.red}Failed: ${fail}${C.reset}`);
|
|
360
|
+
console.log(` ${C.yellow}Warnings: ${warn}${C.reset}`);
|
|
361
|
+
console.log(` ${C.cyan}Skipped: ${skip}${C.reset}`);
|
|
362
|
+
console.log(` Duration: ${duration}s`);
|
|
363
|
+
console.log(`\n Results: ${resultsDir}`);
|
|
364
|
+
console.log(` Report: ${reportFile}\n`);
|
|
365
|
+
|
|
366
|
+
if (failures.length) {
|
|
367
|
+
console.log(`${C.red}Failures:${C.reset}`);
|
|
368
|
+
failures.forEach((f) => console.log(` ${C.red}•${C.reset} ${f}`));
|
|
369
|
+
console.log();
|
|
370
|
+
}
|
|
371
|
+
if (warnings.length) {
|
|
372
|
+
console.log(`${C.yellow}Warnings:${C.reset}`);
|
|
373
|
+
warnings.forEach((w) => console.log(` ${C.yellow}•${C.reset} ${w}`));
|
|
374
|
+
console.log();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
process.exit(fail > 0 ? 1 : 0);
|