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 | |
| 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')
| -rw-r--r-- | test/deep-research/fixtures/google-serp-captcha.json | 4 | ||||
| -rw-r--r-- | test/deep-research/fixtures/google-serp-empty.json | 4 | ||||
| -rw-r--r-- | test/deep-research/fixtures/google-serp-ok.json | 40 | ||||
| -rw-r--r-- | test/deep-research/serp-parser-test.ts | 59 |
4 files changed, 107 insertions, 0 deletions
diff --git a/test/deep-research/fixtures/google-serp-captcha.json b/test/deep-research/fixtures/google-serp-captcha.json new file mode 100644 index 0000000..7c6610f --- /dev/null +++ b/test/deep-research/fixtures/google-serp-captcha.json @@ -0,0 +1,4 @@ +{ + "anchors": [], + "pageText": "About this page Our systems have detected unusual traffic from your computer network. This page checks to see if it's really you sending the requests and not a robot. Why did this happen? To continue, please type the characters below. reCAPTCHA." +} diff --git a/test/deep-research/fixtures/google-serp-empty.json b/test/deep-research/fixtures/google-serp-empty.json new file mode 100644 index 0000000..34bb1dc --- /dev/null +++ b/test/deep-research/fixtures/google-serp-empty.json @@ -0,0 +1,4 @@ +{ + "anchors": [], + "pageText": "Your search - asdkfjhqwlekjrhqwlekjrhqwe - did not match any documents. Suggestions: Make sure that all words are spelled correctly. Try different keywords." +} diff --git a/test/deep-research/fixtures/google-serp-ok.json b/test/deep-research/fixtures/google-serp-ok.json new file mode 100644 index 0000000..61aa483 --- /dev/null +++ b/test/deep-research/fixtures/google-serp-ok.json @@ -0,0 +1,40 @@ +{ + "anchors": [ + { + "href": "https://www.google.com/url?q=https://real.example/x&sa=U&ved=abc", + "heading": "Real Example X — the definitive guide", + "snippet": "Real Example X — the definitive guide. Everything about x, explained clearly." + }, + { + "href": "https://docs.example.org/guide", + "heading": "Official Guide", + "snippet": "Official Guide to the thing you searched for." + }, + { + "href": "https://www.google.com/search?q=related", + "heading": "Related searches on Google", + "snippet": "internal google navigation" + }, + { + "href": "javascript:void(0)", + "heading": "Expand", + "snippet": "ui control" + }, + { + "href": "https://real.example/x/", + "heading": "Real Example X (duplicate with trailing slash)", + "snippet": "should be deduped against the first result" + }, + { + "href": "https://second.example/a", + "heading": "Second Example A", + "snippet": "Second result body." + }, + { + "href": "https://third.example/b", + "heading": "Third Example B", + "snippet": "Third result body." + } + ], + "pageText": "Real Example X the definitive guide Official Guide Second Example Third Example About 12,300 results" +} 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); |
