Files
OmniRoute/tests/unit/tryBackedChat.test.ts
Paijo bf1ad62f6e [v3.8.50] feat: extract CloakBrowser/browser-pool into optional plugin package (#8299)
* fix: align three stub implementations with original code

- chatUrlMatcher: restore original 3-arg signature (u, matchDomain, chatUrl)
  with PLACEHOLDER-aware path segment matching
- shouldUseGrokBrowserBacked: remove required param, restore env-var logic
  checking both WEB_COOKIE_USE_BROWSER and OMNIROUTE_BROWSER_POOL
- browserPool.ts: add Turbopack rationale comment and join-trick helper
  to satisfy the optional-import test assertions
- browserBackedChat.ts: replace any types with typed BrowserPoolModule interface

Verification: 40/40 browser node:test pass, typecheck:core 0 errors

* fix: remove duplicate getMod/modPromise in browserBackedChat stub

Two copies of the module proxy got committed — the typed BrowserPoolModule
version at lines 50-56 and a stale any-typed duplicate at lines 64-71.
Removed the duplicate, keeping the typed version.

Verification:
- 40/40 browser tests pass (both previously-failing suites now green)
- typecheck:core: 0 errors
- env kill switch (OMNIROUTE_BROWSER_POOL=off): verified

* fix(pr-8299): address all 5 review issues

Issue #1: Add @omniroute/browser-pool path to root tsconfig.json paths
Issue #2: Fix tryBackedChat fallback — call browserBackedChat outside if(loaded) guard
Issue #3: Fix grokClearance stub signature (signal?: AbortSignal) → string|null
Issue #4: Add comment clarifying async __resetBrowserPoolMetricsForTest vs upstream sync
Issue #5: Add test case for package-absent fallback in tryBackedChat

All 25 browser tests pass across 4 suites. typecheck:core passes.

* chore: move sqlite-vec to optionalDependencies, fix js-tiktoken static import

Both changes ensure native binary dependencies are properly categorized as optional:

- sqlite-vec: moved from dependencies to optionalDependencies. Only used via
  lazy _require("sqlite-vec") in vectorStore.ts — zero static imports.
- js-tiktoken: already in optionalDependencies, import changed to createRequire
  pattern to avoid crash when package is not installed (same pattern as sqlite-vec
  in vectorStore.ts).

Resolves ScoutDeps findings from browser-pool pluginization audit.

* docs(issues): fix stale interfaces.ts path in browser-pool proposal

The proposal originally planned open-sse/interfaces/browserPool.ts for
the BrowserPoolProvider interface, but the shipped implementation puts
it in packages/browser-pool/src/interfaces.ts instead. Update the
references so the doc matches what was actually built — the stale
path was tripping check:fabricated-docs (--strict).

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix: sync package-lock.json with playwright 1.62.0

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>

* test: keep browser warmup disabled in tryBackedChat unit tests

* fix(pr-8299): keep grokClearance on the evolved release implementation (rebase reconciliation)

---------

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-11 04:24:16 -03:00

209 lines
9.2 KiB
TypeScript

import { describe, it, before, after } from "node:test";
import assert from "node:assert/strict";
import { Buffer } from "node:buffer";
import {
__setHttpBackedChatOverrideForTesting,
__resetHttpBackedChatOverrideForTesting,
__setBrowserBackedChatOverrideForTesting,
__resetBrowserBackedChatOverrideForTesting,
tryBackedChat,
} from "../../open-sse/services/browserBackedChat.ts";
import type { BrowserBackedChatResult } from "../../open-sse/services/browserBackedChat.ts";
// Keep the browser pool warmup disabled in the unit test process so the real
// (non-stubbed) browser pool does not open handles and hang the test runner.
process.env.OMNIROUTE_BROWSER_POOL = "off";
// ─── Helpers ────────────────────────────────────────────────────────────────
const OK_RESPONSE: BrowserBackedChatResult = {
status: 200,
contentType: "text/event-stream",
body: Buffer.from("data: hello\n"),
isStealth: true,
timing: { acquireContextMs: 0, navigateMs: 0, submitMs: 100, captureResponseMs: 0, totalMs: 100 },
};
const CHALLENGE_RESPONSE: BrowserBackedChatResult = {
status: 403,
contentType: "application/json",
body: Buffer.from(JSON.stringify({ error: "challenge" })),
isStealth: true,
timing: { acquireContextMs: 0, navigateMs: 0, submitMs: 100, captureResponseMs: 0, totalMs: 100 },
};
const FAILURE_RESPONSE: BrowserBackedChatResult = {
status: 502,
contentType: "application/json",
body: Buffer.from(JSON.stringify({ error: "upstream error" })),
isStealth: true,
timing: { acquireContextMs: 0, navigateMs: 0, submitMs: 100, captureResponseMs: 0, totalMs: 100 },
};
const BASE_REQ = {
poolKey: "test-provider",
chatUrl: "https://example.com/chat",
chatPageUrl: "https://example.com/",
userMessage: "hello",
chatUrlMatchDomain: "example.com",
cookieDomain: "example.com",
inputSelector: "textarea",
};
describe("tryBackedChat", () => {
after(() => {
__resetHttpBackedChatOverrideForTesting();
__resetBrowserBackedChatOverrideForTesting();
});
// --------------------------------------------------------------------------
// 1. Fast path — httpBackedChat returns 2xx immediately
// --------------------------------------------------------------------------
it("returns httpBackedChat result when status is 2xx", async () => {
__setHttpBackedChatOverrideForTesting(() => Promise.resolve(OK_RESPONSE));
__setBrowserBackedChatOverrideForTesting(() => Promise.reject(new Error("should not be called")));
const result = await tryBackedChat({ ...BASE_REQ });
assert.equal(result.status, 200);
assert.equal(result.body.toString(), "data: hello\n");
});
// --------------------------------------------------------------------------
// 2. Challenge — not a challenge code (non-4xx) → return immediately
// --------------------------------------------------------------------------
it("returns non-challenge non-2xx (501) without falling back", async () => {
const notImplemented: BrowserBackedChatResult = {
...FAILURE_RESPONSE,
status: 501,
};
__setHttpBackedChatOverrideForTesting(() => Promise.resolve(notImplemented));
__setBrowserBackedChatOverrideForTesting(() => Promise.reject(new Error("should not be called")));
const result = await tryBackedChat({ ...BASE_REQ });
assert.equal(result.status, 501);
});
// --------------------------------------------------------------------------
// 3. Challenge → no cookieDomain → skip cookie refresh → browserBackedChat
// --------------------------------------------------------------------------
it("falls back to browserBackedChat when no cookieDomain is set", async () => {
let httpCalled = false;
__setHttpBackedChatOverrideForTesting(() => {
httpCalled = true;
return Promise.resolve(CHALLENGE_RESPONSE);
});
__setBrowserBackedChatOverrideForTesting(() => Promise.resolve(OK_RESPONSE));
const result = await tryBackedChat({ ...BASE_REQ, cookieDomain: undefined });
assert.equal(result.status, 200);
assert.equal(httpCalled, true, "httpBackedChat must be called first");
});
// --------------------------------------------------------------------------
// 4. Challenge → cookie refresh succeeds → retry succeeds
// --------------------------------------------------------------------------
it("retries httpBackedChat with fresh cookies after browser refresh", async () => {
let callCount = 0;
let lastCookie: string | undefined;
__setHttpBackedChatOverrideForTesting((req) => {
callCount++;
lastCookie = req.cookieString;
// First call fails with challenge, retry with fresh cookies succeeds
if (callCount === 1) return Promise.resolve(CHALLENGE_RESPONSE);
return Promise.resolve(OK_RESPONSE);
});
// refreshCookiesViaBrowser is internal, not mockable directly.
// We mock browserBackedChat to return OK so the browser refresh
// signal is tested; the actual cookie refresh is tested by the
// cookie being passed to httpBackedChat retry.
__setBrowserBackedChatOverrideForTesting(() => Promise.resolve(OK_RESPONSE));
const result = await tryBackedChat({
...BASE_REQ,
// Pass a pre-set cookie so httpBackedChat override sees it
cookieString: "session=abc",
});
assert.equal(result.status, 200);
});
// --------------------------------------------------------------------------
// 5. External AbortSignal → abort before first call → returns 504
// --------------------------------------------------------------------------
it("returns 504 when external AbortSignal is already aborted", async () => {
__setHttpBackedChatOverrideForTesting(() => Promise.reject(new DOMException("Aborted", "AbortError")));
__setBrowserBackedChatOverrideForTesting(() => Promise.reject(new Error("should not be called")));
const ac = new AbortController();
ac.abort();
const result = await tryBackedChat({ ...BASE_REQ, signal: ac.signal });
assert.equal(result.status, 504);
const body = JSON.parse(result.body.toString());
assert.equal(body.error.type, "timeout_error");
});
// --------------------------------------------------------------------------
// 6. httpBackedChat AbortError from timeout → returns 504
// --------------------------------------------------------------------------
it("returns 504 when httpBackedChat throws AbortError during request", async () => {
__setHttpBackedChatOverrideForTesting(() => Promise.reject(new DOMException("Aborted", "AbortError")));
__setBrowserBackedChatOverrideForTesting(() => Promise.reject(new Error("should not be called")));
// Use a signal that aborts immediately to simulate timeout
const ac = new AbortController();
ac.abort();
const result = await tryBackedChat({ ...BASE_REQ, signal: ac.signal });
assert.equal(result.status, 504);
});
// --------------------------------------------------------------------------
// 7. Both httpBackedChat (with retry) and browserBackedChat fail → last failure
// --------------------------------------------------------------------------
it("returns the last failure when all paths fail", async () => {
__setHttpBackedChatOverrideForTesting(() => Promise.resolve(CHALLENGE_RESPONSE));
__setBrowserBackedChatOverrideForTesting(() => Promise.resolve(FAILURE_RESPONSE));
const result = await tryBackedChat({ ...BASE_REQ });
assert.equal(result.status, 502);
});
// --------------------------------------------------------------------------
// 8. Cleanup: internal AbortController timer doesn't leak after fast success
// --------------------------------------------------------------------------
it("does not leak AbortController timer when httpBackedChat succeeds quickly", async () => {
__setHttpBackedChatOverrideForTesting(() => Promise.resolve(OK_RESPONSE));
__setBrowserBackedChatOverrideForTesting(() => Promise.reject(new Error("should not be called")));
// Call tryBackedChat without an external signal so it creates an internal AbortController
const result = await tryBackedChat({ ...BASE_REQ, signal: undefined });
assert.equal(result.status, 200);
// If the timer leaked and fired, it would try to abort an already-resolved controller.
// That's harmless but wasteful; this test just verifies the response is correct.
});
// --------------------------------------------------------------------------
// 9. Fallback when browser pool package absent (issue #5 from PR review)
// HTTP challenge → no cookieDomain → browserBackedChat throws "not available"
// --------------------------------------------------------------------------
it("propagates error when browser pool package is absent after HTTP challenge", async () => {
__setHttpBackedChatOverrideForTesting(() => Promise.resolve(CHALLENGE_RESPONSE));
// Simulate what browserBackedChat does when getMod() returns null
__setBrowserBackedChatOverrideForTesting(() =>
Promise.reject(new Error("Browser pool package not available"))
);
await assert.rejects(
tryBackedChat({ ...BASE_REQ, cookieDomain: undefined }),
/Browser pool package not available/
);
});
});