From db7c066f8729964045d5daab0376726bb25bd1bf Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Fri, 7 Aug 2026 05:31:07 -0300 Subject: [PATCH] fix(combo,usage,oauth): drain the base-reds the shard fix exposed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the migration collision and the broken import out of the way the four unit shards actually run, and a further layer of base-reds became visible on the pure tip 9995bc4893. Three are production defects. **Production defects** - open-sse/services/combo/runtimeUnitCapacity.ts:58 called resolveComboTargets() WITHOUT the hidden-model snapshot, so it fell back to the default getHiddenModelsByProvider() — a fresh full key_value read PER nested combo-ref unit, on every request. #8878 threaded the snapshot through the other call sites and missed this one. Threaded it from executeRuntimeUnitCombo (and from the dispatchPrelude call site), restoring the one-snapshot-per-request invariant combo-hidden-leaf-routing.test.ts pins. 9/9. - open-sse/services/usage/firecrawl.ts silently ignored its own `apiKey` parameter: 91bb6aa619 moved the fetch to fetchFirecrawlQuota(connectionId, connection), which reads the key off the connection record, so any caller passing the key directly got "Firecrawl API key not available". The explicit key is now merged into the connection passed down. firecrawl-usage 8/8. - src/lib/oauth/constants/oauth.ts was missing a RAYCAST entry in PROVIDERS while src/lib/oauth/providers/index.ts registers `raycast` (#8895), so every consumer reading PROVIDERS did not know Raycast Pro exists. Also added its OAUTH_TEST_CONFIG entry (checkExpiry only — it is an `import_token` provider with refreshToken always null), which #8408's guard explicitly requires rather than grandfathering. oauth-providers-config 25/25, oauth-test-config-8408 2/2. **Count / contract drift from the same batch** - feature flags 45 -> 46, APIKEY_PROVIDERS 197 -> 198 (Raycast Pro #8895), unique MCP tools 107 -> 108. Each re-derived from the source of truth. - vi + pt-BR locales: translated the 8 keys #9415 added (providers.newApiAggregator* and providers.modelTestQuotaTooltip) instead of relaxing the parity guard. i18n-vi 5/5, i18n-pt-br 3/3. - login-bootstrap-route: #9491 added `authenticated` to the require-login payload so /login can redirect an active session; the three deepEqual bodies now carry it. 10/10. **Flaky-by-construction, made deterministic** tests/unit/chat-combo-live-test.test.ts asserted the early-keepalive frame with a 100ms mocked upstream while resolveKeepaliveThreshold() is 2000ms for openai/*. It only ever passed while unrelated handler latency happened to push the total past the threshold — incidental, not deterministic, and it stopped holding once the handler got faster. The mock now sleeps 2400ms so the slow path is guaranteed and the assertion means what it says. 5/5. typecheck:core exit 0. check:file-size (base-relative) OK. Refs #9298 --- open-sse/services/combo/dispatchPrelude.ts | 1 + .../services/combo/runtimeUnitCapacity.ts | 16 +++++++++++++--- open-sse/services/combo/runtimeUnits.ts | 11 ++++++++++- open-sse/services/usage/firecrawl.ts | 19 ++++++++++++++++--- .../providers/[id]/test/oauthTestConfig.ts | 8 ++++++++ src/i18n/messages/pt-BR.json | 16 ++++++++-------- src/i18n/messages/vi.json | 10 +++++++++- src/lib/oauth/constants/oauth.ts | 4 ++++ tests/unit/chat-combo-live-test.test.ts | 8 +++++++- tests/unit/feature-flags-settings.test.ts | 6 +++--- tests/unit/login-bootstrap-route.test.ts | 6 ++++++ tests/unit/mcp-tool-count-dedup-6854.test.ts | 4 ++-- tests/unit/oauth-providers-config.test.ts | 3 +++ tests/unit/providers-constants-split.test.ts | 14 +++++++------- 14 files changed, 97 insertions(+), 29 deletions(-) diff --git a/open-sse/services/combo/dispatchPrelude.ts b/open-sse/services/combo/dispatchPrelude.ts index 833b8f86c4..65dd76e175 100644 --- a/open-sse/services/combo/dispatchPrelude.ts +++ b/open-sse/services/combo/dispatchPrelude.ts @@ -619,6 +619,7 @@ export async function tryRuntimeUnitDispatch(args: { nesting: nestingContext, baseOptions: buildBaseOptions(args), runCombo: args.runCombo, + hiddenModelsByProvider: args.hiddenModelsByProvider, }); recordRuntimeUnitStickySuccess({ strategy, diff --git a/open-sse/services/combo/runtimeUnitCapacity.ts b/open-sse/services/combo/runtimeUnitCapacity.ts index 4d0265b7a8..6af5430043 100644 --- a/open-sse/services/combo/runtimeUnitCapacity.ts +++ b/open-sse/services/combo/runtimeUnitCapacity.ts @@ -9,7 +9,12 @@ import { isAccountSemaphoreFull } from "../accountSemaphore.ts"; import { resolveComboTargets } from "./comboStructure.ts"; import { lookupPositiveCap } from "./concurrencyCaps.ts"; -import type { ComboCollectionLike, ComboLike, ResolvedComboUnit } from "./types.ts"; +import type { + ComboCollectionLike, + ComboLike, + HiddenModelsByProvider, + ResolvedComboUnit, +} from "./types.ts"; type CapLookup = (connectionId: string) => Promise; @@ -45,7 +50,12 @@ async function isConnectionAtConcurrencyCap( export async function isRuntimeUnitAtConcurrencyCap( unit: ResolvedComboUnit, allCombos: ComboCollectionLike, - lookupCap: CapLookup = lookupPositiveCap + lookupCap: CapLookup = lookupPositiveCap, + // Threaded from the caller so the hidden-model snapshot resolved once per + // request is reused. Without it resolveComboTargets falls back to its default + // getHiddenModelsByProvider(), i.e. a fresh full key_value read per nested + // combo-ref unit on EVERY request (#8878 threaded the other call sites). + hiddenModelsByProvider?: HiddenModelsByProvider ): Promise { if (unit.kind === "model") { if (!unit.connectionId || !unit.provider) return false; @@ -55,7 +65,7 @@ export async function isRuntimeUnitAtConcurrencyCap( const childCombo = findComboByName(allCombos, unit.comboName); if (!childCombo) return false; - const targets = resolveComboTargets(childCombo, allCombos, 1); + const targets = resolveComboTargets(childCombo, allCombos, 1, hiddenModelsByProvider); const byConnection = new Map(); for (const target of targets) { if (!target.connectionId || !target.provider) continue; diff --git a/open-sse/services/combo/runtimeUnits.ts b/open-sse/services/combo/runtimeUnits.ts index 28128d11f3..90ad1c2cac 100644 --- a/open-sse/services/combo/runtimeUnits.ts +++ b/open-sse/services/combo/runtimeUnits.ts @@ -18,6 +18,7 @@ import type { ComboNestingContext, HandleComboChatOptions, HandleSingleModel, + HiddenModelsByProvider, IsModelAvailable, ResolvedComboRefTarget, ResolvedComboUnit, @@ -186,6 +187,7 @@ export async function executeRuntimeUnitCombo(args: { nesting: ComboNestingContext; baseOptions: HandleComboChatOptions; runCombo: RuntimeUnitRunner; + hiddenModelsByProvider?: HiddenModelsByProvider; }): Promise { const maxRetries = Number(args.config.maxRetries ?? 1); const retryDelayMs = resolveDelayMs(args.config.retryDelayMs, 2000); @@ -197,7 +199,14 @@ export async function executeRuntimeUnitCombo(args: { let fallbackCount = 0; for (const unit of orderedUnits) { - if (await isRuntimeUnitAtConcurrencyCap(unit, args.allCombos)) { + if ( + await isRuntimeUnitAtConcurrencyCap( + unit, + args.allCombos, + undefined, + args.hiddenModelsByProvider + ) + ) { args.log.info( "COMBO", `Skipping ${unit.kind} ${unitDisplayName(unit)} — concurrency cap reached` diff --git a/open-sse/services/usage/firecrawl.ts b/open-sse/services/usage/firecrawl.ts index 09ea1f5211..793df7e673 100644 --- a/open-sse/services/usage/firecrawl.ts +++ b/open-sse/services/usage/firecrawl.ts @@ -5,7 +5,11 @@ * credits into the standard `{ plan, quotas }` response. */ -import { fetchFirecrawlQuota, getFirecrawlBaseUrl, type FirecrawlQuota } from "../firecrawlQuotaFetcher.ts"; +import { + fetchFirecrawlQuota, + getFirecrawlBaseUrl, + type FirecrawlQuota, +} from "../firecrawlQuotaFetcher.ts"; import { createQuotaFromUsage, parseResetTime } from "./quota.ts"; function createFirecrawlPlanQuota(q: FirecrawlQuota) { @@ -29,7 +33,11 @@ function createFirecrawlPlanQuota(q: FirecrawlQuota) { }; } -export async function getFirecrawlUsage(connectionId: string, apiKey?: string, connection?: Record) { +export async function getFirecrawlUsage( + connectionId: string, + apiKey?: string, + connection?: Record +) { if (!connectionId) { return { message: "Firecrawl: connection id unavailable." }; } @@ -44,7 +52,12 @@ export async function getFirecrawlUsage(connectionId: string, apiKey?: string, c } try { - const live = await fetchFirecrawlQuota(connectionId, connection); + // The explicit `apiKey` argument was silently dropped when #91bb6aa619 moved + // this to fetchFirecrawlQuota(connectionId, connection): the fetcher reads the + // key off the connection record, so a caller that passes the key directly — + // without a connection carrying it — always got "API key not available". + const resolvedConnection = apiKey ? { ...(connection || {}), apiKey } : connection; + const live = await fetchFirecrawlQuota(connectionId, resolvedConnection); if (!live) { return { message: "Firecrawl API key not available or credit usage unavailable." }; } diff --git a/src/app/api/providers/[id]/test/oauthTestConfig.ts b/src/app/api/providers/[id]/test/oauthTestConfig.ts index b6fdb29a4a..ac1aaa1680 100644 --- a/src/app/api/providers/[id]/test/oauthTestConfig.ts +++ b/src/app/api/providers/[id]/test/oauthTestConfig.ts @@ -111,6 +111,14 @@ export const OAUTH_TEST_CONFIG = { // Validate using token presence/expiry as a lightweight auth check. checkExpiry: true, }, + raycast: { + // #8895 — Raycast Pro is an `import_token` provider: the token is imported + // from the local Raycast install, `refreshToken` is always null and the + // stored `expiresIn` defaults to 30 days. There is nothing to refresh, so + // the test is the expiry check on the imported token; without an entry here + // Test Connection persists testStatus="error" on a healthy account (#8408). + checkExpiry: true, + }, cline: CLINE_OAUTH_TEST_CONFIG, // ClinePass reuses the same WorkOS OAuth flow and token lifecycle as Cline. clinepass: CLINE_OAUTH_TEST_CONFIG, diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index 0aed3399c9..e9727c9624 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -5475,13 +5475,13 @@ "newApiUserIdLabel": "ID de Usuário New-API", "newApiUserIdPlaceholder": "ex.: 12345", "newApiUserIdHint": "Valor do cabeçalho New-Api-User do AgentRouter, usado junto com a chave de API do console para consultar o saldo de cota.", - "newApiAggregatorToggleLabel": "Gateway Agregador", - "newApiAggregatorToggleHint": "Ativar detecção de saldo para nós agregadores New-API / One-API / Sub2API. O painel mostrará o badge de saldo e o roteamento de pré-voo de cota ignorará contas esgotadas.", - "newApiAggregatorConsoleApiKeyHint": "Token de Acesso do Sistema para o endpoint /api/user/self do agregador. Não é a chave de API de roteamento.", - "newApiAggregatorUserIdHint": "Valor do cabeçalho New-Api-User usado para consultar o saldo de cota do usuário do agregador.", - "newApiAggregatorQuotaPerUnitLabel": "Cota por Unidade", - "newApiAggregatorQuotaPerUnitHint": "Unidades de crédito New-API por $1 (padrão: 500000). Substitua se seu agregador usar uma taxa diferente.", - "featureFlagNewApiAggregatorBalanceDescription": "Ativar detecção de saldo para nós compatíveis de agregadores New-API / One-API / Sub2API", + "newApiAggregatorToggleLabel": "Gateway agregador", + "newApiAggregatorToggleHint": "Ativa a detecção de saldo para nós agregadores New-API / One-API / Sub2API. O painel passa a mostrar o selo de saldo e o roteamento com quota-preflight pula contas esgotadas.", + "newApiAggregatorConsoleApiKeyHint": "System Access Token para o endpoint /api/user/self do agregador. Não é a chave de API de roteamento.", + "newApiAggregatorUserIdHint": "Valor do cabeçalho New-Api-User usado para buscar o saldo de quota do usuário do agregador.", + "newApiAggregatorQuotaPerUnitLabel": "Quota por unidade", + "newApiAggregatorQuotaPerUnitHint": "Unidades de crédito New-API por US$ 1 (padrão: 500000). Substitua se o seu agregador usar outra taxa.", + "featureFlagNewApiAggregatorBalanceDescription": "Ativa a detecção de saldo para nós compatíveis New-API / One-API / Sub2API", "cpaModeDisabledTitle": "Habilitar backend CLIProxyAPI para emulação OAuth mais profunda do Claude Code", "cpaModeEnabledTitle": "Usando CLIProxyAPI para uma emulação mais profunda do Claude Code (uTLS, multi-conta, perfis de dispositivo)", "customUserAgentHint": "Override opcional enviado upstream como cabeçalho User-Agent desta conexão.", @@ -5597,7 +5597,7 @@ "tagGroupPlaceholder": "ex.: personal, work, team-a", "testModel": "Test Model", "testingModel": "Testing Model", - "modelTestQuotaTooltip": "Cota esgotada — reinicia amanhã ou precisa de recarga", + "modelTestQuotaTooltip": "Quota esgotada — reseta amanhã ou precisa de recarga", "toggleOffShort": "OFF", "toggleOnShort": "ON", "tokenExpiredBadge": "Expirado", diff --git a/src/i18n/messages/vi.json b/src/i18n/messages/vi.json index f6333012d9..828d2ae2ba 100644 --- a/src/i18n/messages/vi.json +++ b/src/i18n/messages/vi.json @@ -6020,7 +6020,15 @@ "ccAliasAddModelPlaceholder": "Id mô hình (ví dụ: gpt-4o)", "ccAliasAddModelButton": "Thêm ghi đè", "ccAliasLoadError": "Không tải được cài đặt bí danh khám phá: {error}", - "ccAliasSaveError": "Không lưu được cài đặt bí danh khám phá: {error}" + "ccAliasSaveError": "Không lưu được cài đặt bí danh khám phá: {error}", + "newApiAggregatorToggleLabel": "Cổng tổng hợp", + "newApiAggregatorToggleHint": "Bật phát hiện số dư cho các node tổng hợp New-API / One-API / Sub2API. Bảng điều khiển sẽ hiển thị huy hiệu số dư và định tuyến quota-preflight sẽ bỏ qua các tài khoản đã cạn.", + "newApiAggregatorConsoleApiKeyHint": "System Access Token cho endpoint /api/user/self của bộ tổng hợp. Không phải khóa API định tuyến.", + "newApiAggregatorUserIdHint": "Giá trị header New-Api-User dùng để lấy số dư quota của người dùng bộ tổng hợp.", + "newApiAggregatorQuotaPerUnitLabel": "Quota mỗi đơn vị", + "newApiAggregatorQuotaPerUnitHint": "Số đơn vị tín dụng New-API cho mỗi 1 USD (mặc định: 500000). Ghi đè nếu bộ tổng hợp của bạn dùng tỷ lệ khác.", + "featureFlagNewApiAggregatorBalanceDescription": "Bật phát hiện số dư cho các node tương thích New-API / One-API / Sub2API", + "modelTestQuotaTooltip": "Đã hết quota — sẽ đặt lại vào ngày mai hoặc cần nạp thêm" }, "settings": { "title": "Cài đặt", diff --git a/src/lib/oauth/constants/oauth.ts b/src/lib/oauth/constants/oauth.ts index f9aee266a0..02df89e7c9 100644 --- a/src/lib/oauth/constants/oauth.ts +++ b/src/lib/oauth/constants/oauth.ts @@ -531,6 +531,10 @@ export const PROVIDERS = { KIRO: "kiro", AMAZON_Q: "amazon-q", CURSOR: "cursor", + // #8895 — registered in src/lib/oauth/providers/index.ts but missing here, so + // every consumer reading PROVIDERS (onboarding wizard, test-connection routing) + // did not know Raycast Pro exists as an OAuth provider. + RAYCAST: "raycast", KILOCODE: "kilocode", CLINE: "cline", CLINEPASS: "clinepass", diff --git a/tests/unit/chat-combo-live-test.test.ts b/tests/unit/chat-combo-live-test.test.ts index da01178824..3961157f0f 100644 --- a/tests/unit/chat-combo-live-test.test.ts +++ b/tests/unit/chat-combo-live-test.test.ts @@ -250,7 +250,13 @@ test("chat completions route emits early keepalive while waiting for stream read await seedHealthyConnection(); globalThis.fetch = async () => { - await new Promise((resolve) => setTimeout(resolve, 100)); + // Must exceed resolveKeepaliveThreshold()'s DEFAULT_THRESHOLD_MS (2000ms) for + // openai/* — otherwise withEarlyStreamKeepalive takes the FAST path, forwards + // the handler response as-is and no keepalive frame is ever emitted. The old + // 100ms only worked while unrelated handler latency happened to push the + // total past the threshold, which made this assertion incidental rather than + // deterministic; it stopped holding once the handler got faster. + await new Promise((resolve) => setTimeout(resolve, 2_400)); return new Response( [ `data: ${JSON.stringify({ diff --git a/tests/unit/feature-flags-settings.test.ts b/tests/unit/feature-flags-settings.test.ts index c8a8f6d3d4..ff2cf36a66 100644 --- a/tests/unit/feature-flags-settings.test.ts +++ b/tests/unit/feature-flags-settings.test.ts @@ -30,13 +30,13 @@ const { isControlPlaneProxyDirectFallbackEnabled, } = await import("../../src/shared/utils/featureFlags.ts"); -const EXPECTED_FEATURE_FLAG_COUNT = 45; +const EXPECTED_FEATURE_FLAG_COUNT = 46; // ────────────────────────────────────────────────────── // Test group 1 — Flag definitions registry // ────────────────────────────────────────────────────── describe("featureFlagDefinitions", () => { - it("has exactly 45 flag definitions", () => { + it("has exactly 46 flag definitions", () => { assert.strictEqual(FEATURE_FLAG_DEFINITIONS.length, EXPECTED_FEATURE_FLAG_COUNT); }); @@ -332,7 +332,7 @@ describe("resolveFeatureFlag", () => { }); describe("resolveAllFeatureFlags", () => { - it("returns all 45 flags", () => { + it("returns all 46 flags", () => { const all = resolveAllFeatureFlags(); assert.strictEqual(all.length, EXPECTED_FEATURE_FLAG_COUNT); }); diff --git a/tests/unit/login-bootstrap-route.test.ts b/tests/unit/login-bootstrap-route.test.ts index 3764c0f84f..aff64ba9f2 100644 --- a/tests/unit/login-bootstrap-route.test.ts +++ b/tests/unit/login-bootstrap-route.test.ts @@ -46,6 +46,8 @@ test("public login bootstrap route exposes the metadata the login page consumes" assert.equal(response.status, 200); assert.deepEqual(body, { + // #9491 added `authenticated` so /login can redirect an active session. + authenticated: false, requireLogin: true, hasPassword: false, setupComplete: true, @@ -68,6 +70,8 @@ test("public login bootstrap route reports env-provided bootstrap password metad assert.equal(response.status, 200); assert.deepEqual(body, { + // #9491 added `authenticated` so /login can redirect an active session. + authenticated: false, requireLogin: true, hasPassword: true, setupComplete: true, @@ -89,6 +93,8 @@ test("public login bootstrap route reports stored password metadata and disabled assert.equal(response.status, 200); assert.deepEqual(body, { + // #9491 added `authenticated` so /login can redirect an active session. + authenticated: false, requireLogin: false, hasPassword: true, setupComplete: true, diff --git a/tests/unit/mcp-tool-count-dedup-6854.test.ts b/tests/unit/mcp-tool-count-dedup-6854.test.ts index c4cbbabe1f..aaa4d52477 100644 --- a/tests/unit/mcp-tool-count-dedup-6854.test.ts +++ b/tests/unit/mcp-tool-count-dedup-6854.test.ts @@ -6,7 +6,7 @@ import assert from "node:assert/strict"; // (omniroute_agent_skills_list/get/coverage) are intentionally defined in BOTH // MCP_TOOLS (open-sse/mcp-server/schemas/tools.ts) and agentSkillTools // (open-sse/mcp-server/tools/agentSkillTools.ts), so the additive sum reported 121 -// while only 107 distinct tool names actually exist. countUniqueMcpTools +// while only 108 distinct tool names actually exist. countUniqueMcpTools // (open-sse/mcp-server/toolCount.ts) fixes this by unioning tool names from every // registered collection into a Set, so each user-visible tool is counted once. @@ -60,7 +60,7 @@ test("#6854: countUniqueMcpTools de-duplicates tools registered in multiple coll }; const total = countUniqueMcpTools(collections); - assert.equal(total, 107, "the published MCP inventory must match the registered tool set"); + assert.equal(total, 108, "the published MCP inventory must match the registered tool set"); // Independently compute the "true" unique count by unioning every collection's // tool names into a Set — this must equal countUniqueMcpTools's own result AND diff --git a/tests/unit/oauth-providers-config.test.ts b/tests/unit/oauth-providers-config.test.ts index 49432b2ab7..ef1ed581eb 100644 --- a/tests/unit/oauth-providers-config.test.ts +++ b/tests/unit/oauth-providers-config.test.ts @@ -40,6 +40,7 @@ const { OAUTH_TIMEOUT, PROVIDERS: OAUTH_PROVIDER_IDS, QODER_CONFIG, + RAYCAST_CONFIG, TRAE_CONFIG, WINDSURF_CONFIG, XAI_OAUTH_CONFIG, @@ -63,6 +64,7 @@ const EXPECTED_PROVIDER_KEYS = [ "amazon-q", "cursor", "trae", + "raycast", "kilocode", "cline", "clinepass", @@ -100,6 +102,7 @@ const EXPECTED_CONFIG_BY_PROVIDER = { clinepass: CLINE_CONFIG, // reuses the Cline WorkOS flow (clinepass: cline in providers/index.ts) windsurf: WINDSURF_CONFIG, "devin-cli": WINDSURF_CONFIG, + raycast: RAYCAST_CONFIG, trae: TRAE_CONFIG, "grok-cli": GROK_BUILD_OAUTH_CONFIG, "xai-oauth": XAI_OAUTH_CONFIG, diff --git a/tests/unit/providers-constants-split.test.ts b/tests/unit/providers-constants-split.test.ts index 32332fc7da..06def06549 100644 --- a/tests/unit/providers-constants-split.test.ts +++ b/tests/unit/providers-constants-split.test.ts @@ -1,7 +1,7 @@ // Characterization of the providers.ts catalog split (god-file decomposition): the host became a // barrel that re-exports 10 data catalogs now living under constants/providers/*, and APIKEY is // merged from 6 semantic family files (apikey/.ts). Locks: the public surface (every catalog -// + helpers still exported), the spread-merge integrity (197 APIKEY entries, no loss/dup), and that +// + helpers still exported), the spread-merge integrity (198 APIKEY entries, no loss/dup), and that // load-time Zod validation still runs. Pure-data move → behavior must be identical. // Count was 171 before obsolete provider removals (PR #6675: glhf/kluster/cablyai/inclusionai etc., // 171->167) plus #6126 (ClinePass dual-auth): the API-key-only APIKEY_PROVIDERS_GATEWAYS entry was @@ -17,7 +17,7 @@ // sarvam+plamo in regional) to 193, then #8170 (inception/typhoon — inception in frontier-labs, // typhoon in regional) to 195, then Firecrawl dual search+fetch under SEARCH_PROVIDERS.firecrawl // (removed specialty-media duplicate) to 194, #8861 (Xiaomi MiMo Token Plan, regional) to 195, and -// the Cheaper Inference gateway (OSS-sponsor reseller, gateways family) to 197 (UnoRouter, #9009). +// the Cheaper Inference gateway (OSS-sponsor reseller, gateways family) to 198 (UnoRouter #9009, Raycast Pro #8895). import { test } from "node:test"; import assert from "node:assert/strict"; @@ -46,12 +46,12 @@ test("barrel still exports every catalog + key helpers", () => { } }); -test("APIKEY_PROVIDERS merges the 6 family files into 197 entries (no loss / no dup)", async () => { +test("APIKEY_PROVIDERS merges the 6 family files into 198 entries (no loss / no dup)", async () => { const keys = Object.keys((P as Record).APIKEY_PROVIDERS); - assert.equal(keys.length, 197); - assert.equal(new Set(keys).size, 197, "duplicate keys after spread-merge"); + assert.equal(keys.length, 198); + assert.equal(new Set(keys).size, 198, "duplicate keys after spread-merge"); // the merged object's entry-count equals the sum of the 6 semantic family files; families are a - // strict partition (every provider in exactly one), so the sum must be exactly 197. + // strict partition (every provider in exactly one), so the sum must be exactly 198. const families: [string, string][] = [ ["gateways", "APIKEY_PROVIDERS_GATEWAYS"], ["frontier-labs", "APIKEY_PROVIDERS_FRONTIER"], @@ -71,7 +71,7 @@ test("APIKEY_PROVIDERS merges the 6 family files into 197 entries (no loss / no seen.add(k); } } - assert.equal(famTotal, 197, "families must partition all 197 providers"); + assert.equal(famTotal, 198, "families must partition all 198 providers"); }); test("AI_PROVIDERS Proxy aggregates all sections; lookups resolve", () => {