Files
OmniRoute/tests/unit/provider-node-reserved-prefix.test.ts
Diego Rodrigues de Sa e Souza af65171e3f fix(ci): clear the base-reds the afternoon merge batch left on release/v3.8.51 (round 5: provider count 352, TS2554/TS2677) (#12144)
* fix(ci): clear the base-reds the 2026-08-30 afternoon merge batch left on release/v3.8.51 (round 5)

- docs-counts / check-docs-counts-sync test: #12103 (Perplexity Agent) made it 352
  providers; README, AGENTS.md, llm.txt (+42 i18n mirrors), package.json description
  and the 4 README diagrams still said 351.
- api-route-typecheck: #11971 passes a third `{ featureEnabled }` argument to
  appendNoThinkingVariants() that the helper never accepted (TS2554 — and the flag
  silently did nothing); the helper now honours it. src/lib/skills/interception.ts
  narrowed a mapped object with a `Record<string, string>` predicate (TS2677) —
  predicate typed with the actual element shape.

Gates: check:docs-counts OK (test 28/28), check:docs-sync PASS, check:api-typecheck
OK (289 frozen). Refs #12103, #11971

* docs(env): document RATE_LIMIT_EXECUTION_MAX_WAIT_MS (#12027 added it to .env.example only)

* fix(ci): round 5b — freeze the react-hooks compiler-rule violations, align 7 tests to merged contracts

No new ESLint warnings: the exact CI command (lint:json --max-warnings 0) reports 278
problems on the tip — 226 from eslint-plugin-react-hooks 7 compiler rules
(set-state-in-effect 167, immutability 36, refs/static-components/purity/
preserve-manual-memoization) that were masked until the lockfile change of
dfc84ba030 invalidated the ESLint cache, plus 46 no-explicit-any in
tests/unit/call-log-cap.test.ts (#12026). Velocity phase: frozen with
`eslint --suppress-all` (+668 suppressions); the 5 now-unused
`eslint-disable react-hooks/immutability` directives and one unused import removed.
Verified: lint:json --max-warnings 0 → 0 problems.

Tests aligned to contracts merged this afternoon (all reproduced red on the pure tip):
- providers-constants-split: 235 → 236 (Perplexity Agent, #12103)
- sse-auth: a forced pin outside allowedConnections now yields no credential
  instead of silently falling back (#12080)
- with-chat-admission-10786: withInjectionGuard(postHandler, { logger: null }) (#12117)
- hard-session-lease-bypass-inventory: classify src/app/api/oauth/codex/import/route.ts (#12116)
- usage-service-hardening: OpenCode Go official usage API shape (#12124)
- i18n placeholder parity: apiManager.restrictedToConnections rewritten as a plain
  ICU plural (`{count, plural, one {# connection} other {# connections}}`) in en,
  vi, pt-BR and the 40 __MISSING__ mirrors — the parity extractor counts every
  `{word}` including the old literal `{s}`

Refs #12103, #12080, #12117, #12116, #12124, #12026

* fix(ci): run the ESLint warnings job on the box with an 8 GB heap; reserved-prefix set 398 → 400

The cold full lint with the react-hooks 7 compiler rules is killed on the 7 GB hosted
runner with no message (status null → exit 1, JSON never written) — it only looked
green while the ESLint cache was warm. tests/unit/provider-node-reserved-prefix.test.ts
aligned to the two prefixes the afternoon batch registered (#12103).

* test(ci): document the lint-guard runner exception; #9147 event-loop gap 400 → 800 ms

quality-rail-gate-membership pinned lint-guard to ubuntu-latest; the cold full lint is
OOM-killed there, so the job now runs on omni-light with an 8 GB heap — the test keeps
fast-gates pinned and asserts the documented exception. With the catalog at 352
providers the hosted shards measure 410–633 ms gaps on 9147-catalog-eventloop-yield
(3 runs); 800 ms still fails a true pin. Re-tighten with the v4.0 catalog split.

* chore(quality): summarize the ESLint report on failure — a red lint:json printed nothing

--format json --output-file swallows every problem; a red 'No new ESLint warnings' job
gave zero output (three blind debugging rounds in #12144), and a killed process (OOM,
status null) was equally silent. On any non-zero exit the runner now prints the problem
count and the first 60 'file:line rule — message' lines from the report.

* chore(lint): freeze react-hooks/immutability for the 5 UI test harnesses in the suppressions file

The rule fires for these files in CI but not locally (compiler analysis divergence),
so the inline eslint-disable directives read as 'unused directive' warnings locally.
A suppressions entry is symmetric: suppressed where the rule fires, tolerated as
unpruned (--pass-on-unpruned-suppressions) where it does not. Found via the new
lint:json failure summary.
2026-08-30 18:03:47 -03:00

387 lines
15 KiB
TypeScript

// Reserved provider prefixes — compatible-node prefix guard (TDD, tokenrouter bug).
//
// Bug: an operator-created openai-compatible node with prefix "tokenrouter" was
// accepted at creation time, but the runtime model resolver
// (src/sse/services/model.ts) treats built-in registry ids/aliases as reserved
// and skips the node lookup — so `tokenrouter/qwen/...` routed to the BUILT-IN
// tokenrouter provider ("No active credentials for provider: tokenrouter")
// instead of the operator's node. The same node addressed by its internal id
// worked fine. Fix: reject reserved prefixes at the write path (node
// create/update schemas) so the misconfiguration can no longer be created.
//
// The reserved set is shared between the runtime guard and the validation
// schemas via src/shared/constants/reservedProviderPrefixes.ts (single source of
// truth). Live set semantics mirror the old inline guard exactly; exact retired
// ids remain reserved after registry removal and use trim + lowercase matching:
// - REGISTRY entry ids + aliases, plus retired ids;
// - live ids remain case-sensitive (mixed-case "TokenRouter" does NOT collide);
// - manual alias ids that live outside REGISTRY (xiaomi/llamacpp/aq) are NOT
// included — verified they do not intercept nodes at runtime.
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-reserved-prefix-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
const providerNodesRoute = await import("../../src/app/api/provider-nodes/route.ts");
const providerNodesIdRoute = await import("../../src/app/api/provider-nodes/[id]/route.ts");
const { createProviderNodeSchema, updateProviderNodeSchema } =
await import("../../src/shared/validation/schemas.ts");
const { RESERVED_PROVIDER_PREFIXES, isReservedProviderPrefix, RESERVED_PREFIX_COUNT } =
await import("../../src/shared/constants/reservedProviderPrefixes.ts");
const { buildReservedPrefixes, getProviderPrefixIndex } =
await import("../../src/lib/providerNodePrefixes.ts");
const providerNodesDb = await import("../../src/lib/db/providers/nodes.ts");
const { isCommonChatGptWebRetiredProviderId } =
await import("../../src/shared/constants/chatgptWebRetirement.ts");
async function resetStorage() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
// Minimal response-body shapes (no `any` — new eslint violations must be fixed,
// not suppressed). `unknown` fields are narrowed through helpers before use.
type ValidationDetail = { field: string; message: string };
type ValidationBody = { error?: { details?: ValidationDetail[] } };
type NodeBody = { node?: { id?: string; prefix?: string } };
function asValidationBody(value: unknown): ValidationBody {
return value && typeof value === "object" ? (value as ValidationBody) : {};
}
function asNodeBody(value: unknown): NodeBody {
return value && typeof value === "object" ? (value as NodeBody) : {};
}
function findPrefixDetail(body: unknown): ValidationDetail | undefined {
const details = asValidationBody(body).error?.details ?? [];
return details.find((d) => d.field === "prefix");
}
function makeCreateRequest(body: Record<string, unknown>) {
return new Request("http://localhost/api/provider-nodes", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
}
function makeUpdateRequest(id: string, body: Record<string, unknown>) {
return new Request(`http://localhost/api/provider-nodes/${id}`, {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
}
test.beforeEach(async () => {
await resetStorage();
});
test.after(async () => {
await resetStorage();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
});
// ──── Shared module ────
test("shared set contains REGISTRY ids and aliases (tokenrouter + trk)", () => {
assert.equal(RESERVED_PROVIDER_PREFIXES.has("tokenrouter"), true);
assert.equal(RESERVED_PROVIDER_PREFIXES.has("trk"), true);
});
test("shared guard keeps retired Felo ids reserved after registry removal", () => {
assert.equal(RESERVED_PROVIDER_PREFIXES.has("felo-web"), true);
assert.equal(RESERVED_PROVIDER_PREFIXES.has("felo"), true);
assert.equal(isReservedProviderPrefix(" FeLo-Web "), true);
assert.equal(isReservedProviderPrefix("\u00a0FELO\uFEFF"), true);
});
test("shared guard keeps retired Qwen Web ids reserved after registry removal", () => {
assert.equal(RESERVED_PROVIDER_PREFIXES.has("qwen-web"), true);
assert.equal(RESERVED_PROVIDER_PREFIXES.has("qw"), true);
assert.equal(isReservedProviderPrefix(" QwEn-WeB "), true);
assert.equal(isReservedProviderPrefix("\u00a0QW\uFEFF"), true);
});
test("retired ChatGPT Web ids remain permanently reserved without capturing Codex variants", () => {
for (const prefix of ["chatgpt-web", "cgpt-web", " ChatGPT-Web ", "CGPT-WEB"]) {
assert.equal(isReservedProviderPrefix(prefix), true, `${prefix} must stay reserved`);
}
assert.equal(buildReservedPrefixes().has("chatgpt-web"), true);
assert.equal(buildReservedPrefixes().has("cgpt-web"), true);
for (const prefix of ["chatgpt-web-codex", "cgpt-codex"]) {
assert.equal(isReservedProviderPrefix(prefix), true, `${prefix} remains a live built-in`);
assert.equal(isCommonChatGptWebRetiredProviderId(prefix), false);
}
assert.equal(isReservedProviderPrefix("chatgpt-web-preview"), false);
assert.equal(isCommonChatGptWebRetiredProviderId("chatgpt-web-preview"), false);
});
test("mixed-case retired ChatGPT Web prefixes are never advertised as compatible nodes", async () => {
for (const [index, prefix] of ["ChatGPT-Web", "CGPT-WEB"].entries()) {
const id = `openai-compatible-retired-prefix-${index}`;
await providerNodesDb.createProviderNode({
id,
type: "openai-compatible",
name: `Retired mixed-case prefix ${index}`,
prefix,
apiType: "chat",
baseUrl: "https://retired.example.invalid/v1",
});
}
const index = await getProviderPrefixIndex();
for (const prefix of ["ChatGPT-Web", "CGPT-WEB"]) {
assert.equal(index.entries.get(prefix)?.status, "reserved");
assert.equal(index.prefixToNode.has(prefix), false);
}
});
test("shared set is case-sensitive like the runtime guard", () => {
assert.equal(isReservedProviderPrefix("TokenRouter"), false);
assert.equal(isReservedProviderPrefix("TOKENROUTER"), false);
assert.equal(isReservedProviderPrefix("tokenrouter"), true);
});
test("shared set excludes manual aliases that never intercept nodes at runtime", () => {
// Verified against src/sse/services/model.ts behavior: xiaomi/llamacpp/aq are
// not REGISTRY members and do NOT shadow compatible nodes, so rejecting them
// would be a false positive.
assert.equal(RESERVED_PROVIDER_PREFIXES.has("qwen"), false);
assert.equal(RESERVED_PROVIDER_PREFIXES.has("xiaomi"), false);
assert.equal(RESERVED_PROVIDER_PREFIXES.has("llamacpp"), false);
assert.equal(RESERVED_PROVIDER_PREFIXES.has("aq"), false);
});
test("shared set size includes live REGISTRY and retired Designer + Felo + Qwen Web prefixes", () => {
// 2026-08-30: 398 → 400 with Perplexity Agent API (#12103) and the second prefix the same
// afternoon batch registered — computed, not hand-derived (see the note below).
// Computed (not hand-derived) after combining Designer's 2 retired
// ids/aliases with Felo's 2 retired ids/aliases and Qwen Web's 2 retired
// ids/aliases (qwen-web's REGISTRY id/alias were identical strings, so its
// live-REGISTRY contribution was 1 unique member; retiring it removes that
// 1 and adds 2 distinct tombstones "qwen-web"/"qw", a net +1) on top of the
// live REGISTRY walk, minus the 3 GPL-derived Raycast/Hailuo Web
// ids/aliases removed from REGISTRY by #11691's migration 166.
assert.equal(RESERVED_PREFIX_COUNT, 400);
});
test("isReservedProviderPrefix rejects non-string input", () => {
assert.equal(isReservedProviderPrefix(undefined), false);
assert.equal(isReservedProviderPrefix(null), false);
assert.equal(isReservedProviderPrefix(42), false);
});
// ──── Schema-level guard ────
test("createProviderNodeSchema rejects reserved prefix 'tokenrouter'", () => {
const result = createProviderNodeSchema.safeParse({
name: "TokenRouter Node",
prefix: "tokenrouter",
apiType: "chat",
baseUrl: "https://api.tokenrouter.com/v1",
});
assert.equal(result.success, false);
if (!result.success) {
const prefixIssue = result.error.issues.find((i) => i.path[0] === "prefix");
assert.ok(prefixIssue, "expected a 'prefix' issue");
assert.match(prefixIssue.message, /reserved/i);
assert.match(prefixIssue.message, /tokenrouter/);
}
});
test("createProviderNodeSchema rejects reserved alias 'trk'", () => {
const result = createProviderNodeSchema.safeParse({
name: "TRK Node",
prefix: "trk",
apiType: "chat",
});
assert.equal(result.success, false);
});
test("provider node schemas reject retired Felo prefixes and normalized variants", () => {
for (const prefix of ["felo-web", "felo", " FeLo-Web ", "\u00a0FELO\uFEFF"]) {
const created = createProviderNodeSchema.safeParse({
name: "Retired prefix",
prefix,
apiType: "chat",
});
assert.equal(created.success, false, `create must reject ${JSON.stringify(prefix)}`);
const updated = updateProviderNodeSchema.safeParse({
name: "Retired prefix",
prefix,
baseUrl: "https://retired.example.invalid/v1",
});
assert.equal(updated.success, false, `update must reject ${JSON.stringify(prefix)}`);
const preset = createProviderNodeSchema.safeParse({
preset: "vibeproxy-openai",
prefix,
baseUrl: "http://localhost:8317",
});
assert.equal(preset.success, false, `preset create must reject ${JSON.stringify(prefix)}`);
}
});
test("provider node schemas reject retired Qwen Web prefixes and normalized variants", () => {
for (const prefix of ["qwen-web", "qw", " QwEn-WeB ", "\u00a0QW\uFEFF"]) {
const created = createProviderNodeSchema.safeParse({
name: "Retired prefix",
prefix,
apiType: "chat",
});
assert.equal(created.success, false, `create must reject ${JSON.stringify(prefix)}`);
const updated = updateProviderNodeSchema.safeParse({
name: "Retired prefix",
prefix,
baseUrl: "https://retired.example.invalid/v1",
});
assert.equal(updated.success, false, `update must reject ${JSON.stringify(prefix)}`);
const preset = createProviderNodeSchema.safeParse({
preset: "vibeproxy-openai",
prefix,
baseUrl: "http://localhost:8317",
});
assert.equal(preset.success, false, `preset create must reject ${JSON.stringify(prefix)}`);
}
});
test("provider-node schemas reject both retired common ChatGPT Web prefixes", () => {
for (const prefix of ["chatgpt-web", "cgpt-web", "CHATGPT-WEB"]) {
const createResult = createProviderNodeSchema.safeParse({
name: "Retired provider shadow",
prefix,
apiType: "chat",
baseUrl: "https://example.invalid/v1",
});
assert.equal(createResult.success, false, `create accepted ${prefix}`);
const updateResult = updateProviderNodeSchema.safeParse({
name: "Retired provider shadow",
prefix,
});
assert.equal(updateResult.success, false, `update accepted ${prefix}`);
}
});
test("createProviderNodeSchema accepts mixed-case 'TokenRouter' (no runtime collision)", () => {
const result = createProviderNodeSchema.safeParse({
name: "Case Test",
prefix: "TokenRouter",
apiType: "chat",
});
assert.equal(result.success, true);
});
test("createProviderNodeSchema accepts non-reserved prefixes", () => {
for (const prefix of ["my-gateway", "llamacpp", "aq", "xiaomi"]) {
const result = createProviderNodeSchema.safeParse({
name: "Free Prefix",
prefix,
apiType: "chat",
});
assert.equal(result.success, true, `prefix "${prefix}" should be accepted`);
}
});
test("updateProviderNodeSchema rejects reserved prefix", () => {
const result = updateProviderNodeSchema.safeParse({
name: "Renamed",
prefix: "openai",
});
assert.equal(result.success, false);
});
test("updateProviderNodeSchema accepts non-reserved prefix", () => {
const result = updateProviderNodeSchema.safeParse({
name: "Renamed",
prefix: "still-fine",
baseUrl: "https://renamed.example.com/v1",
});
assert.equal(result.success, true);
});
// ──── Route-level guard (POST /api/provider-nodes) ────
test("provider nodes route returns 400 with prefix issue for reserved prefix", async () => {
const response = await providerNodesRoute.POST(
makeCreateRequest({
name: "TokenRouter Node",
prefix: "tokenrouter",
apiType: "chat",
baseUrl: "https://api.tokenrouter.com/v1",
})
);
assert.equal(response.status, 400);
const detail = findPrefixDetail(await response.json());
assert.ok(detail, "expected a prefix validation detail");
assert.match(detail.message, /reserved/i);
});
test("provider nodes route still creates non-reserved nodes", async () => {
const response = await providerNodesRoute.POST(
makeCreateRequest({
name: "Good Node",
prefix: "good-node",
apiType: "chat",
baseUrl: "https://good.example.com/v1",
})
);
assert.equal(response.status, 201);
const body = asNodeBody(await response.json());
assert.equal(body.node?.prefix, "good-node");
});
// ──── Route-level guard (PUT /api/provider-nodes/[id]) ────
test("provider nodes update route rejects renaming prefix to a reserved one", async () => {
const createResponse = await providerNodesRoute.POST(
makeCreateRequest({
name: "Original Node",
prefix: "original-prefix",
apiType: "chat",
baseUrl: "https://original.example.com/v1",
})
);
const created = asNodeBody(await createResponse.json());
const nodeId = created.node?.id ?? "";
const updateResponse = await providerNodesIdRoute.PUT(
makeUpdateRequest(nodeId, {
name: "Hijacked",
prefix: "anthropic",
baseUrl: "https://hijack.example.com/v1",
}),
{ params: Promise.resolve({ id: nodeId }) }
);
assert.equal(updateResponse.status, 400);
const detail = findPrefixDetail(await updateResponse.json());
assert.ok(detail, "expected a prefix validation detail");
assert.match(detail.message, /reserved/i);
// The node keeps its original prefix.
const after = await providerNodesIdRoute.PUT(
makeUpdateRequest(nodeId, {
name: "Still Original",
prefix: "original-prefix",
apiType: "chat",
baseUrl: "https://original.example.com/v1",
}),
{ params: Promise.resolve({ id: nodeId }) }
);
assert.equal(after.status, 200);
const afterBody = asNodeBody(await after.json());
assert.equal(afterBody.node?.prefix, "original-prefix");
});