Files
OmniRoute/tests/unit/executor-web-cookie-sweep.test.ts
Diego Rodrigues de Sa e Souza 7d57d9f4a1 fix(providers): retire common ChatGPT Web provider (#11754)
Rebased onto the current release/v3.8.51 tip as part of a combined provider-retirement/provenance merge batch (Designer Web, Felo Web, Runtime, GPL-derived removal, Qwen Web already landed). Large conflict set (this is the biggest PR in the batch — the common ChatGPT Web provider touches chat, images, count-tokens, session leases, and combos). Conflicts resolved:

- `open-sse/config/providers/registry/chatgpt-web/*`, `open-sse/executors/chatgpt-web*`, `open-sse/handlers/imageGeneration/providers/chatgptWeb.ts`, and their tests: kept deleted, matching the PR's stated scope.
- `open-sse/config/providers/registry/minimax/web/index.ts`, `open-sse/handlers/imageGeneration/providers/geminiWeb.ts`, `open-sse/executors/gemini-web.ts`'s stale image-mode branch: base-drift collisions against already-merged sibling retirements (#11691, #11708) — kept deleted / dropped the dead code, since this PR's own branch forked before those merged.
- `src/shared/constants/reservedProviderPrefixes.ts`, `open-sse/executors/index.ts`, `executorProxy.ts`, `virtualFactory.ts`, `autoStrategy.ts`, `src/lib/db/providers.ts`, `src/sse/handlers/chat.ts`: combined the Designer + Runtime (Felo/Qwen) + common-ChatGPT-Web retirement guard calls at each shared chokepoint — compute-once-then-OR pattern, consistent with prior combinations in this batch.
- `src/sse/services/model.ts` / `src/sse/handlers/chatHelpers.ts`: adopted this PR's new `getModelInfoOrRetirementResponse()` central wrapper (a real improvement over ad-hoc try/catch), and extended it to also catch the Designer + Runtime retirement errors it didn't originally cover, so the consolidation doesn't regress the other two mechanisms.
- `src/app/api/v1/images/edits/route.ts`: this PR moved the retirement check earlier (before `enforceApiKeyPolicy`) but left the old later call+catch block in place from base drift — removed the now-redundant duplicate `resolveImageRouteModel()` call and merged the Designer catch into the earlier one.
- `open-sse/config/imageRegistry.ts`, `tests/snapshots/executors/executor-map.json` (`keyCount` recomputed to 133), `tests/snapshots/provider/translate-path.json`: same "both sides inserted a different retired provider at the same slot" pattern — resolved by dropping both.
- `tests/unit/chatcore-executor-proxy.test.ts`, `provider-node-reserved-prefix.test.ts`, `combo-auto-candidate-expansion.test.ts`, `messages-count-tokens-route.test.ts`, `virtual-auto-combo.test.ts`: split into independent per-mechanism test blocks (established pattern); `virtual-auto-combo.test.ts`'s old "includes cookie web-session providers" positive-inclusion test (which used chatgpt-web as its example) was retired along with the provider and replaced by this PR's negative-exclusion test for the same slot.
- `docs/architecture/ARCHITECTURE.md`, `CODEBASE_DOCUMENTATION.md` (+ 4 i18n mirrors), `README.md`, `FREE-TIERS-GUIDE.md`, `docs/diagrams/free-tier-budget.svg`, `docs/screenshots/free-tier-budget-card.svg`, `docs/reference/PROVIDER_REFERENCE.md`: recomputed every stale count from the real merged state — 104 executors (`countFiles` gate logic), 351 providers (regenerated via `gen:provider-reference`), 152/351 `hasFree` entries, 445/438/7 free-tier catalog rows, 13 ToS-avoid providers, budget-card regenerated via its real generator script. One doc conflict (`oauth/` module list) needed picking HEAD's side specifically — theirs still listed the already-removed `raycast` module instead of the real `openference`.
- `config/quality/test-masking-allowlist.json`: additive merge of the PR's 17 `_deletedWithReplacement` entries alongside the batch's existing ones (one real duplicate-key mistake in my first pass, caught and fixed via a `object_pairs_hook` duplicate-key check before finalizing).

Also fixed two real, unrelated-to-my-merge issues surfaced by the focused suite:
- `tests/unit/resolve-web-provider-host.test.ts`: the PR's own test had a typo — it asserted `perplexity-web`'s resolved host as `"perplexity.ai"`, but the provider's registered `website` is `"https://www.perplexity.ai"` and the resolver returns the URL's `host` verbatim (no www-stripping), so the correct value is `"www.perplexity.ai"` (consistent with the same test's own `url` assertion).
- `tests/unit/hard-session-lease-bypass-inventory.test.ts`: this golden call-site inventory was already stale on the pristine post-#11713 tip (confirmed via a throwaway probe worktree) — `src/lib/db/providers.ts`'s 3 connection-fallback sites and a third `src/app/api/providers/route.ts` site were never added to the golden list by the earlier-merged #11698/#11720 PRs. Updated it to the real current inventory (dated inline comments explain each delta and which PR introduced it), plus this PR's own legitimate deltas (image-edits duplicate-call removal, `ChatGptWebExecutor.execute()` site removed).

Focused suite green (433/433 across executor-proxy, reserved-prefix, hard-session-lease-bypass-inventory, resolve-web-provider-host, retirement/runtime-block/source-retirement/management-retirement/image-handler-retirement, migration-168, combo-auto-candidate-expansion, virtual-auto-combo, executor-map-golden and siblings), plus `typecheck:core`, `check-file-size`, and `check-changelog-integrity` clean. Thanks for the thorough provenance-hold retirement work — appreciated.
2026-08-28 06:52:46 -03:00

201 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Web-Cookie + NoAuth executor wrapper contract sweep.
*
* Why this file exists
* --------------------
* `open-sse/handlers/chatCore.ts` reads `res.response.status`, `res.response.headers`,
* and uses `res.url` / `res.transformedBody` to classify upstream responses
* (see chatCore.ts:39374486, BaseExecutor.execute() at base.ts:1146).
*
* An executor that returns a raw `Response` instead of the wrapper shape
* `{response, url, headers, transformedBody}` causes `res.response.status` to
* throw `Cannot read properties of undefined (reading 'status')`. That JS
* TypeError was then surfaced as a 502 via `formatProviderError` in
* `open-sse/utils/error.ts:496`, and showed up to the client as
* `[502]: Cannot read properties of undefined (reading 'status')`.
*
* The duckduckgo-web executor was the first known case. To prevent any
* future executor from regressing on the same contract, this sweep test
* imports every executor in `WEB_COOKIE_PROVIDERS` + `NOAUTH_PROVIDERS`
* (26 web-cookie + 2 noauth = 28 total), calls `execute()` with a minimal
* but valid input, and asserts the wrapper shape. Tests use the
* pre-aborted signal path or empty-creds path so no real upstream call
* is needed.
*
* If this file ever flags a missing executor, the fix is in the executor
* — the contract is the executor's responsibility.
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { getExecutor } from "../../open-sse/executors/index.ts";
import { WEB_COOKIE_PROVIDERS, NOAUTH_PROVIDERS } from "../../src/shared/constants/providers.ts";
type WebCookieId = keyof typeof WEB_COOKIE_PROVIDERS;
type NoauthId = keyof typeof NOAUTH_PROVIDERS;
const WEB_COOKIE_IDS = Object.keys(WEB_COOKIE_PROVIDERS) as WebCookieId[];
const NOAUTH_IDS = Object.keys(NOAUTH_PROVIDERS) as NoauthId[];
/**
* Per-provider fake-credential strings that pass the executor's own
* input-validation gate without making a real upstream call succeed.
* Each executor parses a different cookie/header — the goal is only
* to short-circuit the network call with a synthetic 401/403/4xx/5xx,
* not to actually authenticate.
*/
const FAKE_CREDS: Record<string, string> = {
"grok-web": "sso=fake-audit-sweep",
"gemini-web": "__Secure-1PSID=fake-audit-sweep",
"perplexity-web": "__Secure-next-auth.session-token=fake-audit-sweep",
"blackbox-web": "__Secure-authjs.session-token=fake-audit-sweep",
"muse-spark-web": "ecto_1_sess=fake-audit-sweep",
"claude-web": "sessionKey=fake-audit-sweep",
"deepseek-web": "userToken=fake-audit-sweep",
"copilot-web": "fake-audit-sweep",
"t3-web": "fake-audit-sweep",
"inner-ai": "fake-audit-sweep user@example.com",
"adapta-web": "__client=fake-audit-sweep",
huggingchat: "hf-chat=fake-audit-sweep",
"poe-web": "p-b=fake-audit-sweep",
"venice-web": "fake-audit-sweep",
"v0-vercel-web": "fake-audit-sweep",
"kimi-web": "fake-audit-sweep",
"doubao-web": "sessionid=fake-audit-sweep; ttwid=fake-audit-sweep; s_v_web_id=verify_fake",
"duckduckgo-web": "",
"veoaifree-web": "",
};
const VALID_BODY = {
model: "test",
messages: [{ role: "user", content: "ping" }],
};
/**
* Asserts that `result` has the executor wrapper contract shape:
* { response: Response, url: string, headers: object, transformedBody: unknown }
*
* The contract is what `open-sse/handlers/chatCore.ts` and
* `BaseExecutor.execute()` (open-sse/executors/base.ts:1146) depend on.
*/
function assertExecutorWrapperShape(
result: unknown,
provider: string
): asserts result is {
response: Response;
url: string;
headers: Record<string, unknown>;
transformedBody: unknown;
} {
assert.ok(
result && typeof result === "object",
`[${provider}] execute() must return an object, not ${typeof result}`
);
const r = result as Record<string, unknown>;
assert.ok(
r.response instanceof Response,
`[${provider}] result.response must be a Response (got ${typeof r.response})`
);
assert.equal(typeof r.url, "string", `[${provider}] result.url must be a string`);
assert.ok(
r.headers && typeof r.headers === "object",
`[${provider}] result.headers must be an object`
);
// transformedBody may be null/undefined/object; just check it doesn't
// throw when accessed.
void r.transformedBody;
// Critical: r.response.status must be reachable without throwing
// — this is the exact property read that the duckduckgo-web bug
// (#3106) crashed on.
const status = (r.response as Response).status;
assert.ok(
Number.isInteger(status) && status >= 100 && status < 600,
`[${provider}] result.response.status must be a valid HTTP status, got ${status}`
);
}
describe("web-cookie + noauth executor wrapper contract sweep", () => {
describe("WEB_COOKIE_PROVIDERS (26)", () => {
for (const providerId of WEB_COOKIE_IDS) {
it(`${providerId} executor returns wrapper shape`, async () => {
const executor = await getExecutor(providerId);
assert.ok(executor, `[${providerId}] getExecutor must return an executor`);
const result = await executor.execute({
model: providerId,
body: VALID_BODY,
stream: false,
credentials: { apiKey: FAKE_CREDS[providerId] ?? "fake" },
signal: null,
} as never);
assertExecutorWrapperShape(result, providerId);
// Result should never be a JS TypeError. Real executor returns
// a proper Response with a JSON error body for invalid creds.
// If a regression introduces a raw Response return, the shape
// assertion above will fail.
const body = await result.response.text();
// Most executors return JSON error bodies for invalid creds.
// We don't require JSON, but we DO require the body to be a
// non-empty string (not the literal "[object Response]" or
// a TypeError stack trace).
assert.ok(body.length > 0, `[${providerId}] response body must be non-empty`);
// And it must NOT be the duckduckgo-web regression signature.
assert.doesNotMatch(
body,
/Cannot read properties of undefined \(reading 'status'\)/,
`[${providerId}] must not surface the chatCore-side TypeError`
);
});
}
});
describe("NOAUTH_PROVIDERS (4 total; 2 require cookie='') ", () => {
// Only noauth providers that should be probed without creds:
// duckduckgo-web and veoaifree-web. opencode/notice have dedicated
// executor tests already (executor-opencode.test.ts / executor-notice.test.ts).
const TARGETS = NOAUTH_IDS.filter((id) => id === "duckduckgo-web" || id === "veoaifree-web");
for (const providerId of TARGETS) {
it(`${providerId} noauth executor returns wrapper shape`, async () => {
const executor = await getExecutor(providerId);
assert.ok(executor, `[${providerId}] getExecutor must return an executor`);
// Use a pre-aborted signal so the executor short-circuits via
// its AbortError path before any real network call.
const controller = new AbortController();
controller.abort();
const result = await executor.execute({
model: providerId,
body: VALID_BODY,
stream: false,
credentials: { apiKey: "" },
signal: controller.signal,
} as never);
// duckduckgo-web may legitimately short-circuit with a bare
// 499 Response on a pre-aborted signal; chatCore's
// normalizeExecutorResult already accepts both shapes. Only
// insist on the full wrapper for executors that are expected
// to produce one.
if (result instanceof Response) {
assert.ok(
result.status >= 100 && result.status < 600,
`[${providerId}] bare Response must have a valid HTTP status, got ${result.status}`
);
} else {
assertExecutorWrapperShape(result, providerId);
}
const body = await (result instanceof Response ? result : result.response).text();
assert.ok(body.length > 0, `[${providerId}] response body must be non-empty`);
assert.doesNotMatch(
body,
/Cannot read properties of undefined \(reading 'status'\)/,
`[${providerId}] must not surface the chatCore-side TypeError`
);
});
}
});
});