diff options
| author | amankumarsingh77 <amankumarsingh7702@gmail.com> | 2026-07-08 21:13:15 +0530 |
|---|---|---|
| committer | amankumarsingh77 <amankumarsingh7702@gmail.com> | 2026-07-08 21:13:15 +0530 |
| commit | c5c42df5976ed63faac2adcf3b04a4ad2f519b32 (patch) | |
| tree | 77d389a590864eaf5c2408c9bc99ed07f55343d8 /test/deep-research/serp-parser-test.ts | |
| parent | 7826c950adee89512162185d07fee565ab0e96cc (diff) | |
feat: add browser_web_search tool (real-Chrome SERP scrape, no API key)
Thin query→ranked-links primitive. Isolated-attach tab lifecycle mirroring
browser_open_urls (never touches the user's current tab); pure Google SERP
parser with semantic (a h3) extraction and redirect unwrapping; CAPTCHA and
no-results surfaced as invalid_state + details.reason. Fixture-based parser
tests (9 scenarios). Establishes the isolated-tab lifecycle and tsx+fixture
test convention that browser_read_page and deep-research build on.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'test/deep-research/serp-parser-test.ts')
| -rw-r--r-- | test/deep-research/serp-parser-test.ts | 59 |
1 files changed, 59 insertions, 0 deletions
diff --git a/test/deep-research/serp-parser-test.ts b/test/deep-research/serp-parser-test.ts new file mode 100644 index 0000000..dabd72b --- /dev/null +++ b/test/deep-research/serp-parser-test.ts @@ -0,0 +1,59 @@ +/** + * Unit tests for the pure Google SERP parser — no browser required. + * + * Run: npx tsx test/deep-research/serp-parser-test.ts + */ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { classifySerp, parseGoogleSerp, type SerpExtraction } from "../../src/domains/search/google-serp"; + +let passed = 0; +let failed = 0; +const check = (cond: boolean, label: string): void => { + if (cond) { + passed++; + console.log(` ✓ ${label}`); + } else { + failed++; + console.error(` ✗ ${label}`); + } +}; + +const fixture = (name: string): SerpExtraction => + JSON.parse(readFileSync(join(import.meta.dirname, "fixtures", name), "utf8")) as SerpExtraction; + +const ok = fixture("google-serp-ok.json"); +const captcha = fixture("google-serp-captcha.json"); +const empty = fixture("google-serp-empty.json"); + +// S1: redirect unwrapping + internal/javascript anchors dropped +{ + const results = parseGoogleSerp(ok.anchors, 10); + const urls = results.map((r) => r.url); + check(urls.includes("https://real.example/x"), "S1: /url?q= redirect is unwrapped to the real target"); + check(!urls.some((u) => u.includes("google.com")), "S1: google-internal anchors are dropped"); + check(!urls.some((u) => u.startsWith("javascript:")), "S1: javascript: anchors are dropped"); +} + +// S2: dedupe + rank + limit +{ + const results = parseGoogleSerp(ok.anchors, 10); + const realExampleCount = results.filter((r) => r.url.startsWith("https://real.example/x")).length; + check(realExampleCount === 1, "S2: trailing-slash duplicate collapses to one result"); + check( + results.every((r, i) => r.rank === i + 1), + "S2: rank runs 1..n in input order", + ); + const capped = parseGoogleSerp(ok.anchors, 2); + check(capped.length === 2 && capped[capped.length - 1]?.rank === 2, "S2: limit truncates and ranks stay 1..limit"); +} + +// S3: CAPTCHA / no-results classification +{ + check(classifySerp(ok.pageText, parseGoogleSerp(ok.anchors, 10).length) === "ok", "S3: populated SERP classifies as ok"); + check(classifySerp(captcha.pageText, 0) === "captcha", "S3: captcha page classifies as captcha"); + check(classifySerp(empty.pageText, 0) === "no_results", "S3: empty results classify as no_results"); +} + +console.log(`\n${passed} passed, ${failed} failed`); +if (failed > 0) process.exit(1); |
