Files
OmniRoute/tests/unit/tryBackedChat.test.ts
Praveen K Palaniswamy 65e81158ab fix(ollama): route models by advertised capability (#11088)
Landed with the design call resolved per the owner's pick — **option 1**: the synced store is now endpoint-agnostic (persistDiscoveredModels and managedModelImport no longer drop non-chat models at write time), and chat selectability moved to read time (auto-pool expansion in autoStrategy applies filterChatSelectableModels; the models-route projection already had its chatOnly filter). Your discovery test now passes end-to-end (3/3): /api/show capabilities persist per connection and image/embedding requests route through the advertising host.

Reconciliation notes: conflicted areas merged onto the current tip (adobe discovery import, requestedModel preflight signature, resolvedProvider fast-path coexists with the synced-route override — explicit resolution wins); carried base-red drains (#10055 memoization, #11071 test variants) dropped as already-landed; the managed-model-import exclusion test was propagated to the new contract (image/video models persist; the read filter still hides them from chat pickers — pinned by a new assertion). Full battery: 205/206 focused (the one red is a confirmed periodic-timer timing flake on the loaded devbox — 20/20 isolated), autoCombo vitest 30/30, combo suites 46/46, gates + typecheck clean.

Thank you @yourspraveen — the capability probe + routing design was right; it just needed the store contract opened up. Fixes #11087.
2026-08-23 11:45:01 -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/
);
});
});