diff --git a/packages/ai/test/pi-native-client.test.ts b/packages/ai/test/pi-native-client.test.ts index f9adecbca..930c23166 100644 --- a/packages/ai/test/pi-native-client.test.ts +++ b/packages/ai/test/pi-native-client.test.ts @@ -48,12 +48,30 @@ function stalledBody(bytes: Uint8Array[] = []): ReadableStream { } function delayedBody(chunks: Array<{ atMs: number; bytes: Uint8Array }>): ReadableStream { + let active = true; return new ReadableStream({ start(controller) { for (const chunk of chunks) { - setTimeout(() => controller.enqueue(chunk.bytes), chunk.atMs); + setTimeout(() => { + if (!active) return; + try { + controller.enqueue(chunk.bytes); + } catch {} + }, chunk.atMs); } - setTimeout(() => controller.close(), Math.max(...chunks.map(chunk => chunk.atMs)) + 1); + setTimeout( + () => { + if (!active) return; + active = false; + try { + controller.close(); + } catch {} + }, + Math.max(...chunks.map(chunk => chunk.atMs)) + 1, + ); + }, + cancel() { + active = false; }, }); } @@ -348,8 +366,8 @@ describe("streamPiNative event flow", () => { const stream = streamPiNative(fakeModel(), baseContext, { apiKey: "k", fetch: fetchImpl, - streamFirstEventTimeoutMs: 40, - streamIdleTimeoutMs: 30, + streamFirstEventTimeoutMs: 1000, + streamIdleTimeoutMs: 1000, }); const result = await stream.result(); diff --git a/packages/coding-agent/scripts/compile-binary.ts b/packages/coding-agent/scripts/compile-binary.ts index a8b7d4885..5e7d27710 100644 --- a/packages/coding-agent/scripts/compile-binary.ts +++ b/packages/coding-agent/scripts/compile-binary.ts @@ -1,5 +1,5 @@ -import { createLegacyPiVirtualModulePlugin } from "./legacy-pi-virtual-module"; import { buildDocsIndexPayload } from "./generate-docs-index"; +import { createLegacyPiVirtualModulePlugin } from "./legacy-pi-virtual-module"; /** Inputs shared by local and release coding-agent binary builds. */ export interface CodingAgentCompileOptions { diff --git a/packages/coding-agent/src/extensibility/plugins/legacy-pi-compat.ts b/packages/coding-agent/src/extensibility/plugins/legacy-pi-compat.ts index 190aaf13b..62f4ea90e 100644 --- a/packages/coding-agent/src/extensibility/plugins/legacy-pi-compat.ts +++ b/packages/coding-agent/src/extensibility/plugins/legacy-pi-compat.ts @@ -1,3 +1,4 @@ +/// import * as fs from "node:fs"; import { isBuiltin } from "node:module"; import * as path from "node:path"; @@ -1233,7 +1234,10 @@ async function collectExtensionModules(entryRealPath: string): Promise): void { +function installExtensionGraphHook( + entryRealPath: string, + modules: Map, +): { asyncModules: Map; syncCommonJsModules: Map } { const asyncModules = new Map(); const syncCommonJsModules = new Map(); for (const [modulePath, source] of modules) { @@ -1289,6 +1293,7 @@ function installExtensionGraphHook(entryRealPath: string, modules: Map | undefined> { +async function ensureExtensionGraphHook(entryRealPath: string): Promise<{ clear(): void } | undefined> { const currentModules = await collectExtensionModules(entryRealPath); let hookedModules = extensionGraphHookModules.get(entryRealPath); if (!hookedModules) { @@ -1317,11 +1322,16 @@ async function ensureExtensionGraphHook(entryRealPath: string): Promise { expect(decodeDocsIndex("")).toBeNull(); }); }); - diff --git a/packages/coding-agent/test/tool-regex-guidance.test.ts b/packages/coding-agent/test/tool-regex-guidance.test.ts deleted file mode 100644 index 6dd0a5338..000000000 --- a/packages/coding-agent/test/tool-regex-guidance.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { afterEach, describe, expect, it } from "bun:test"; -import * as fs from "node:fs/promises"; -import * as os from "node:os"; -import * as path from "node:path"; -import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; -import type { ToolSession } from "@oh-my-pi/pi-coding-agent/tools"; -import { BashTool } from "@oh-my-pi/pi-coding-agent/tools/bash"; -import { GrepTool } from "@oh-my-pi/pi-coding-agent/tools/grep"; -import { removeWithRetries } from "@oh-my-pi/pi-utils"; - -function makeSession(cwd: string): ToolSession { - return { - cwd, - hasUI: false, - skills: [], - getSessionFile: () => null, - settings: Settings.isolated({ - "async.enabled": false, - "bash.autoBackground.enabled": false, - "bash.autoBackground.thresholdMs": 60_000, - "bashInterceptor.enabled": false, - "astGrep.enabled": true, - "astEdit.enabled": true, - "grep.enabled": true, - "glob.enabled": true, - "edit.mode": "patch", - readLineNumbers: true, - }), - getClientBridge: () => undefined, - } as unknown as ToolSession; -} - -function textOf(result: { content: Array<{ type: string; text?: string }> }): string { - return result.content - .filter(entry => entry.type === "text") - .map(entry => entry.text ?? "") - .join("\n"); -} - -function expectAlternationGuidance(description: string, goodPattern: string, escapedPattern: string): void { - expect(description).toContain(goodPattern); - expect(description).toContain(escapedPattern); - const goodIndex = description.indexOf(goodPattern); - const escapedIndex = description.indexOf(escapedPattern); - const start = Math.max(0, Math.min(goodIndex, escapedIndex) - 160); - const end = Math.min(description.length, Math.max(goodIndex, escapedIndex) + escapedPattern.length + 160); - const localGuidance = description.slice(start, end); - expect(localGuidance).toMatch(/\b(?:not|avoid|rather than|instead of|don't|do not)\b/i); -} -function expectEscapedBreWarning(description: string, escapedToken: string): void { - expect(description).toContain(escapedToken); - const tokenIndex = description.indexOf(escapedToken); - const localGuidance = description.slice(Math.max(0, tokenIndex - 120), tokenIndex + escapedToken.length + 120); - expect(localGuidance).toMatch(/\b(?:not|avoid|rather than|instead of|don't|do not|not guaranteed)\b/i); -} - -describe("tool regex guidance", () => { - const tempDirs: string[] = []; - - afterEach(async () => { - await Promise.all(tempDirs.splice(0).map(dir => removeWithRetries(dir))); - }); - - it("advertises Rust-style alternation for the built-in grep pattern", () => { - const description = new GrepTool(makeSession("/tmp")).description; - - expect(description).toContain("Rust"); - expect(description).toContain("RE2"); - expectAlternationGuidance(description, "foo|bar", String.raw`foo\|bar`); - expect(description).toContain(String.raw`\bword\b`); - }); - - it("advertises grep -E for shell commands that need alternation", () => { - const description = new BashTool(makeSession("/tmp")).description; - - expect(description).toContain("grep -E 'json|tool'"); - expectEscapedBreWarning(description, String.raw`\|`); - }); - - it("runs an extended-grep command through BashTool", async () => { - const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "omp-bash-grep-guidance-")); - tempDirs.push(cwd); - await Bun.write(path.join(cwd, "fixture.txt"), "json contract\ntool description\nignored line\njson later\n"); - - const result = await new BashTool(makeSession(cwd)).execute("grep-e-command", { - command: "grep -E 'json|tool' fixture.txt", - }); - - expect(result.isError).toBeUndefined(); - const output = textOf(result); - expect(output).toContain("json contract"); - expect(output).toContain("tool description"); - expect(output).toContain("json later"); - expect(output).not.toContain("ignored line"); - }); -}); diff --git a/packages/coding-agent/test/tools/web-search-bing.test.ts b/packages/coding-agent/test/tools/web-search-bing.test.ts deleted file mode 100644 index eb5766a00..000000000 --- a/packages/coding-agent/test/tools/web-search-bing.test.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { describe, expect, it } from "bun:test"; -import type { AuthStorage, FetchImpl } from "@oh-my-pi/pi-ai"; -import type { SearchParams } from "@oh-my-pi/pi-coding-agent/web/search/providers/base"; -import { searchBing } from "@oh-my-pi/pi-coding-agent/web/search/providers/bing"; -import { SearchProviderError } from "@oh-my-pi/pi-coding-agent/web/search/types"; - -const fakeAuthStorage = { - async getApiKey() { - throw new Error("Bing search must not request API keys"); - }, - resolver() { - throw new Error("Bing search must not request credential resolvers"); - }, - hasAuth() { - throw new Error("Bing search must not check auth"); - }, -} as unknown as AuthStorage; - -function makeParams(query: string, fetch: FetchImpl): SearchParams { - return { - query, - authStorage: fakeAuthStorage, - systemPrompt: "Bing search test prompt", - fetch, - }; -} - -/** Wrap a target URL the way Bing's `/ck/a` click-tracking redirect does (`u=a1`). */ -function wrapBingHref(target: string): string { - const payload = Buffer.from(target, "utf-8").toString("base64url"); - return `https://www.bing.com/ck/a?!&&p=6ddfcabc8528ae9bbd50e99e9ccfcb85&ptn=3&ver=2&hsh=4&fclid=2a06eaa3&u=a1${payload}&ntb=1`; -} - -/** Render a `b_algo` block matching Bing's live markup (entity-escaped href, sitelink anchor outside `h2`). */ -function algoResult(href: string, title: string, snippet?: string): string { - const escaped = href.replace(/&/g, "&"); - return `
  • - -

    ${title}

    - ${snippet ? `

    ${snippet}

    ` : ""} -
  • `; -} - -function resultsPage(...items: string[]): string { - return `
      ${items.join("\n")}
    `; -} - -describe("Bing web search provider", () => { - it("requests the HTML result page with browser navigation headers and recency filter", async () => { - let capturedUrl = ""; - let capturedInit: RequestInit | undefined; - const fetchMock: FetchImpl = (input, init) => { - capturedUrl = typeof input === "string" ? input : input.toString(); - capturedInit = init; - return Promise.resolve( - new Response( - resultsPage(algoResult(wrapBingHref("https://example.com/result"), "Result", "Search snippet")), - { status: 200, headers: { "Content-Type": "text/html" } }, - ), - ); - }; - - const response = await searchBing({ - ...makeParams("browser headers & parsing", fetchMock), - numSearchResults: 99, - recency: "week", - }); - - const url = new URL(capturedUrl); - expect(url.origin + url.pathname).toBe("https://www.bing.com/search"); - expect(url.searchParams.get("q")).toBe("browser headers & parsing"); - expect(url.searchParams.get("count")).toBe("20"); - expect(url.searchParams.get("mkt")).toBe("en-US"); - expect(url.searchParams.get("setlang")).toBe("en"); - expect(url.searchParams.get("filters")).toBe('ex1:"ez2"'); - expect(capturedInit?.method).toBeUndefined(); - const headers = new Headers(capturedInit?.headers); - expect(headers.get("accept")).toContain("text/html"); - expect(headers.get("user-agent")).toMatch(/Chrome\/\d+\.0\.0\.0/); - expect(headers.get("referer")).toBe("https://www.bing.com/"); - expect(headers.get("sec-fetch-dest")).toBe("document"); - expect(headers.get("sec-fetch-mode")).toBe("navigate"); - expect(headers.get("sec-fetch-site")).toBe("same-origin"); - expect(response.sources).toEqual([ - { title: "Result", url: "https://example.com/result", snippet: "Search snippet" }, - ]); - }); - - it("maps every recency window to Bing's native freshness codes and omits the param otherwise", async () => { - const filtersFor = async (recency?: SearchParams["recency"]): Promise => { - let captured = ""; - const fetchMock: FetchImpl = input => { - captured = typeof input === "string" ? input : input.toString(); - return Promise.resolve(new Response(resultsPage(), { status: 200 })); - }; - await searchBing({ ...makeParams("recency mapping", fetchMock), recency }); - return new URL(captured).searchParams.get("filters"); - }; - - expect(await filtersFor("day")).toBe('ex1:"ez1"'); - expect(await filtersFor("month")).toBe('ex1:"ez3"'); - expect(await filtersFor(undefined)).toBeNull(); - - // "Past year" has no fixed code; Bing's own dropdown emits an epoch-day range. - const year = await filtersFor("year"); - const match = year?.match(/^ex1:"ez5_(\d+)_(\d+)"$/); - expect(match).toBeTruthy(); - const [, start, end] = match as RegExpMatchArray; - expect(Number(end) - Number(start)).toBe(365); - expect(Math.abs(Number(end) - Math.floor(Date.now() / 86_400_000))).toBeLessThanOrEqual(1); - }); - - it("unwraps ck/a redirects, keeps direct links, deduplicates targets, and skips junk rows", async () => { - const target = "https://example.com/docs?a=1&b=2"; - const html = resultsPage( - algoResult( - wrapBingHref(target), - "Bun — A fast runtime", - `Jan 3, 2026 · Bundle & run JavaScript. How to verify you are human on CAPTCHA walls.`, - ), - // Direct external href; snippet only in the b_algoSlug fallback container. - `
  • Direct result

    Slug snippet
  • `, - algoResult(wrapBingHref(target), "Duplicate target", "duplicate"), - // Bing-internal navigation link must be dropped. - algoResult("https://www.bing.com/images/search?q=x", "Images tab"), - // Unknown u= payload version must be dropped, not garbage-decoded. - `
  • Future wrapper

  • `, - algoResult(wrapBingHref("https://example.com/bare"), "No snippet row"), - ); - const fetchMock: FetchImpl = () => Promise.resolve(new Response(html, { status: 200 })); - - const response = await searchBing({ ...makeParams("mixed markup", fetchMock), numSearchResults: 10 }); - - expect(response.provider).toBe("bing"); - expect(response.sources).toEqual([ - { - title: "Bun — A fast runtime", - url: target, - snippet: "Jan 3, 2026 · Bundle & run JavaScript. How to verify you are human on CAPTCHA walls.", - }, - { title: "Direct result", url: "https://example.com/direct", snippet: "Slug snippet" }, - { title: "No snippet row", url: "https://example.com/bare", snippet: undefined }, - ]); - }); - - it("returns empty sources for Bing's genuine no-results page", async () => { - const html = `
    1. There are no results for xzqv

    `; - const fetchMock: FetchImpl = () => Promise.resolve(new Response(html, { status: 200 })); - - const response = await searchBing(makeParams("xzqv", fetchMock)); - - expect(response).toEqual({ provider: "bing", sources: [] }); - }); - - it("surfaces Bing's CAPTCHA challenge as a provider-tagged 429", async () => { - const challenge = `
    Please solve the challenge
    `; - const fetchMock: FetchImpl = () => Promise.resolve(new Response(challenge, { status: 200 })); - - try { - await searchBing(makeParams("blocked", fetchMock)); - expect.unreachable("Bing CAPTCHA challenge should reject"); - } catch (error) { - expect(error).toBeInstanceOf(SearchProviderError); - expect(error).toMatchObject({ provider: "bing", status: 429 }); - expect((error as SearchProviderError).message).toContain("CAPTCHA"); - } - }); - - it("propagates non-OK HTTP statuses as provider errors", async () => { - const fetchMock: FetchImpl = () => Promise.resolve(new Response("Service Unavailable", { status: 503 })); - - try { - await searchBing(makeParams("outage", fetchMock)); - expect.unreachable("HTTP 503 should reject"); - } catch (error) { - expect(error).toBeInstanceOf(SearchProviderError); - expect(error).toMatchObject({ - provider: "bing", - status: 503, - message: "Bing HTML error (503)", - }); - } - }); -}); diff --git a/packages/coding-agent/test/tools/web-search-duckduckgo.test.ts b/packages/coding-agent/test/tools/web-search-duckduckgo.test.ts deleted file mode 100644 index 0c513a884..000000000 --- a/packages/coding-agent/test/tools/web-search-duckduckgo.test.ts +++ /dev/null @@ -1,268 +0,0 @@ -import { describe, expect, it } from "bun:test"; -import type { AuthStorage, FetchImpl } from "@oh-my-pi/pi-ai"; -import { searchDuckDuckGo } from "@oh-my-pi/pi-coding-agent/web/search/providers/duckduckgo"; -import { SearchProviderError } from "@oh-my-pi/pi-coding-agent/web/search/types"; -import { formatSearchProviderFailures } from "../../src/web/search/provider"; - -const fakeAuthStorage = { - async getApiKey() { - throw new Error("DuckDuckGo must not request API keys"); - }, - resolver() { - throw new Error("DuckDuckGo must not request credential resolvers"); - }, - hasAuth() { - throw new Error("DuckDuckGo search must not check auth"); - }, -} as unknown as AuthStorage; - -function makeParams(query: string, fetch: FetchImpl) { - return { - query, - authStorage: fakeAuthStorage, - systemPrompt: "DuckDuckGo test prompt", - fetch, - } as const; -} - -function htmlPage(...results: Array<{ url: string; title: string; snippet?: string }>): string { - const blocks = results - .map(r => { - const href = `//duckduckgo.com/l/?uddg=${encodeURIComponent(r.url)}&rut=abc`; - const snippet = r.snippet === undefined ? "" : `${r.snippet}`; - return ``; - }) - .join("\n"); - return `${blocks}`; -} - -function anomalyPage(): string { - return ` -
    -
    Unfortunately, bots use DuckDuckGo too.
    -
    - `; -} - -describe("DuckDuckGo web search provider", () => { - it("POSTs the query and recency filter to the no-JS HTML frontend", async () => { - const captured: { url?: string } = {}; - let capturedInit: RequestInit | undefined; - const fetchMock: FetchImpl = (input, init) => { - captured.url = typeof input === "string" ? input : input.toString(); - capturedInit = init; - return Promise.resolve( - new Response(htmlPage({ url: "https://example.com/a", title: "A" }), { - status: 200, - headers: { "Content-Type": "text/html" }, - }), - ); - }; - - await searchDuckDuckGo({ ...makeParams("how to fix bug in code", fetchMock), recency: "week" }); - - expect(captured.url).toBe("https://html.duckduckgo.com/html/"); - expect(capturedInit?.method).toBe("POST"); - const form = new URLSearchParams(capturedInit?.body as string); - expect(form.get("q")).toBe("how to fix bug in code"); - expect(form.get("kl")).toBe("us-en"); - expect(form.get("b")).toBe(""); - expect(form.get("df")).toBe("w"); - const headers = capturedInit?.headers as Record; - expect(headers["Content-Type"]).toBe("application/x-www-form-urlencoded"); - expect(headers["User-Agent"]).toContain("Mozilla/5.0"); - expect(headers.Referer).toBe("https://html.duckduckgo.com/"); - expect(headers["Accept-Language"]).toContain("en"); - expect(headers["Sec-Fetch-Mode"]).toBe("navigate"); - expect(headers["Sec-Ch-Ua"]).toContain("Chromium"); - }); - - it("omits the df form param when no recency is requested", async () => { - let capturedInit: RequestInit | undefined; - const fetchMock: FetchImpl = (_input, init) => { - capturedInit = init; - return Promise.resolve( - new Response(htmlPage({ url: "https://example.com/x", title: "X" }), { - status: 200, - headers: { "Content-Type": "text/html" }, - }), - ); - }; - - await searchDuckDuckGo(makeParams("plain query", fetchMock)); - - const form = new URLSearchParams(capturedInit?.body as string); - expect(form.has("df")).toBe(false); - }); - - it("parses result blocks, unwraps DDG redirect URLs, and clamps to numSearchResults", async () => { - const fetchMock: FetchImpl = () => - Promise.resolve( - new Response( - htmlPage( - { - url: "https://example.com/first", - title: "First & result", - snippet: "Snippet one", - }, - { url: "https://example.com/second", title: "Second" }, - { url: "https://example.com/third", title: "Third" }, - ), - { status: 200, headers: { "Content-Type": "text/html" } }, - ), - ); - - const response = await searchDuckDuckGo({ ...makeParams("multi", fetchMock), numSearchResults: 2 }); - - expect(response.provider).toBe("duckduckgo"); - expect(response.answer).toBeUndefined(); - expect(response.sources).toEqual([ - { - title: "First & result", - url: "https://example.com/first", - snippet: "Snippet one", - }, - { - title: "Second", - url: "https://example.com/second", - snippet: undefined, - }, - ]); - }); - - it("deduplicates results that share the same target URL", async () => { - const fetchMock: FetchImpl = () => - Promise.resolve( - new Response( - htmlPage( - { url: "https://example.com/dup", title: "First copy", snippet: "one" }, - { url: "https://example.com/dup", title: "Second copy", snippet: "two" }, - { url: "https://example.com/unique", title: "Other" }, - ), - { status: 200, headers: { "Content-Type": "text/html" } }, - ), - ); - - const response = await searchDuckDuckGo(makeParams("dup query", fetchMock)); - - expect(response.sources.map(s => s.url)).toEqual(["https://example.com/dup", "https://example.com/unique"]); - }); - - it("clamps oversized result limits to the provider maximum", async () => { - const many = Array.from({ length: 40 }, (_, i) => ({ - url: `https://example.com/r-${i}`, - title: `Result ${i}`, - })); - const fetchMock: FetchImpl = () => - Promise.resolve( - new Response(htmlPage(...many), { - status: 200, - headers: { "Content-Type": "text/html" }, - }), - ); - - const response = await searchDuckDuckGo({ ...makeParams("clamp", fetchMock), numSearchResults: 999 }); - - expect(response.sources).toHaveLength(20); - expect(response.sources.at(0)?.url).toBe("https://example.com/r-0"); - expect(response.sources.at(-1)?.url).toBe("https://example.com/r-19"); - }); - - it("supports unwrapped result hrefs (sponsored/instant rows)", async () => { - const html = `

    - Direct -

    `; - const fetchMock: FetchImpl = () => - Promise.resolve(new Response(html, { status: 200, headers: { "Content-Type": "text/html" } })); - - const response = await searchDuckDuckGo(makeParams("direct", fetchMock)); - - expect(response.sources).toEqual([{ title: "Direct", url: "https://direct.example/page", snippet: undefined }]); - }); - - it("throws a clear SearchProviderError when DDG serves the anomaly modal", async () => { - const fetchMock: FetchImpl = () => - Promise.resolve( - new Response(anomalyPage(), { - status: 202, - headers: { "Content-Type": "text/html" }, - }), - ); - - try { - await searchDuckDuckGo(makeParams("blocked", fetchMock)); - expect.unreachable("DDG anomaly response should reject"); - } catch (error) { - expect(error).toBeInstanceOf(SearchProviderError); - const err = error as SearchProviderError; - expect(err.provider).toBe("duckduckgo"); - expect(err.status).toBe(429); - expect(err.message).toMatch(/bot-detection challenge/i); - expect(err.message).toContain("datacenter/shared-egress IPs"); - expect(err.message).toContain("configure a credentialed provider"); - } - }); - - it("flags anomaly pages served with a 200 status", async () => { - const fetchMock: FetchImpl = () => - Promise.resolve( - new Response(anomalyPage(), { - status: 200, - headers: { "Content-Type": "text/html" }, - }), - ); - - await expect(searchDuckDuckGo(makeParams("blocked-200", fetchMock))).rejects.toMatchObject({ - provider: "duckduckgo", - status: 429, - }); - }); - - it("throws a provider-tagged SearchProviderError for HTTP failures", async () => { - const fetchMock: FetchImpl = () => - Promise.resolve( - new Response("upstream unavailable", { - status: 503, - }), - ); - - try { - await searchDuckDuckGo(makeParams("http failure", fetchMock)); - expect.unreachable("DuckDuckGo HTTP failure should reject"); - } catch (error) { - expect(error).toBeInstanceOf(SearchProviderError); - expect(error).toMatchObject({ - provider: "duckduckgo", - status: 503, - message: "DuckDuckGo HTML error (503)", - }); - } - }); - - it("formats DuckDuckGo bot detection clearly in a fallback-chain failure", () => { - const message = `All web search providers failed: ${formatSearchProviderFailures([ - { - provider: { id: "codex", label: "OpenAI" }, - error: new SearchProviderError("codex", "codex: 401 unauthorized", 401), - }, - { - provider: { id: "duckduckgo", label: "DuckDuckGo" }, - error: new SearchProviderError( - "duckduckgo", - "DuckDuckGo blocked the request with a bot-detection challenge. DuckDuckGo throttles automated HTML searches from datacenter/shared-egress IPs; configure a credentialed provider such as Brave, Tavily, Exa, or Kagi for reliable web search.", - 429, - ), - }, - ])}`; - - expect(message).toContain("All web search providers failed"); - expect(message).toContain("codex: OpenAI authorization failed (401). Check API key or base URL."); - expect(message).toContain("duckduckgo: DuckDuckGo blocked the request with a bot-detection challenge."); - expect(message).toContain("datacenter/shared-egress IPs"); - expect(message).toContain("configure a credentialed provider"); - expect(message).not.toContain("codex: 401 unauthorized"); - }); -}); diff --git a/packages/coding-agent/test/tools/web-search-ecosia.test.ts b/packages/coding-agent/test/tools/web-search-ecosia.test.ts deleted file mode 100644 index af5544b72..000000000 --- a/packages/coding-agent/test/tools/web-search-ecosia.test.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { describe, expect, it } from "bun:test"; -import type { AuthStorage, FetchImpl } from "@oh-my-pi/pi-ai"; -import type { SearchParams } from "@oh-my-pi/pi-coding-agent/web/search/providers/base"; -import { searchEcosia } from "@oh-my-pi/pi-coding-agent/web/search/providers/ecosia"; -import { SearchProviderError } from "@oh-my-pi/pi-coding-agent/web/search/types"; - -const fakeAuthStorage = { - async getApiKey() { - throw new Error("Ecosia search must not request API keys"); - }, - resolver() { - throw new Error("Ecosia search must not request credential resolvers"); - }, - hasAuth() { - throw new Error("Ecosia search must not check auth"); - }, -} as unknown as AuthStorage; - -function makeParams(query: string, fetch: FetchImpl): SearchParams { - return { - query, - authStorage: fakeAuthStorage, - systemPrompt: "Ecosia search test prompt", - fetch, - }; -} - -/** - * Trimmed organic result matching Ecosia's server-rendered markup: two - * `result-link` anchors per article (breadcrumb row + title row) and a - * description container that mixes a screen-reader thumbnail caption with - * the real `web-result-description` paragraph. - */ -function organicResult(url: string, title: string, snippet?: string): string { - return `
    - -
    `; -} - -/** Trimmed Cloudflare managed-challenge page as served by Ecosia's firewall. */ -const CLOUDFLARE_CHALLENGE = `Ecosia Firewall
    -

    Confirm you’re not a robot

    -

    Our system has detected unusual traffic from your network. Please solve the challenge below to show you’re not a robot.

    - -
    `; - -describe("Ecosia web search provider", () => { - it("requests the results page with browser navigation headers and ignores recency", async () => { - let capturedUrl = ""; - let capturedInit: RequestInit | undefined; - const fetchMock: FetchImpl = (input, init) => { - capturedUrl = typeof input === "string" ? input : input.toString(); - capturedInit = init; - return Promise.resolve( - new Response(organicResult("https://example.com/result", "Result", "Search snippet"), { - status: 200, - headers: { "Content-Type": "text/html" }, - }), - ); - }; - - const response = await searchEcosia({ - ...makeParams("browser headers & parsing", fetchMock), - numSearchResults: 99, - recency: "week", - }); - - const url = new URL(capturedUrl); - expect(url.origin + url.pathname).toBe("https://www.ecosia.org/search"); - expect(url.searchParams.get("q")).toBe("browser headers & parsing"); - // Ecosia has no confirmed time filter; recency must not leak into the request. - expect([...url.searchParams.keys()]).toEqual(["q"]); - expect(capturedInit?.method).toBeUndefined(); - const headers = new Headers(capturedInit?.headers); - expect(headers.get("accept")).toContain("text/html"); - expect(headers.get("user-agent")).toMatch(/Chrome\/\d+\.0\.0\.0/); - expect(headers.get("referer")).toBe("https://www.ecosia.org/"); - expect(headers.get("sec-fetch-dest")).toBe("document"); - expect(headers.get("sec-fetch-mode")).toBe("navigate"); - expect(headers.get("sec-fetch-site")).toBe("same-origin"); - expect(response.sources).toEqual([ - { title: "Result", url: "https://example.com/result", snippet: "Search snippet" }, - ]); - }); - - it("parses organic articles, skips junk rows, and deduplicates targets", async () => { - const html = [ - organicResult("https://example.com/first", "First & decoded", "Leading snippet"), - // Ad slot: rendered client-side into an empty container, never an organic article. - `
    `, - // Internal navigation and non-http targets must be rejected. - organicResult("https://www.ecosia.org/images?q=first", "Images vertical", "internal"), - organicResult("javascript:void(0)", "Script link", "junk"), - // Article without a title heading is skipped. - `
    `, - organicResult("https://example.com/first", "Duplicate of first", "duplicate"), - organicResult("https://example.com/bare", "Snippetless result"), - ].join("\n"); - const fetchMock: FetchImpl = () => Promise.resolve(new Response(html, { status: 200 })); - - const response = await searchEcosia({ ...makeParams("mixed markup", fetchMock), numSearchResults: 10 }); - - expect(response.provider).toBe("ecosia"); - expect(response.sources).toEqual([ - { title: "First & decoded", url: "https://example.com/first", snippet: "Leading snippet" }, - { title: "Snippetless result", url: "https://example.com/bare", snippet: undefined }, - ]); - }); - - it("clamps sources to the requested count", async () => { - const html = [ - organicResult("https://example.com/1", "One", "s1"), - organicResult("https://example.com/2", "Two", "s2"), - organicResult("https://example.com/3", "Three", "s3"), - ].join("\n"); - const fetchMock: FetchImpl = () => Promise.resolve(new Response(html, { status: 200 })); - - const response = await searchEcosia({ ...makeParams("clamped", fetchMock), numSearchResults: 2 }); - - expect(response.sources.map(source => source.url)).toEqual(["https://example.com/1", "https://example.com/2"]); - }); - - it("surfaces the Cloudflare challenge as a provider-tagged 429, regardless of HTTP status", async () => { - for (const status of [403, 200]) { - const fetchMock: FetchImpl = () => Promise.resolve(new Response(CLOUDFLARE_CHALLENGE, { status })); - try { - await searchEcosia(makeParams("blocked", fetchMock)); - expect.unreachable(`Cloudflare challenge with status ${status} should reject`); - } catch (error) { - expect(error).toBeInstanceOf(SearchProviderError); - expect(error).toMatchObject({ provider: "ecosia", status: 429 }); - expect((error as SearchProviderError).message).toContain("Cloudflare bot challenge"); - } - } - }); - - it("maps non-challenge HTTP failures to a provider-tagged error with the upstream status", async () => { - const fetchMock: FetchImpl = () => - Promise.resolve(new Response("Internal Server Error", { status: 500 })); - - try { - await searchEcosia(makeParams("broken", fetchMock)); - expect.unreachable("HTTP 500 should reject"); - } catch (error) { - expect(error).toBeInstanceOf(SearchProviderError); - expect(error).toMatchObject({ - provider: "ecosia", - status: 500, - message: "Ecosia HTML error (500)", - }); - } - }); -}); diff --git a/packages/coding-agent/test/tools/web-search-google.test.ts b/packages/coding-agent/test/tools/web-search-google.test.ts deleted file mode 100644 index 0976ce381..000000000 --- a/packages/coding-agent/test/tools/web-search-google.test.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { describe, expect, it } from "bun:test"; -import type { AuthStorage, FetchImpl } from "@oh-my-pi/pi-ai"; -import type { SearchParams } from "@oh-my-pi/pi-coding-agent/web/search/providers/base"; -import { searchGoogle } from "@oh-my-pi/pi-coding-agent/web/search/providers/google"; -import { SearchProviderError } from "@oh-my-pi/pi-coding-agent/web/search/types"; - -const fakeAuthStorage = { - async getApiKey() { - throw new Error("Google search must not request API keys"); - }, - resolver() { - throw new Error("Google search must not request credential resolvers"); - }, - hasAuth() { - throw new Error("Google search must not check auth"); - }, -} as unknown as AuthStorage; - -function makeParams(query: string, fetch: FetchImpl): SearchParams { - return { - query, - authStorage: fakeAuthStorage, - systemPrompt: "Google search test prompt", - fetch, - }; -} - -function modernResult(url: string, title: string, snippet?: string): string { - return `
    -

    ${title}

    - ${snippet ? `
    ${snippet}
    ` : ""} -
    `; -} - -describe("Google web search provider", () => { - it("requests the rendered Web result page with browser navigation headers and recency", async () => { - let capturedUrl = ""; - let capturedInit: RequestInit | undefined; - const fetchMock: FetchImpl = (input, init) => { - capturedUrl = typeof input === "string" ? input : input.toString(); - capturedInit = init; - return Promise.resolve( - new Response(modernResult("https://example.com/result", "Result", "Search snippet"), { - status: 200, - headers: { "Content-Type": "text/html" }, - }), - ); - }; - - const response = await searchGoogle({ - ...makeParams("browser headers & parsing", fetchMock), - numSearchResults: 99, - recency: "week", - }); - - const url = new URL(capturedUrl); - expect(url.origin + url.pathname).toBe("https://www.google.com/search"); - expect(url.searchParams.get("q")).toBe("browser headers & parsing"); - expect(url.searchParams.get("num")).toBe("20"); - expect(url.searchParams.get("hl")).toBe("en"); - expect(url.searchParams.get("gl")).toBe("us"); - expect(url.searchParams.get("udm")).toBe("14"); - expect(url.searchParams.get("pws")).toBe("0"); - expect(url.searchParams.get("tbs")).toBe("qdr:w"); - expect(capturedInit?.method).toBeUndefined(); - const headers = new Headers(capturedInit?.headers); - expect(headers.get("accept")).toContain("text/html"); - expect(headers.get("user-agent")).toMatch(/Chrome\/\d+\.0\.0\.0/); - expect(headers.get("referer")).toBe("https://www.google.com/"); - expect(headers.get("sec-fetch-dest")).toBe("document"); - expect(headers.get("sec-fetch-mode")).toBe("navigate"); - expect(headers.get("sec-fetch-site")).toBe("same-origin"); - expect(response.sources).toEqual([ - { title: "Result", url: "https://example.com/result", snippet: "Search snippet" }, - ]); - }); - - it("parses modern and legacy result markup, unwraps redirects, and deduplicates targets", async () => { - const target = "https://example.com/legacy?a=1&b=2"; - const redirect = `/url?q=${encodeURIComponent(target)}&sa=U`; - const html = [ - modernResult( - "https://example.com/modern", - "Modern & result", - "A useful snippet Read more", - ), - `

    Legacy result

    Legacy snippet
    `, - modernResult(target, "Duplicate target", "duplicate"), - modernResult("/search?q=internal", "Google navigation"), - ].join("\n"); - const fetchMock: FetchImpl = () => Promise.resolve(new Response(html, { status: 200 })); - - const response = await searchGoogle({ ...makeParams("mixed markup", fetchMock), numSearchResults: 10 }); - - expect(response.provider).toBe("google"); - expect(response.sources).toEqual([ - { - title: "Modern & result", - url: "https://example.com/modern", - snippet: "A useful snippet", - }, - { - title: "Legacy result", - url: target, - snippet: "Legacy snippet", - }, - ]); - }); - - it("surfaces Google's JavaScript challenge as a provider-tagged block", async () => { - const challenge = ``; - const fetchMock: FetchImpl = () => Promise.resolve(new Response(challenge, { status: 200 })); - - try { - await searchGoogle(makeParams("blocked", fetchMock)); - expect.unreachable("Google JavaScript challenge should reject"); - } catch (error) { - expect(error).toBeInstanceOf(SearchProviderError); - expect(error).toMatchObject({ - provider: "google", - status: 429, - message: "Google returned its JavaScript challenge instead of rendered search results.", - }); - } - }); -}); diff --git a/packages/coding-agent/test/tools/web-search-startpage.test.ts b/packages/coding-agent/test/tools/web-search-startpage.test.ts deleted file mode 100644 index a826a20d8..000000000 --- a/packages/coding-agent/test/tools/web-search-startpage.test.ts +++ /dev/null @@ -1,243 +0,0 @@ -import { describe, expect, it } from "bun:test"; -import type { AuthStorage, FetchImpl } from "@oh-my-pi/pi-ai"; -import type { SearchParams } from "@oh-my-pi/pi-coding-agent/web/search/providers/base"; -import { searchStartpage } from "@oh-my-pi/pi-coding-agent/web/search/providers/startpage"; -import { SearchProviderError } from "@oh-my-pi/pi-coding-agent/web/search/types"; - -const fakeAuthStorage = { - async getApiKey() { - throw new Error("Startpage search must not request API keys"); - }, - resolver() { - throw new Error("Startpage search must not request credential resolvers"); - }, - hasAuth() { - throw new Error("Startpage search must not check auth"); - }, -} as unknown as AuthStorage; - -function makeParams(query: string, fetch: FetchImpl): SearchParams { - return { - query, - authStorage: fakeAuthStorage, - systemPrompt: "Startpage search test prompt", - fetch, - }; -} - -const SC_TOKEN = "2sbbv9IndMZLVHNdHqjDurhVAo8vrQSiu3q8EtaSRZMbFzmQ0Qt1"; - -/** Homepage shell trimmed from a live capture: the `/sp/search` form with its hidden inputs. */ -function homepageHtml(sc: string): string { - return ` - - `; -} - -/** Organic result block trimmed from a live capture. */ -function resultHtml(url: string, title: string, snippet?: string): string { - return `
    -
    -
    - -
    - -

    ${title}

    -
    - ${snippet ? `

    ${snippet}

    ` : ""} -
    `; -} - -function resultsPage(...blocks: string[]): string { - return `Startpage Search Results -
    ${blocks.join("\n")}
    -
    - `; -} - -/** CAPTCHA/error SPA shell trimmed from a live `/en/errors/` capture. */ -const CHALLENGE_HTML = `
    `; - -interface CapturedRequest { - url: string; - init: RequestInit | undefined; -} - -/** Dispatch mocked responses by URL; records every request for assertions. */ -function dispatchFetch(routes: Record Response>, captured: CapturedRequest[]): FetchImpl { - return (input, init) => { - const url = typeof input === "string" ? input : input.toString(); - captured.push({ url, init }); - const pathname = new URL(url).pathname; - const route = routes[pathname]; - if (!route) throw new Error(`Unexpected request: ${url}`); - return Promise.resolve(route()); - }; -} - -describe("Startpage web search provider", () => { - it("performs the homepage-token dance: GET home, then POST the form inputs with query and recency", async () => { - const captured: CapturedRequest[] = []; - const fetchMock = dispatchFetch( - { - "/": () => new Response(homepageHtml(SC_TOKEN), { status: 200 }), - "/sp/search": () => - new Response(resultsPage(resultHtml("https://example.com/result", "Result", "Search snippet")), { - status: 200, - }), - }, - captured, - ); - - const response = await searchStartpage({ - ...makeParams("browser headers & parsing", fetchMock), - numSearchResults: 99, - recency: "week", - }); - - expect(captured.map(r => r.url)).toEqual(["https://www.startpage.com/", "https://www.startpage.com/sp/search"]); - - const homeInit = captured[0].init; - expect(homeInit?.method).toBeUndefined(); - const homeHeaders = new Headers(homeInit?.headers); - expect(homeHeaders.get("accept")).toContain("text/html"); - expect(homeHeaders.get("user-agent")).toMatch(/Chrome\/\d+\.0\.0\.0/); - expect(homeHeaders.get("sec-fetch-site")).toBe("none"); - - const searchInit = captured[1].init; - expect(searchInit?.method).toBe("POST"); - const searchHeaders = new Headers(searchInit?.headers); - expect(searchHeaders.get("content-type")).toBe("application/x-www-form-urlencoded"); - expect(searchHeaders.get("referer")).toBe("https://www.startpage.com/"); - expect(searchHeaders.get("sec-fetch-site")).toBe("same-origin"); - expect(searchHeaders.get("sec-fetch-dest")).toBe("document"); - expect(searchHeaders.get("user-agent")).toMatch(/Chrome\/\d+\.0\.0\.0/); - - const form = new URLSearchParams(String(searchInit?.body)); - expect(form.get("query")).toBe("browser headers & parsing"); - expect(form.get("sc")).toBe(SC_TOKEN); - expect(form.get("with_date")).toBe("w"); - expect(form.get("cat")).toBe("home"); - expect(form.get("segment")).toBe("startpage.udog"); - expect(form.get("abp")).toBe("0"); - expect(form.get("lui")).toBe("english"); - - expect(response.provider).toBe("startpage"); - expect(response.sources).toEqual([ - { title: "Result", url: "https://example.com/result", snippet: "Search snippet" }, - ]); - }); - - it("parses result blocks, skips junk rows, deduplicates targets, and clamps to the requested count", async () => { - const html = resultsPage( - resultHtml("https://example.com/a", "First & best result", "A useful\n\tsnippet"), - resultHtml("https://example.com/a", "Duplicate of first", "duplicate"), - `

    No title anchor: instant-answer widget

    `, - resultHtml("https://www.startpage.com/en/privacy", "Internal Startpage link", "never a result"), - resultHtml("javascript:void(0)", "Bad scheme", "never a result"), - resultHtml("https://example.com/b", "Second result"), - resultHtml("https://example.com/c", "Third result", "clamped away"), - ); - const captured: CapturedRequest[] = []; - const fetchMock = dispatchFetch( - { - "/": () => new Response(homepageHtml(SC_TOKEN), { status: 200 }), - "/sp/search": () => new Response(html, { status: 200 }), - }, - captured, - ); - - const response = await searchStartpage({ ...makeParams("mixed markup", fetchMock), numSearchResults: 2 }); - - expect(response.sources).toEqual([ - { title: "First & best result", url: "https://example.com/a", snippet: "A useful snippet" }, - { title: "Second result", url: "https://example.com/b", snippet: undefined }, - ]); - }); - - it("falls back to a direct GET with query params when the homepage yields no sc token", async () => { - const captured: CapturedRequest[] = []; - const fetchMock = dispatchFetch( - { - "/": () => new Response("redesigned homepage without the form", { status: 200 }), - "/sp/search": () => - new Response(resultsPage(resultHtml("https://example.com/fallback", "Fallback", "via GET")), { - status: 200, - }), - }, - captured, - ); - - const response = await searchStartpage({ ...makeParams("fallback query", fetchMock), recency: "month" }); - - expect(captured).toHaveLength(2); - const searchUrl = new URL(captured[1].url); - expect(searchUrl.origin + searchUrl.pathname).toBe("https://www.startpage.com/sp/search"); - expect(searchUrl.searchParams.get("query")).toBe("fallback query"); - expect(searchUrl.searchParams.get("with_date")).toBe("m"); - expect(captured[1].init?.method).toBeUndefined(); - const headers = new Headers(captured[1].init?.headers); - expect(headers.get("referer")).toBe("https://www.startpage.com/"); - expect(headers.get("sec-fetch-site")).toBe("same-origin"); - expect(response.sources).toEqual([ - { title: "Fallback", url: "https://example.com/fallback", snippet: "via GET" }, - ]); - }); - - it("surfaces the CAPTCHA/error shell as a provider-tagged 429", async () => { - const captured: CapturedRequest[] = []; - const fetchMock = dispatchFetch( - { - "/": () => new Response(homepageHtml(SC_TOKEN), { status: 200 }), - "/sp/search": () => new Response(CHALLENGE_HTML, { status: 200 }), - }, - captured, - ); - - try { - await searchStartpage(makeParams("blocked", fetchMock)); - expect.unreachable("Startpage CAPTCHA shell should reject"); - } catch (error) { - expect(error).toBeInstanceOf(SearchProviderError); - expect(error).toMatchObject({ provider: "startpage", status: 429 }); - expect((error as SearchProviderError).message).toContain("Startpage"); - expect((error as SearchProviderError).message).toContain("CAPTCHA"); - } - }); - - it("maps non-OK search responses to a status-tagged provider error", async () => { - const captured: CapturedRequest[] = []; - const fetchMock = dispatchFetch( - { - "/": () => new Response(homepageHtml(SC_TOKEN), { status: 200 }), - "/sp/search": () => new Response("upstream exploded", { status: 500 }), - }, - captured, - ); - - try { - await searchStartpage(makeParams("server error", fetchMock)); - expect.unreachable("HTTP 500 should reject"); - } catch (error) { - expect(error).toBeInstanceOf(SearchProviderError); - expect(error).toMatchObject({ - provider: "startpage", - status: 500, - message: "Startpage HTML error (500)", - }); - } - }); -}); diff --git a/packages/coding-agent/test/tools/web-search-yahoo.test.ts b/packages/coding-agent/test/tools/web-search-yahoo.test.ts deleted file mode 100644 index df0167e14..000000000 --- a/packages/coding-agent/test/tools/web-search-yahoo.test.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { describe, expect, it } from "bun:test"; -import type { AuthStorage, FetchImpl } from "@oh-my-pi/pi-ai"; -import type { SearchParams } from "@oh-my-pi/pi-coding-agent/web/search/providers/base"; -import { searchYahoo } from "@oh-my-pi/pi-coding-agent/web/search/providers/yahoo"; -import { SearchProviderError } from "@oh-my-pi/pi-coding-agent/web/search/types"; - -const fakeAuthStorage = { - async getApiKey() { - throw new Error("Yahoo search must not request API keys"); - }, - resolver() { - throw new Error("Yahoo search must not request credential resolvers"); - }, - hasAuth() { - throw new Error("Yahoo search must not check auth"); - }, -} as unknown as AuthStorage; - -function makeParams(query: string, fetch: FetchImpl): SearchParams { - return { - query, - authStorage: fakeAuthStorage, - systemPrompt: "Yahoo search test prompt", - fetch, - }; -} - -/** Current Yahoo layout: tracker `` wraps a breadcrumb div plus the `

    `. */ -function wrappedResult(target: string, title: string, snippet?: string): string { - const ru = encodeURIComponent(target); - return `
  • `; -} - -function serp(body: string): string { - return `

    Search Results

    -
      ${body}
    `; -} - -describe("Yahoo web search provider", () => { - it("requests the HTML result page with browser navigation headers, count, and recency", async () => { - let capturedUrl = ""; - let capturedInit: RequestInit | undefined; - const fetchMock: FetchImpl = (input, init) => { - capturedUrl = typeof input === "string" ? input : input.toString(); - capturedInit = init; - return Promise.resolve( - new Response(serp(wrappedResult("https://example.com/result", "Result", "Search snippet")), { - status: 200, - headers: { "Content-Type": "text/html" }, - }), - ); - }; - - const response = await searchYahoo({ - ...makeParams("browser headers & parsing", fetchMock), - numSearchResults: 99, - recency: "week", - }); - - const url = new URL(capturedUrl); - expect(url.origin + url.pathname).toBe("https://search.yahoo.com/search"); - expect(url.searchParams.get("p")).toBe("browser headers & parsing"); - expect(url.searchParams.get("n")).toBe("20"); - expect(url.searchParams.get("btf")).toBe("w"); - expect(capturedInit?.method).toBeUndefined(); - const headers = new Headers(capturedInit?.headers); - expect(headers.get("accept")).toContain("text/html"); - expect(headers.get("user-agent")).toMatch(/Chrome\/\d+\.0\.0\.0/); - expect(headers.get("referer")).toBe("https://search.yahoo.com/"); - expect(headers.get("sec-fetch-dest")).toBe("document"); - expect(headers.get("sec-fetch-mode")).toBe("navigate"); - expect(headers.get("sec-fetch-site")).toBe("same-origin"); - expect(response.sources).toEqual([ - { title: "Result", url: "https://example.com/result", snippet: "Search snippet" }, - ]); - }); - - it("maps day and month recency to btf and silently drops the unsupported year filter", async () => { - const captured: string[] = []; - const fetchMock: FetchImpl = input => { - captured.push(typeof input === "string" ? input : input.toString()); - return Promise.resolve(new Response(serp(""), { status: 200 })); - }; - - await searchYahoo({ ...makeParams("q", fetchMock), recency: "day" }); - await searchYahoo({ ...makeParams("q", fetchMock), recency: "month" }); - await searchYahoo({ ...makeParams("q", fetchMock), recency: "year" }); - await searchYahoo(makeParams("q", fetchMock)); - - expect(new URL(captured[0]).searchParams.get("btf")).toBe("d"); - expect(new URL(captured[1]).searchParams.get("btf")).toBe("m"); - expect(new URL(captured[2]).searchParams.get("btf")).toBeNull(); - expect(new URL(captured[2]).searchParams.get("p")).toBe("q"); - expect(new URL(captured[3]).searchParams.get("btf")).toBeNull(); - }); - - it("unwraps /RU= tracker links, handles legacy plain hrefs, deduplicates, and skips junk rows", async () => { - const target = "https://example.com/page?a=1&b=2"; - const html = serp( - [ - wrappedResult( - "https://bun.sh/", - "Bun — A fast all-in-one JavaScript runtime", - 'Jan 3, 2010 · Bundle, install, and run JavaScript & TypeScript.', - ), - // Legacy layout: anchor nested inside the h3, plain unwrapped href. - `
  • Legacy snippet

  • `, - // Same target again: must deduplicate. - wrappedResult(target, "Duplicate target", "duplicate"), - // Tracker link without a recoverable /RU= segment: skipped. - `
  • `, - // Internal navigation resolves to search.yahoo.com: skipped. - `
  • `, - // Module header h3 outside any .algo block: never considered. - `
  • Videos

  • `, - ].join("\n"), - ); - const fetchMock: FetchImpl = () => Promise.resolve(new Response(html, { status: 200 })); - - const response = await searchYahoo({ ...makeParams("mixed markup", fetchMock), numSearchResults: 10 }); - - expect(response.provider).toBe("yahoo"); - expect(response.sources).toEqual([ - { - title: "Bun — A fast all-in-one JavaScript runtime", - url: "https://bun.sh/", - snippet: "Jan 3, 2010 · Bundle, install, and run JavaScript & TypeScript.", - }, - { - title: "Legacy result", - url: target, - snippet: "Legacy snippet", - }, - ]); - }); - - it("clamps the parsed results to the requested count", async () => { - const html = serp( - Array.from({ length: 5 }, (_, i) => - wrappedResult(`https://example.com/${i}`, `Result ${i}`, `snippet ${i}`), - ).join("\n"), - ); - const fetchMock: FetchImpl = () => Promise.resolve(new Response(html, { status: 200 })); - - const response = await searchYahoo({ ...makeParams("clamp", fetchMock), numSearchResults: 2 }); - - expect(response.sources.map(s => s.url)).toEqual(["https://example.com/0", "https://example.com/1"]); - }); - - it("surfaces the GDPR consent interstitial as a provider-tagged 429", async () => { - const consent = ``; - const fetchMock: FetchImpl = () => Promise.resolve(new Response(consent, { status: 200 })); - - try { - await searchYahoo(makeParams("blocked", fetchMock)); - expect.unreachable("Yahoo consent interstitial should reject"); - } catch (error) { - expect(error).toBeInstanceOf(SearchProviderError); - expect(error).toMatchObject({ provider: "yahoo", status: 429 }); - expect((error as SearchProviderError).message).toContain("consent"); - } - }); - - it("maps non-OK HTTP responses to a provider-tagged error with the upstream status", async () => { - const fetchMock: FetchImpl = () => Promise.resolve(new Response("upstream broke", { status: 503 })); - - try { - await searchYahoo(makeParams("unavailable", fetchMock)); - expect.unreachable("HTTP 503 should reject"); - } catch (error) { - expect(error).toBeInstanceOf(SearchProviderError); - expect(error).toMatchObject({ - provider: "yahoo", - status: 503, - message: "Yahoo HTML error (503)", - }); - } - }); -}); diff --git a/python/robomp/tests/test_host_tools.py b/python/robomp/tests/test_host_tools.py index 9d4ab2b1e..8c3b69cd1 100644 --- a/python/robomp/tests/test_host_tools.py +++ b/python/robomp/tests/test_host_tools.py @@ -2303,7 +2303,8 @@ def test_gh_push_branch_runs_fix_and_check_before_pushing( # Both gates ran, and fix preceded check (both have one call recorded). assert fix_calls.read_text() == "called" assert check_calls.read_text() == "called" - # The formatter's diff was committed by the bot as a `style: bun run fix` commit. + # The formatter's diff was amended into the agent's HEAD commit — no + # standalone `style:` commit; subject and author are retained. log = subprocess.run( ["git", "-C", str(ws.repo_dir), "log", "--format=%an <%ae> %s", "-n", "2"], capture_output=True, @@ -2311,7 +2312,17 @@ def test_gh_push_branch_runs_fix_and_check_before_pushing( check=True, ) lines = log.stdout.strip().splitlines() - assert lines[0].startswith("robomp-bot style: bun run fix"), lines + assert lines[0] == "robomp-bot feat: follow-up", lines + assert lines[1] == "robomp-bot init", lines + assert (ws.repo_dir / "src.txt").read_text() == "formatted\n" + # HEAD's tree contains the formatter output (not just the worktree). + show = subprocess.run( + ["git", "-C", str(ws.repo_dir), "show", "HEAD:src.txt"], + capture_output=True, + text=True, + check=True, + ) + assert show.stdout == "formatted\n" # And the branch ended up on the remote at the new head. assert result.startswith(f"pushed {ws.branch} ") refs = subprocess.run( @@ -2920,10 +2931,10 @@ def test_gh_push_branch_skip_checks_still_refuses_dirty_worktree( assert not any(r.startswith("refs/heads/farm/") for r in refs.stdout.splitlines()), refs.stdout -def test_gh_open_pr_runs_fix_then_check_and_commits_fixup( +def test_gh_open_pr_runs_fix_then_check_and_amends_formatter_diff( db: Database, tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: - """gh_open_pr runs `bun run fix`, commits any diff as the bot, then runs `bun check`.""" + """gh_open_pr runs `bun run fix`, amends any diff into HEAD, then runs `bun check`.""" import os import subprocess @@ -3078,7 +3089,8 @@ def test_gh_open_pr_runs_fix_then_check_and_commits_fixup( # Both bun stages ran, and fix preceded check. assert fix_calls.read_text() == "called" assert check_calls.read_text() == "called" - # The formatter diff was committed by the bot as a "style:" commit. + # The formatter diff was amended into HEAD — subject and author retained, + # no standalone "style:" commit, and HEAD's tree holds the formatted file. log = subprocess.run( ["git", "-C", str(ws.repo_dir), "log", "--format=%an|%ae|%s", "-2"], capture_output=True, @@ -3086,8 +3098,15 @@ def test_gh_open_pr_runs_fix_then_check_and_commits_fixup( check=True, ) lines = log.stdout.strip().splitlines() - assert lines[0] == "robomp-bot|robomp-bot@example.invalid|style: bun run fix" - assert lines[1].endswith("|feat: initial change") + assert lines[0] == "robomp-bot|robomp-bot@example.invalid|feat: initial change" + assert lines[1].endswith("|init") + show = subprocess.run( + ["git", "-C", str(ws.repo_dir), "show", "HEAD:src.txt"], + capture_output=True, + text=True, + check=True, + ) + assert show.stdout == "formatted\n" # Worktree is clean again (gate before push would have rejected otherwise). status = subprocess.run( ["git", "-C", str(ws.repo_dir), "status", "--porcelain"], @@ -3113,8 +3132,8 @@ def test_gh_open_pr_refuses_dirty_worktree_before_fix( ) -> None: """A pre-existing uncommitted edit MUST cause gh_open_pr (and gh_push_branch) to refuse BEFORE `bun run fix` runs — otherwise `git add -A` after fix - would silently fold the unrelated edit into the `style: bun run fix` - commit and ship it in the PR.""" + would silently amend the unrelated edit into the agent's HEAD commit + and ship it in the PR.""" import os import subprocess @@ -3272,14 +3291,6 @@ def test_gh_open_pr_refuses_dirty_worktree_before_fix( check=True, ) assert "src.txt" in status.stdout - # No commit named "style: bun run fix" exists. - log = subprocess.run( - ["git", "-C", str(ws.repo_dir), "log", "--format=%s"], - capture_output=True, - text=True, - check=True, - ) - assert "style: bun run fix" not in log.stdout # Origin's farm/* branch was never created — push refused before reaching the network. refs = subprocess.run( ["git", "-C", str(bare), "for-each-ref", "--format=%(refname)"], diff --git a/scripts/ci-release-build-binaries.ts b/scripts/ci-release-build-binaries.ts index 79ff61b7e..8ee6a8ec6 100644 --- a/scripts/ci-release-build-binaries.ts +++ b/scripts/ci-release-build-binaries.ts @@ -163,6 +163,17 @@ async function resetArtifacts(): Promise { await runCommand(["bun", "run", "gen:stats:reset"], repoRoot); await runCommand(["bun", "run", "gen:mupdf:reset"], repoRoot); } + +async function main(): Promise { + const requestedTargets = parseRequestedTargets(); + const selectedTargets = requestedTargets ? targets.filter(target => requestedTargets.has(target.id)) : targets; + + if (requestedTargets) { + const unknownTargets = [...requestedTargets].filter( + requestedTarget => !targets.some(target => target.id === requestedTarget), + ); + if (unknownTargets.length > 0) { + throw new Error(`Unknown release target(s): ${unknownTargets.join(", ")}`); } }