Files
OmniRoute/tests/unit/duckduckgo-challenge-split.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

117 lines
5.5 KiB
TypeScript

import { test } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import vm from "node:vm";
// Split-guard for the duckduckgo-web challenge-solver extraction.
// The anti-abuse challenge solver + FE signals live in duckduckgo-web/challenge.ts
// (pure of module state; the vm sandbox + 5s timeout are preserved). Host imports back.
const HERE = dirname(fileURLToPath(import.meta.url));
const EXE = join(HERE, "../../open-sse/executors");
const HOST = join(EXE, "duckduckgo-web.ts");
const LEAF = join(EXE, "duckduckgo-web/challenge.ts");
test("leaf hosts the solver and does not import the host", () => {
const src = readFileSync(LEAF, "utf8");
assert.match(src, /export async function solveDuckDuckGoChallenge\b/);
assert.match(src, /export function makeDuckDuckGoFeSignals\b/);
assert.match(src, /type DuckDuckGoChallengeResult\s*=/);
assert.doesNotMatch(src, /from "\.\.\/duckduckgo-web\.ts"/);
// The vm sandbox timeout must survive the move (security invariant).
assert.match(src, /timeout/);
});
test("host imports the solver back from the leaf", () => {
const host = readFileSync(HOST, "utf8");
assert.match(host, /from "\.\/duckduckgo-web\/challenge\.ts"/);
assert.doesNotMatch(host, /type DuckDuckGoChallengeResult\s*=/);
});
test("makeDuckDuckGoFeSignals returns a base64 string", async () => {
const { makeDuckDuckGoFeSignals } =
await import("../../open-sse/executors/duckduckgo-web/challenge.ts");
const out = makeDuckDuckGoFeSignals();
assert.equal(typeof out, "string");
assert.ok(out.length > 0);
});
// Regression guard: CHALLENGE_STUBS is browser-emulation source executed by
// `vm.runInContext`, which compiles in SCRIPT mode — module syntax is a hard
// SyntaxError there. A refactor once mass-added `export` to the `function`
// declarations inside this template literal (they look like ordinary top-level
// TS functions), which made every solve throw. The executor swallows solve
// failures and sends the raw unsolved challenge, so DuckDuckGo answered every
// chat request with HTTP 418 ERR_CHALLENGE while the site worked fine in a
// browser from the same IP. The three tests below fail on that class of bug.
test("CHALLENGE_STUBS contains no module syntax (vm runs it in script mode)", async () => {
const { CHALLENGE_STUBS } = await import("../../open-sse/executors/duckduckgo-web/challenge.ts");
assert.doesNotMatch(
CHALLENGE_STUBS,
/(^|[\s;{}])(export|import)[\s{*]/,
"CHALLENGE_STUBS must not use export/import — vm.runInContext compiles in script mode"
);
});
test("CHALLENGE_STUBS compiles as a script", async () => {
const { CHALLENGE_STUBS } = await import("../../open-sse/executors/duckduckgo-web/challenge.ts");
// Placeholders are substituted before execution; do the same here so the
// source is syntactically complete.
const source = CHALLENGE_STUBS.replace("__DDG_REAL_UA__", '"test-ua"').replace(
"__DDG_HTML_LOOKUP__",
"{}"
);
assert.doesNotThrow(() => new vm.Script(source), "CHALLENGE_STUBS must parse in script mode");
});
test("CHALLENGE_STUBS evaluates and defines the browser stubs the challenge probes", async () => {
const { CHALLENGE_STUBS } = await import("../../open-sse/executors/duckduckgo-web/challenge.ts");
const source = CHALLENGE_STUBS.replace("__DDG_REAL_UA__", '"test-ua"').replace(
"__DDG_HTML_LOOKUP__",
"{}"
);
const context = vm.createContext({});
vm.runInContext(source, context, { timeout: 5000 });
// A real DDG challenge reads these; if the stubs silently failed to evaluate
// they would all be undefined and the solver would produce garbage.
assert.equal(
vm.runInContext("navigator.userAgent", context),
"test-ua",
"navigator.userAgent must carry the injected UA"
);
assert.equal(vm.runInContext("typeof document.querySelector", context), "function");
assert.equal(vm.runInContext("document.getElementById('jsa').tagName", context), "IFRAME");
assert.equal(vm.runInContext("typeof getComputedStyle", context), "function");
assert.equal(vm.runInContext("window.top === window", context), true);
});
// End-to-end guard on the solver itself, using a stand-in challenge that mimics
// the real one's contract: an async IIFE returning { server_hashes, client_hashes,
// signals, meta }. This exercises the full stubs -> vm -> hash -> base64 path
// without hitting the network.
test("solveDuckDuckGoChallenge solves a representative challenge payload", async () => {
const { solveDuckDuckGoChallenge, sha256Base64 } =
await import("../../open-sse/executors/duckduckgo-web/challenge.ts");
const fakeChallenge = `(async function(){
return {
server_hashes: ["s1", "s2"],
client_hashes: [navigator.userAgent, document.getElementById('jsa').tagName],
signals: {},
meta: { v: "4", challenge_id: "test" }
};
})()`;
const ua = "Mozilla/5.0 (X11; Linux x86_64) TestAgent/1.0";
const solved = await solveDuckDuckGoChallenge(
Buffer.from(fakeChallenge, "utf8").toString("base64"),
ua
);
const decoded = JSON.parse(Buffer.from(solved, "base64").toString("utf8"));
assert.deepEqual(decoded.server_hashes, ["s1", "s2"], "server_hashes pass through untouched");
// Slot 0 is overwritten with the real UA before hashing, then every slot is sha256+base64.
assert.deepEqual(decoded.client_hashes, [sha256Base64(ua), sha256Base64("IFRAME")]);
assert.equal(decoded.meta.challenge_id, "test");
});