aboutsummaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authoramankumarsingh77 <amankumarsingh7702@gmail.com>2026-07-08 21:13:15 +0530
committeramankumarsingh77 <amankumarsingh7702@gmail.com>2026-07-08 21:13:15 +0530
commitc5c42df5976ed63faac2adcf3b04a4ad2f519b32 (patch)
tree77d389a590864eaf5c2408c9bc99ed07f55343d8 /test
parent7826c950adee89512162185d07fee565ab0e96cc (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.json4
-rw-r--r--test/deep-research/fixtures/google-serp-empty.json4
-rw-r--r--test/deep-research/fixtures/google-serp-ok.json40
-rw-r--r--test/deep-research/serp-parser-test.ts59
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);