mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
fix(release): v3.8.44 Phase-0 pre-flight — base-red sweep + ratchet absorption
- 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
This commit is contained in:
10
CHANGELOG.md
10
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 <KooshaPari@users.noreply.github.com>
|
||||
|
||||
@@ -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 791<cap pre-feature). Cohesive request/logging chokepoint wiring; structural shrink of chat.ts tracked in #3501.",
|
||||
"src/sse/handlers/chat.ts": 1647,
|
||||
"src/sse/handlers/chat.ts": 1662,
|
||||
"src/sse/handlers/chatHelpers.ts": 842,
|
||||
"src/sse/services/auth.ts": 2405,
|
||||
"src/sse/services/auth.ts": 2426,
|
||||
"open-sse/executors/default.ts": 877,
|
||||
"open-sse/translator/request/openai-responses.ts": 902,
|
||||
"open-sse/executors/kiro.ts": 944,
|
||||
|
||||
@@ -11,5 +11,16 @@
|
||||
"tests/unit/chatgpt-web.test.ts": "v3.8.43 #5549: fix(chatgpt-web) restore dot-form Pro model ids — dois assert.equal separados (base Pro slug + pass-through slug) consolidados num único assert.equal(body.model, expectedSlugById[omniId], ...) orientado por tabela de mapeamento (281→280). Asserts consolidados, não enfraquecidos. Verificado legítimo. Prune após v3.8.43 mergear para main.",
|
||||
"tests/unit/chatcore-sanitization.test.ts": "v3.8.43 #5805: fix(translator) strip orphaned tool results — orphaned tool_result blocks (no matching tool_use) are now removed by stripOrphanedToolResults BEFORE content normalization, so the 3 positive `[Tool Result: …]`-text asserts were replaced by removal asserts (no tool_result block, no text), net 65→64. Behavior aligned to the merged #5805 contract; the sibling 'preserves Claude passthrough tool_result' assert (matching tool_use) is untouched. Verified legitimate, not masking. Prune after v3.8.43 merges to main.",
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/__tests__/useModelVisibilityHandlers.test.tsx": "v3.8.43 #5856: fix(dashboard) unify CSRF origin fallback — the model-visibility handler no longer issues a separate `/api/auth/csrf` fetch, so the two asserts pinning the 2nd fetch + its CSRF header were removed and the fetch-count assert updated 2→1 (7→5). Asserts follow the reduced fetch behavior, not weakened. Verified legitimate. Prune after v3.8.43 merges to main.",
|
||||
"tests/unit/provider-validation-specialty.test.ts": "v3.8.43 #5855: fix(qwen-web) unblock validator (retired endpoint) — the old `chat.qwen.ai/api/v2/user` probe asserts (exact URL / Authorization / source / Cookie / WAF-error) no longer apply after the endpoint migration and were replaced by new chathub-path behavior asserts (valid/error/warning), net 406→400. Asserts migrated to the new API surface (#5855/#5432), not weakened. Verified legitimate. Prune after v3.8.43 merges to main."
|
||||
"tests/unit/provider-validation-specialty.test.ts": "v3.8.43 #5855: fix(qwen-web) unblock validator (retired endpoint) — the old `chat.qwen.ai/api/v2/user` probe asserts (exact URL / Authorization / source / Cookie / WAF-error) no longer apply after the endpoint migration and were replaced by new chathub-path behavior asserts (valid/error/warning), net 406→400. Asserts migrated to the new API surface (#5855/#5432), not weakened. Verified legitimate. Prune after v3.8.43 merges to main.",
|
||||
"tests/unit/chatcore-translation-paths.test.ts": "v3.8.44 #5958: fix(cc-compatible) send SSE accept for streamed requests — o assert do header Accept mudou de application/json para text/event-stream e dois asserts (stream + context_management) foram consolidados num único assert.deepEqual de tupla (297→296). Asserts consolidados/atualizados ao novo contrato, não enfraquecidos. Verificado legítimo. Prune após v3.8.44 mergear para main.",
|
||||
"tests/unit/combo-context-window-filter.test.ts": "v3.8.44 #6088: fix(combo) prefer known context capacity over unknown — a semântica do filtro mudou (unknown-context só sobrevive como fallback), o arquivo foi reescrito: 18 asserts pequenos (length/didFallback/ok) viraram 4 assert.deepEqual na LISTA ORDENADA completa de sobreviventes por caso (mais forte por caso). Asserts migrados ao novo contrato, não enfraquecidos. Verificado legítimo. Prune após v3.8.44 mergear para main.",
|
||||
"tests/unit/qoder-executor.test.ts": "v3.8.44 #5816: feat(qoder) drive PAT auth via qodercli — o executor migrou de chamadas HTTP diretas (api.qoder.com/api1.qoder.sh, headers Cosy-*) para o contrato stdio qodercli://; os asserts que pinavam URLs/headers da superfície aposentada foram substituídos por asserts do novo contrato (73→65). Asserts migrados à nova superfície, não enfraquecidos. Verificado legítimo. Prune após v3.8.44 mergear para main.",
|
||||
"tests/unit/qoder-jobtoken-exchange-4683.test.ts": "v3.8.44 #5816: feat(qoder) drive PAT auth via qodercli — o exchange de job token deixou de fazer o POST personal_token direto (agora via qodercli); 2 asserts da superfície HTTP aposentada removidos, demais migrados (27→25). Verificado legítimo, não mascaramento. Prune após v3.8.44 mergear para main.",
|
||||
"_deletedWithReplacement": {
|
||||
"_comment": "Deleções de arquivo de teste com SUBSTITUTO verificado (o gate exige que o replacement exista no HEAD e seja arquivo de teste). Uso restrito ao caso 'reescrito em outro path sem rename detectável pelo -M do git'. Cada entrada precisa de reason com PR ref e passa por revisão humana no release PR. Prune após o release mergear para main.",
|
||||
"open-sse/services/combo/__tests__/targetExhaustion.test.ts": {
|
||||
"replacement": "tests/unit/combo/combo-target-exhaustion.test.ts",
|
||||
"reason": "v3.8.44 #5976: os testes de exaustão eram flake-prone (delays Math.random, timeouts 30s, >3min 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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string> = new Set(
|
||||
auggieProvider.models.map((m) => m.id)
|
||||
);
|
||||
const AUGGIE_MODEL_ALLOWLIST: ReadonlySet<string> = 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<string, string>;
|
||||
@@ -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 = "";
|
||||
|
||||
@@ -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<string>();
|
||||
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;
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -2224,6 +2224,29 @@
|
||||
"stream": "https://chatapi.innerai.com/chat"
|
||||
}
|
||||
},
|
||||
"kenari": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
"apiKey": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"nonStream": {
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"oauth": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"nonStream": "https://kenari.id/v1/chat/completions",
|
||||
"stream": "https://kenari.id/v1/chat/completions"
|
||||
}
|
||||
},
|
||||
"kie": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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/<model>` 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", () => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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/<family>.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<string, object>).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", () => {
|
||||
|
||||
Reference in New Issue
Block a user