From 979a7c63e96b02b7dca18aeb54ee02161e444f02 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Sat, 4 Jul 2026 06:37:54 -0300 Subject: [PATCH] =?UTF-8?q?fix(release):=20v3.8.44=20Phase-0=20pre-flight?= =?UTF-8?q?=20=E2=80=94=20base-red=20sweep=20+=20ratchet=20absorption?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix(models): stop resolveProviderAlias at registered provider ids so oc/ reaches the no-auth opencode provider again (#2901 contract, regressed by #5918's transitive chain; transitivity kept across alias-only hops) - fix(auggie): handle async EPIPE 'error' events on child stdin so a fast-exiting CLI surfaces a sanitized error instead of crashing (both spawn sites); deflakes auggie-executor tests - test: align provider family count 166->167 (Kenari #6104), regenerate translate-path golden on Linux (+kenari), opencode quota scope provider->connection (#6061) - quality(test-masking): add _deletedWithReplacement allowlist support to check-test-masking.mjs (deletion exempt ONLY when the declared replacement test exists in HEAD; 5 new gate unit tests) + reduction allowlist entries for the verified #5958/#6088/#5816 migrations + targetExhaustion-> combo-target-exhaustion replacement (#5976, 21 cases/52 asserts vs 13/37) - quality(file-size): absorb v3.8.44 cycle drift (oauth route 960, providerLimits 998, chat 1662, auth 2426) with justification; #6158 will restore the oauth-route freeze - changelog: bullets for the above + the #6155 cooling-panel build fix --- CHANGELOG.md | 10 +++- config/quality/file-size-baseline.json | 9 ++-- config/quality/test-masking-allowlist.json | 13 ++++- open-sse/executors/auggie.ts | 28 ++++++----- open-sse/services/model.ts | 9 +++- scripts/check/check-test-masking.mjs | 31 ++++++++++-- tests/snapshots/provider/translate-path.json | 23 +++++++++ tests/unit/check-test-masking.test.ts | 50 +++++++++++++++++++ .../provider-alias-transitive-5918.test.ts | 17 +++++-- tests/unit/provider-error-rules.test.ts | 4 +- tests/unit/providers-constants-split.test.ts | 12 ++--- 11 files changed, 171 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44b4802e86..dae566dc5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,12 @@ ### 🔧 Bug Fixes +- **models (`oc/` alias must reach the no-auth OpenCode provider):** restore the [#2901](https://github.com/diegosouzapw/OmniRoute/issues/2901) routing contract after the #5918 transitive-alias change made the registered no-auth `opencode` provider unreachable by any prefix (`oc/` chained through the manual `opencode` → `opencode-zen` slug override and misrouted its combo entries). `resolveProviderAlias` now stops the alias chain as soon as a hop lands on a registered provider id, while keeping #5918's transitivity across alias-only hops and its loop/depth guards. Regression guards: `tests/unit/combo-builder-opencode-prefix.test.ts`, `tests/unit/provider-alias-transitive-5918.test.ts`. + +- **providers (Auggie executor EPIPE crash):** a fast-exiting `auggie` CLI (e.g. binary present but immediately failing) delivered `EPIPE` **asynchronously** as an `'error'` event on the child's stdin stream — which a plain try/catch around `stdin.write()` cannot catch — crashing the request instead of surfacing the sanitized CLI error. Both spawn sites now attach a stdin `'error'` handler so the child's own exit/close handlers report the failure. Regression guard: `tests/unit/auggie-executor.test.ts` (deterministic 3/3 locally). + +- **dashboard (CoolingConnectionsPanel broke `next build`):** the cooling-connections panel from #6061 imported `Card` from a shadcn-style path that does not exist in this repo (`@/components/ui/card`) and pulled the server DB barrel (`@/lib/localDb`) into a client component — `next build` failed to compile on the release branch. The panel now renders with repo-native markup and reads `formatResetCountdown` from the new client-safe `src/shared/utils/formatting.ts`. Regression guards: `tests/unit/format-reset-countdown.test.ts`, `tests/unit/ui/CoolingConnectionsPanel.test.tsx`. ([#6155](https://github.com/diegosouzapw/OmniRoute/pull/6155)) + - **oauth (Zed "Unknown provider" crash):** adding **Zed** from the providers dashboard threw an unhandled `OAuth GET error: Unknown provider: zed` (500) ([#6041](https://github.com/diegosouzapw/OmniRoute/issues/6041)). Zed is a **keychain-import-only** provider — it's listed in the OAuth catalog so the UI shows it, but has no OAuth handler, so the generic `/api/oauth/[provider]/[action]` route hit `getProvider("zed")` and crashed. The route now recognizes keychain-import-only providers and returns a clear **400** pointing users at the **Import** button (for both GET and POST OAuth actions), instead of a 500. Regression guard: `tests/unit/oauth-keychain-import-only-6041.test.ts`. (thanks @imblowsnow) - **fix(providers):** disable the unsupported `thinking` param for `minimax-m2.7` on NVIDIA NIM (the upstream rejects it). Regression guard: `tests/unit/nvidia-minimax-thinking-strip.test.ts`. (thanks @anki1kr) @@ -34,7 +40,7 @@ - **translator (Responses → Chat Completions):** strip the Responses-API-only `truncation` field before forwarding a `/v1/responses` request to a non-OpenAI Chat Completions upstream. Strict upstreams (e.g. NVIDIA NIM) rejected it with HTTP 400 `Unsupported parameter(s): truncation`, breaking Codex-style clients routed to those providers. `client_metadata`, `background`, and `safety_identifier` were already stripped — `truncation` was the remaining gap. Regression guard: `tests/unit/responses-strip-truncation-2311.test.ts`. (thanks @TuanNguyen0708) -- **combo (prefer known context capacity over unknown):** when a combo filters out at least one target for exceeding a *known* context limit, the router now prefers the remaining known-compatible targets over targets whose context metadata is simply unknown, instead of letting unknown-metadata targets be the only survivors. If no known-compatible context target remains, context-only candidates fall back to the normal strategy order. Regression guard: `tests/unit/combo-context-window-filter.test.ts`. ([#6088](https://github.com/diegosouzapw/OmniRoute/pull/6088) — thanks @Thinkscape) +- **combo (prefer known context capacity over unknown):** when a combo filters out at least one target for exceeding a _known_ context limit, the router now prefers the remaining known-compatible targets over targets whose context metadata is simply unknown, instead of letting unknown-metadata targets be the only survivors. If no known-compatible context target remains, context-only candidates fall back to the normal strategy order. Regression guard: `tests/unit/combo-context-window-filter.test.ts`. ([#6088](https://github.com/diegosouzapw/OmniRoute/pull/6088) — thanks @Thinkscape) - **models (GLM-5.2 context normalization):** stop treating every hosted GLM-5.2 provider alias as the native 1M-context model. Native/bare GLM-5.2 and verified OpenCode / ZenMux routes keep their 1,000,000-token context, while hosted-provider aliases now respect the caps declared in their provider metadata instead of inheriting the native max. Regression guards: `tests/unit/model-capabilities-registry.test.ts`, `tests/unit/models-catalog-route.test.ts`. ([#6091](https://github.com/diegosouzapw/OmniRoute/pull/6091) — thanks @Thinkscape) @@ -46,6 +52,8 @@ ### 📝 Maintenance +- **release close (Phase 0 pre-flight):** align cycle-stale tests with merged behavior — provider count 166→167 (Kenari #6104), Linux-regenerated translate-path golden (+`kenari`), OpenCode quota scope `provider`→`connection` (#6061) — and absorb cycle ratchet drift (file-size caps for `oauth/[provider]/[action]/route.ts` 960, `providerLimits.ts` 998, `chat.ts` 1662, `auth.ts` 2426, with #6158 tracked to restore the oauth-route freeze). The test-masking gate gains a narrowly-scoped `_deletedWithReplacement` allowlist section (deletion is exempt ONLY when the declared replacement test file exists in HEAD — used for `targetExhaustion.test.ts` → `tests/unit/combo/combo-target-exhaustion.test.ts`, which has MORE coverage: 21 cases/52 asserts vs 13/37), plus 5 new gate unit tests and reduction-allowlist entries for the verified-legitimate #5958/#6088/#5816 assert migrations. + - **test (deflake `setup-claude`):** `tests/unit/cli/setup-claude.test.ts` failed ~50% of runs with `Unable to deserialize cloned data due to invalid or unsupported version` at file teardown (all subtests passed), randomly reddening `Unit Tests fast-path (2/2)` / `Fast Quality Gates` across the PR→release queue. Root cause: `node --test` streams each file's report to the parent as V8-serialized frames on fd 1 (stdout), and the CLI helper under test (`syncClaudeProfilesFromModels`) prints progress via `console.log` — that stdout output interleaved with the serialized frames and corrupted the stream. The test now silences the stdout-writing `console` methods for the file's duration (no assertion inspects stdout), making it deterministic (15/15 green locally). ([#5959](https://github.com/diegosouzapw/OmniRoute/issues/5959)) - **API validation:** add a `validatedJsonBody(request, schema)` helper in `src/shared/validation/helpers.ts` that fuses JSON body parsing and Zod validation into a single call, returning either the type-narrowed data or a ready-to-return 400 `NextResponse` with the standard error envelope. Salvaged from the closed refactor PR #5075 (Tier 1 portable helper) with a focused 6-case regression test. Co-authored-by: KooshaPari diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 67fdc82d7e..090083aa63 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -1,4 +1,5 @@ { + "_rebaseline_2026_07_04_v3844_release_close": "Release v3.8.44 Phase 0 (generate-release): drift de ciclo absorvido no fechamento, medido no tip 415d159c8 (fast-gates PR->release nao rodam check:file-size). oauth/[provider]/[action]/route.ts 924->960 (#6054 zed keychain-import 400 gracioso; PR #6158 aberto extrai o guard e restaura o freeze — quando mergear, o frozen so encolhe), providerLimits.ts 982->998 (#6139 TOCTOU quota recovery + #6128), chat.ts 1647->1662 (#6057 per-request Auto-Combo X-OmniRoute-Mode/Budget + #6097), auth.ts 2405->2426 (#6139 + #6090 quota preflight lockouts + #5943 codex session affinity). Crescimento irreducivel em chokepoints existentes, coberto por testes por-PR; shrink estrutural rastreado no roadmap #3501.", "_rebaseline_2026_07_03_v3844_ipfilter_release_green": "testFrozen bumps: models-catalog-route 1507->1600, perplexity-web 959->999, route-edge-coverage 1234->1241 (last is my #5975 comment +7). v3.8.44 cycle drift measured on release tip 32e4c906e during the #6131/#5975 release-green rebaseline. Inherited from the merge burst (Quality Ratchet does not run on PR->release fast-gates). route-edge-coverage +7 is my #5975 test comment; the rest is parallel-session drift. Tighten via --update next cycle.", "_rebaseline_2026_07_03_v3844_residual_release_green": "Residual file-size drift on tip 716041223: providerLimits.ts 955->982 + accountFallback.ts 1790->1864 (production god-files grown by parallel-session merges e.g. #6128; ideally DECOMPOSE not rebaseline, tracked as debt) + sse-auth.test.ts 1553->1600. None mine.", "_comment": "Catraca de tamanho (check-file-size.mjs). frozen so pode encolher; arquivos novos <= cap. --update ratcheta.", @@ -221,7 +222,7 @@ "src/app/(dashboard)/dashboard/usage/components/BudgetTab.tsx": 1016, "src/app/(dashboard)/dashboard/usage/components/EvalsTab.tsx": 2148, "src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx": 1121, - "src/app/api/oauth/[provider]/[action]/route.ts": 924, + "src/app/api/oauth/[provider]/[action]/route.ts": 960, "src/app/api/providers/[id]/models/route.ts": 2593, "src/app/api/providers/[id]/test/route.ts": 940, "src/app/api/usage/analytics/route.ts": 942, @@ -242,7 +243,7 @@ "src/lib/resilience/settings.ts": 841, "src/lib/tailscaleTunnel.ts": 1202, "src/lib/usage/callLogs.ts": 997, - "src/lib/usage/providerLimits.ts": 982, + "src/lib/usage/providerLimits.ts": 998, "src/lib/usage/usageHistory.ts": 988, "_rebaseline_2026_06_27_5193_5203_antigravity_oauthmodal": "Antigravity remote-login own growth: OAuthModal.tsx 960->969 (gate units). #5193 (+~4: remote paste instruction shown for all remote incl. Google + its rationale comment) and #5203 (+~5: handleManualSubmit credential-blob branch + button guard; submit logic extracted to oauthBlobSubmit.ts to minimize). Frozen set to the SUM so either merge order passes. Cohesive at the existing manual-submit chokepoint.", "src/shared/components/OAuthModal.tsx": 989, @@ -255,9 +256,9 @@ "src/shared/services/cliRuntime.ts": 1100, "src/shared/validation/schemas.ts": 2523, "_rebaseline_2026_06_28_5275_correlation_id_extract": "Extraction of the safe CorrelationId subset of #5275 (hartmark) — request correlation id stored in call_logs (migration 109) and returned via the X-Correlation-Id response header, WITHOUT the combo/resilience or build/lazy-loading changes (those stay in #5275). Own growth: callLogs.ts 975->985 (correlation_id column on CallLogSummaryRow + read/map), usageHistory.ts 983->988 (correlationId metadata normalize), chat.ts 1575->1632 (withCorrelationId response wiring + combo-failure log carrying correlationId), chatHelpers.ts new 811 (withCorrelationId helper + reqId threading; was 7913min no CI) e foram REESCRITOS como unit determinístico com MAIS cobertura (21 casos/52 asserts vs 13 casos/37 asserts). Documentado no commit 5fe225850. Revisão humana: apresentado ao operador no STOP #1 do release v3.8.44." + } + } } diff --git a/open-sse/executors/auggie.ts b/open-sse/executors/auggie.ts index f15d82c7e3..a97890f0f1 100644 --- a/open-sse/executors/auggie.ts +++ b/open-sse/executors/auggie.ts @@ -38,9 +38,7 @@ const AUGGIE_URL = "auggie://cli/stdio"; // untrusted-input sink. We only ever pass a model that is declared in the // registry entry — this closes flag-smuggling (a `model` starting with "-" would // otherwise be parsed by auggie as an option) and unknown-model passthrough. -const AUGGIE_MODEL_ALLOWLIST: ReadonlySet = new Set( - auggieProvider.models.map((m) => m.id) -); +const AUGGIE_MODEL_ALLOWLIST: ReadonlySet = new Set(auggieProvider.models.map((m) => m.id)); const DEFAULT_AUGGIE_MODEL = auggieProvider.models[0]?.id ?? "claude-sonnet-4.6"; type AuggieModelResolution = { ok: true; model: string } | { ok: false; error: string }; @@ -229,13 +227,7 @@ export class AuggieExecutor extends BaseExecutor { return null; } - async execute({ - model, - body, - stream, - signal, - log, - }: ExecuteInput): Promise<{ + async execute({ model, body, stream, signal, log }: ExecuteInput): Promise<{ response: Response; url: string; headers: Record; @@ -282,6 +274,11 @@ export class AuggieExecutor extends BaseExecutor { env: process.env, stdio: ["pipe", "pipe", "pipe"], }); + // EPIPE from a fast-exiting CLI arrives ASYNCHRONOUSLY as an 'error' event on + // stdin (not a sync throw), so the try/catch below cannot swallow it — without + // this handler the unhandled stream error crashes the process instead of + // letting the child's own 'error'/'close' handlers surface the failure. + child.stdin.on("error", () => {}); try { child.stdin.write(promptText); child.stdin.end(); @@ -379,10 +376,15 @@ export class AuggieExecutor extends BaseExecutor { }); } catch (err) { const message = err instanceof Error ? err.message : String(err); - emitError(isEnoentLike(message) ? cliNotFoundMessage(auggieBin) : sanitizeErrorMessage(message)); + emitError( + isEnoentLike(message) ? cliNotFoundMessage(auggieBin) : sanitizeErrorMessage(message) + ); return; } + // Async EPIPE lands as an 'error' event on stdin, not a sync throw (see + // spawnAuggie) — handle it so a fast-exiting CLI can't crash the stream. + child.stdin.on("error", () => {}); try { child.stdin.write(promptText); child.stdin.end(); @@ -399,7 +401,9 @@ export class AuggieExecutor extends BaseExecutor { child.on("error", (err: NodeJS.ErrnoException) => { const message = err?.message || String(err); - emitError(isEnoentLike(message) ? cliNotFoundMessage(auggieBin) : sanitizeErrorMessage(message)); + emitError( + isEnoentLike(message) ? cliNotFoundMessage(auggieBin) : sanitizeErrorMessage(message) + ); }); let stderrTail = ""; diff --git a/open-sse/services/model.ts b/open-sse/services/model.ts index 122247a966..03b8ba4853 100644 --- a/open-sse/services/model.ts +++ b/open-sse/services/model.ts @@ -142,14 +142,19 @@ interface ProviderConnectionLike { */ export function resolveProviderAlias(aliasOrId: string | null | undefined): string | null { if (typeof aliasOrId !== "string") return null; - // Follow the alias chain transitively so intermediate aliases - // (e.g. "oc" -> "opencode" -> "opencode-zen") resolve to the final target. + // Follow the alias chain transitively so intermediate alias-only hops resolve + // to the final target, but STOP as soon as a hop lands on a registered + // provider id (#2901): "oc" must resolve to the no-auth "opencode" provider, + // NOT continue through the manual "opencode" → "opencode-zen" slug override — + // that override is for user-typed `opencode/` prefixes only. Without this + // boundary the no-auth provider becomes unreachable by any prefix. // Guarded against infinite loops with both a depth limit and a seen-set. let current = aliasOrId; const seen = new Set(); for (let i = 0; i < 10; i++) { const next = ALIAS_TO_PROVIDER_ID[current]; if (!next || next === current) return current; + if (next in PROVIDER_ID_TO_ALIAS) return next; if (seen.has(next)) return next; seen.add(next); current = next; diff --git a/scripts/check/check-test-masking.mjs b/scripts/check/check-test-masking.mjs index 8092a744ba..47b0060a58 100644 --- a/scripts/check/check-test-masking.mjs +++ b/scripts/check/check-test-masking.mjs @@ -61,15 +61,32 @@ export function countExtendedTautologies(src) { * (6A.10 subcheck 1) Sinaliza arquivos de teste DELETADOS ou renomeados-e-não- * substituídos. Recebe lista de paths de arquivos de teste que foram deletados * (filtro D do git diff --diff-filter=MDR). + * + * `deletionAllowlist` (`_deletedWithReplacement` no test-masking-allowlist.json) + * isenta uma deleção SOMENTE quando o substituto declarado existe no HEAD e é + * ele próprio um arquivo de teste — o caso "reescrito em outro path sem rename + * detectável" (conteúdo novo demais para o -M do git). Qualquer entrada cujo + * substituto não exista ou não seja teste continua flagada. */ -export function evaluateDeletedFiles(deletedPaths) { +export function evaluateDeletedFiles( + deletedPaths, + deletionAllowlist = {}, + fileExists = fs.existsSync +) { const flags = []; for (const f of deletedPaths) { - if (TEST_RE.test(f)) { + if (!TEST_RE.test(f)) continue; + const entry = deletionAllowlist[f]; + if (entry && typeof entry.replacement === "string") { + if (TEST_RE.test(entry.replacement) && fileExists(entry.replacement)) continue; flags.push( - `${f}: arquivo de teste deletado — revisão humana obrigatória (mascaramento alto-sinal)` + `${f}: deleção allowlistada mas o substituto declarado (${entry.replacement}) não existe ou não é arquivo de teste` ); + continue; } + flags.push( + `${f}: arquivo de teste deletado — revisão humana obrigatória (mascaramento alto-sinal)` + ); } return flags; } @@ -197,8 +214,6 @@ function main() { }); } - const deletedFlags = evaluateDeletedFiles([...deletedTests, ...relocatedOutOfTest]); - // Arquivos de teste modificados (subcheck original + skips + extTaut) const changed = git(["diff", "--name-only", "--diff-filter=M", `${base}...HEAD`]) .split("\n") @@ -225,13 +240,19 @@ function main() { // Per-file allowlist for verified-legitimate net-assert reductions (refactor/field-removal). // Only exempts the reduction signal; tautology/skip/deletion signals still fire. let assertReductionAllowlist = new Set(); + let deletionAllowlist = {}; try { const raw = JSON.parse(fs.readFileSync("config/quality/test-masking-allowlist.json", "utf8")); assertReductionAllowlist = new Set(Object.keys(raw).filter((k) => !k.startsWith("_"))); + deletionAllowlist = raw._deletedWithReplacement || {}; } catch { // no allowlist file — treat as empty } + const deletedFlags = evaluateDeletedFiles( + [...deletedTests, ...relocatedOutOfTest], + deletionAllowlist + ); const maskingFlags = evaluateMasking(perFile, assertReductionAllowlist); const allFlags = [...deletedFlags, ...maskingFlags]; diff --git a/tests/snapshots/provider/translate-path.json b/tests/snapshots/provider/translate-path.json index 6fe22b4d39..e477551cec 100644 --- a/tests/snapshots/provider/translate-path.json +++ b/tests/snapshots/provider/translate-path.json @@ -2224,6 +2224,29 @@ "stream": "https://chatapi.innerai.com/chat" } }, + "kenari": { + "format": "openai", + "headers": { + "apiKey": { + "Accept": "text/event-stream", + "Authorization": "Bearer ", + "Content-Type": "application/json" + }, + "nonStream": { + "Authorization": "Bearer ", + "Content-Type": "application/json" + }, + "oauth": { + "Accept": "text/event-stream", + "Authorization": "Bearer ", + "Content-Type": "application/json" + } + }, + "url": { + "nonStream": "https://kenari.id/v1/chat/completions", + "stream": "https://kenari.id/v1/chat/completions" + } + }, "kie": { "format": "openai", "headers": { diff --git a/tests/unit/check-test-masking.test.ts b/tests/unit/check-test-masking.test.ts index c02cb3ed0f..92d9d713b1 100644 --- a/tests/unit/check-test-masking.test.ts +++ b/tests/unit/check-test-masking.test.ts @@ -92,6 +92,56 @@ test("evaluateDeletedFiles: empty list returns no flags", () => { assert.deepEqual(evaluateDeletedFiles([]), []); }); +test("evaluateDeletedFiles: deletion with verified replacement (allowlisted, replacement exists) is not flagged", () => { + const allow = { + "tests/unit/foo.test.ts": { + replacement: "tests/unit/bar.test.ts", + reason: "v9.9.9 #0000: rewritten as deterministic unit test", + }, + }; + const flags = evaluateDeletedFiles( + ["tests/unit/foo.test.ts"], + allow, + (p) => p === "tests/unit/bar.test.ts" + ); + assert.deepEqual(flags, []); +}); + +test("evaluateDeletedFiles: allowlisted deletion whose replacement does NOT exist is still flagged", () => { + const allow = { + "tests/unit/foo.test.ts": { + replacement: "tests/unit/missing.test.ts", + reason: "v9.9.9 #0000: bogus", + }, + }; + const flags = evaluateDeletedFiles(["tests/unit/foo.test.ts"], allow, () => false); + assert.equal(flags.length, 1); + assert.match(flags[0], /substituto|replacement/i); +}); + +test("evaluateDeletedFiles: allowlisted deletion whose replacement is not a test file is still flagged", () => { + const allow = { + "tests/unit/foo.test.ts": { + replacement: "src/lib/notATest.ts", + reason: "v9.9.9 #0000: bogus", + }, + }; + const flags = evaluateDeletedFiles(["tests/unit/foo.test.ts"], allow, () => true); + assert.equal(flags.length, 1); +}); + +test("evaluateDeletedFiles: deletion not present in the allowlist is flagged as before", () => { + const allow = { + "tests/unit/other.test.ts": { + replacement: "tests/unit/bar.test.ts", + reason: "v9.9.9 #0000: unrelated entry", + }, + }; + const flags = evaluateDeletedFiles(["tests/unit/foo.test.ts"], allow, () => true); + assert.equal(flags.length, 1); + assert.match(flags[0], /foo\.test\.ts/); +}); + test("evaluateDeletedFiles: multiple deleted test files all flagged", () => { const flags = evaluateDeletedFiles([ "tests/unit/a.test.ts", diff --git a/tests/unit/provider-alias-transitive-5918.test.ts b/tests/unit/provider-alias-transitive-5918.test.ts index ae8df4cf88..f5f0be2e56 100644 --- a/tests/unit/provider-alias-transitive-5918.test.ts +++ b/tests/unit/provider-alias-transitive-5918.test.ts @@ -12,15 +12,26 @@ * * Fix: resolve transitively with a depth limit AND a seen-set so cycles cannot loop. * These assertions FAIL on the old single-hop implementation and pass on the fix. + * + * RECONCILED at v3.8.44 with the #2901 contract: the chain STOPS as soon as a hop + * lands on a REGISTERED provider id. "oc" is the registry alias of the no-auth + * "opencode" provider, so `oc/` must resolve to it — continuing through the + * manual "opencode" → "opencode-zen" slug override (which exists only for + * user-typed `opencode/` prefixes) would leave the no-auth provider unreachable by + * any prefix and misroute its combo entries (see combo-builder-opencode-prefix + * test #2901). Transitivity still applies across alias-only hops, and the + * loop/depth guards from #5918 are retained. */ import test from "node:test"; import assert from "node:assert/strict"; import { resolveProviderAlias } from "../../open-sse/services/model.ts"; -test("resolveProviderAlias follows the oc -> opencode -> opencode-zen chain transitively", () => { - // The bug: single-hop returned "opencode" (intermediate) instead of "opencode-zen". - assert.equal(resolveProviderAlias("oc"), "opencode-zen"); +test("resolveProviderAlias stops the oc chain at the registered no-auth opencode provider (#2901)", () => { + // "opencode" is a REGISTERED provider id (the no-auth tier, alias "oc"), so the + // chain must stop there instead of following the manual opencode→opencode-zen + // slug override — otherwise the no-auth provider is unreachable by any prefix. + assert.equal(resolveProviderAlias("oc"), "opencode"); }); test("resolveProviderAlias resolves a direct one-hop alias", () => { diff --git a/tests/unit/provider-error-rules.test.ts b/tests/unit/provider-error-rules.test.ts index c431592828..163d08ea01 100644 --- a/tests/unit/provider-error-rules.test.ts +++ b/tests/unit/provider-error-rules.test.ts @@ -71,7 +71,9 @@ test("S2b: provider error rules match canonical-cased plain header records", asy }); assert.ok(opencodeMatch, "Opencode quota headers must be case-insensitive"); assert.equal(opencodeMatch.reason, "quota_exhausted"); - assert.equal(opencodeMatch.scope, "provider"); + // #6061: opencode quota is per-account, so the lock scopes to the CONNECTION + // (locking the whole provider would disable every other account's connection). + assert.equal(opencodeMatch.scope, "connection"); const minimaxMatch = getProviderErrorRuleMatch("Minimax", 429, { "X-Model-Quota-Remaining": "haiku=0,sonnet=42", diff --git a/tests/unit/providers-constants-split.test.ts b/tests/unit/providers-constants-split.test.ts index 7e187dbbb2..260657fa62 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 (166 APIKEY entries, no loss/dup), and that +// + helpers still exported), the spread-merge integrity (167 APIKEY entries, no loss/dup), and that // load-time Zod validation still runs. Pure-data move → behavior must be identical. import { test } from "node:test"; import assert from "node:assert/strict"; @@ -31,12 +31,12 @@ test("barrel still exports every catalog + key helpers", () => { } }); -test("APIKEY_PROVIDERS merges the 6 family files into 166 entries (no loss / no dup)", async () => { +test("APIKEY_PROVIDERS merges the 6 family files into 167 entries (no loss / no dup)", async () => { const keys = Object.keys((P as Record).APIKEY_PROVIDERS); - assert.equal(keys.length, 166); - assert.equal(new Set(keys).size, 166, "duplicate keys after spread-merge"); + assert.equal(keys.length, 167); + assert.equal(new Set(keys).size, 167, "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 166. + // strict partition (every provider in exactly one), so the sum must be exactly 167. const families: [string, string][] = [ ["gateways", "APIKEY_PROVIDERS_GATEWAYS"], ["frontier-labs", "APIKEY_PROVIDERS_FRONTIER"], @@ -56,7 +56,7 @@ test("APIKEY_PROVIDERS merges the 6 family files into 166 entries (no loss / no seen.add(k); } } - assert.equal(famTotal, 166, "families must partition all 166 providers"); + assert.equal(famTotal, 167, "families must partition all 167 providers"); }); test("AI_PROVIDERS Proxy aggregates all sections; lookups resolve", () => {