@apmantza/greedysearch-pi 1.8.2 → 1.8.4

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