* refactor(sse): extract combo dispatch prelude into combo/dispatchPrelude.ts
Pure move, no behaviour change. First of ~7 PRs decomposing the combo.ts
god-file (#3501).
handleComboChat evaluates a series of dispatch branches before it ever
reaches target resolution or the sequential attempt loop. None of them
iterate targets in priority order or need the failover/retry/credential
gate machinery that follows, so they move to a leaf:
- context-cache pin routing (Fix#679), including the
pinIsDurablyUnhealthy / isPinnedModelDurablyUnhealthy health gate
- fusion panel dispatch + the #6455 misconfiguration warn
- pipeline chaining
- nested combo-ref execute-mode runtime-unit dispatch
Only the chaos and round-robin hand-offs stay inline (11 and 13 lines);
extracting those would be pure indirection.
open-sse/services/combo.ts 3642 -> 3341 (-301)
open-sse/services/combo/dispatchPrelude.ts: 619 (under the 800 cap)
Each helper keeps the fall-through protocol the inline blocks had: return
a Response to OWN the request, return null to fall through. A flipped
null/Response would silently bypass the whole combo strategy, so the new
tests pin both directions for every branch.
combo.ts re-exports pinIsDurablyUnhealthy so combo-pin-health-gate.test.ts
keeps resolving. The leaf takes handleComboChat as a `runCombo` parameter
instead of importing it, so combo/ keeps zero back-edges into combo.ts.
Complexity-neutral: the first cut added +3 violations (two
max-lines-per-function, one complexity) inside the new leaf, so
evaluatePinnedResponse, orderRuntimeUnits, recordRuntimeUnitStickySuccess
and buildBaseOptions were split out. check:complexity now measures 2169
and check:cognitive-complexity 956 — identical to the pristine base.
* test(sse): close the dispatch-prelude coverage holes found by mutation testing
An adversarial mutation audit of the suite added in the previous commit
found it guarded the fall-through protocol well but asserted almost
nothing about what the helpers do once they OWN the request. 5 of 12
seeded mutations survived. Worst case: deleting the pinned-model
dispatch call outright left all 12 tests green.
Three holes, now closed (8 tests -> 20):
Hole A — the honored-pin path had zero coverage. Both existing pin tests
DROP the pin, so the dispatch, the 200-but-empty quality gate, the
[408, 429, 500, 502, 503, 504] failover list and the catch(pinErr)
branch were unguarded — exactly the logic the 2026-06-21 / 2026-06-22
incident comments call load-bearing. Adds five tests over a seeded
healthy provider connection so the pin is actually honored.
Hole B — orderRuntimeUnits was only ever driven with `priority`, which
is a no-op through it. Four of five strategy branches could be deleted
with nothing failing. Adds round-robin rotation and weighted sticky
ordering tests.
Hole C — recordRuntimeUnitStickySuccess never did anything under test:
both its guards need weighted/round-robin, so an early return changed
nothing. Covered by the new sticky-batch test.
Verified by re-running the mutations rather than assuming: all 7 that
previously survived (delete-pin-dispatch, serve-despite-failed-quality,
never-fail-over-on-transient, rr-counter-not-advanced, rotation-removed,
weighted-sticky-skipped, sticky-recording-no-op) are now killed.
The first sticky-batch test I wrote was itself vacuous — asserting "same
unit twice" holds equally when the recording helper is stubbed out, since
nothing advances the counter either. It now asserts the batch runs out
and rotation resumes on the third dispatch, which is what actually
distinguishes the two.
Also restores API_KEY_SECRET in test.after; it was set at module load and
never put back, inconsistent with the DATA_DIR handling beside it.
* fix(ci): teach known-symbols gate the relocated fusion/pipeline dispatch
The combo sub-check of check:known-symbols asserts every canonical routing
strategy has a real dispatch branch. It scanned a hardcoded file list and
matched only `strategy === "..."`, so the prelude extraction tripped it twice:
[combo] 2 estratégia(s) canônica(s) sem branch de despacho em combo.ts:
✗ fusion
✗ pipeline
Both branches are still wired — they just moved to combo/dispatchPrelude.ts and
took the early-return guard form `if (strategy !== "fusion") return null;` that
extracting a branch into a `tryXDispatch()` leaf naturally produces.
Two changes, both extending existing precedent (the list already carries the
Block J leaves for the same reason):
- register combo/dispatchPrelude.ts in comboDispatchFiles
- widen the extractor to `strategy [!=]== "..."` so the inverted guard counts
Loose `==`/`!=` stay rejected, and no `handledNotCanonical` fallout: the gate
now reports 20 canonical strategies, all 20 via despacho.
* chore(ci): register combo-dispatch-prelude test in stryker tap.testFiles
check:mutation-test-coverage --strict failed once the known-symbols fix let
Fast Quality Gates advance to it:
✗ 2 covering unit test(s) across 2 module(s) are missing from
stryker.conf.json tap.testFiles
open-sse/services/combo/comboStructure.ts
open-sse/services/combo/rrState.ts
The new tests/unit/combo-dispatch-prelude.test.ts exercises both modules, and
both are already in stryker's mutate list, so without the registration its
mutant kills would not have counted toward the nightly mutation gate.
Note (unchanged, still out of scope): combo/dispatchPrelude.ts itself is not in
stryker's `mutate` list. Adding it would widen the nightly mutation surface,
which is a separate call from fixing this drift.
* docs(changelog): add fragment for #8582 combo dispatch prelude
Closes the gap that let #8463 ship. `oauthModal.googleOAuthWarning`'s English value
was rewritten when the Antigravity login helper landed (#5203); 39 of 43 locales kept a
translation of the PREVIOUS English, which told operators to "copy the full URL and
paste it below" — a flow that cannot complete for that provider family. Non-English
users read confident, wrong instructions for months and no gate noticed.
None of the three existing gates can see this class:
- `sync-ui-keys.mjs` only backfills keys that are ABSENT, never ones that are STALE;
- `check-ui-keys-coverage.mjs` counts key PRESENCE, so a stale translation scores as
fully covered (all 43 locales sat at 99.6% throughout);
- `check-translation-drift.mjs` tracks the `docs/i18n/<locale>/**.md` documentation
mirrors — it never reads `src/i18n/messages/*.json` at all. (Its `.i18n-state.json` is
also absent, so it self-skips, but bootstrapping it would not have helped: wrong
surface.)
New gate `scripts/i18n/check-ui-value-drift.mjs` is DIFF-AWARE rather than
baseline-backed: it compares `en.json` at the merge base against the working tree, and
for every key whose English value changed, reports any locale still holding an untouched
translation.
That choice deliberately freezes pre-existing debt — a diff cannot reveal which old
English a long-standing translation came from, so the gate judges only what the current
change touches, and unrelated PRs never pay for historical drift. The alternative, a
per-key hash baseline over 11207 keys, would have cost a ~600 KB generated file (3x the
largest existing baseline) churning on every i18n PR.
Two ways to satisfy it: refresh the translations, or set them to
`__MISSING__:<new english>` so the runtime serves the corrected English (#7258) while
the key queues for translation. When the string's MEANING changes, renaming the key is
better still — a new key cannot inherit a stale translation, which is what #8463 did.
Wired blocking into the `i18n-ui-coverage` job (the `i18n` job is
`continue-on-error: true`, so a gate there could not block anything). That job gains
`fetch-depth: 0` because the gate needs the base ref; without it the gate self-skips with
`base-unresolved`, mirroring `check-openapi-breaking`. `BASE_REF` is passed via `env:`
and reaches git only through `execFileSync` argv — never a shell string.
Verified against the real defect: rewriting an English value with translations left
behind reports exactly 39 stale locales and exits 1; `--warn` exits 0; an unresolvable
base exits 0 with `SKIP reason=base-unresolved`.
Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
synthOpenAIErrorChunk() sets error.type to upstream_empty_response but omits
error.code. The isRequestScopedUpstreamFailure guard only checks type for
context_length_exceeded, so an upstream_empty_response error slips past it.
- Added code: upstream_empty_response to the error object in synthOpenAIErrorChunk()
- Added test assertion verifying error.code matches error.type
This ensures the guard correctly identifies upstream empty responses as
request-scoped failures.
Closes#8469
Passes through request.additionalModelRequestFields in openAIToBedrockConverse so reasoning/thinking options passed from Kiro/Bedrock translators are retained in the AWS Converse payload.
Adds @cf/qwen/qwen2.5-coder-32b-instruct, @cf/meta/llama-3.3-70b-instruct-fp8-fast, @cf/meta/llama-3.2-3b-instruct, @cf/qwen/qwq-32b, @cf/zai-org/glm-4.7-flash, @cf/moonshotai/kimi-k2.6, and @cf/google/gemma-4-26b-a4b-it to freeModelCatalog.data.ts to match the provider registry.
* fix(plugins): delete the host script synchronously and stop two tests leaking child processes
Three related leaks in the plugin child-process lifecycle, found while tracing 27 node
processes on a developer machine.
1. loader.ts removed the generated omniroute-plugin-host-*.mjs with a fire-and-forget
`rm(...).catch(() => {})`. That unlink loses the race against process exit: test:unit
runs with --test-force-exit, which tears the process down before the promise settles,
so every plugin load leaked one temp .mjs into TMPDIR. Measured at 6 files per
full-suite run, 40 accumulated over a handful of local runs. rmSync closes the race;
the throw stays swallowed because an exception raised from a child "exit" handler
would take the server down, and a leftover temp script would not.
2. plugins-manager-lifecycle.test.ts "activates an installed plugin" called activate() --
which spawns the plugin's child process -- but never deactivate(). deactivate() is the
only path that reaches the loader's cleanup(), so the child outlived the test and its
IPC channel kept the test process's event loop alive.
3. plugins-manager-restart-reload-7806.test.ts simulateRestart() deleted the entry from
loadedPlugins without calling cleanup(), dropping the only handle that can kill child
#1. The reload then spawned child #2, and the finally block's deactivate() could reach
only child #2 -- one dangling child per test. A real restart takes the whole process
tree down, so calling cleanup() here is both the faithful simulation and the fix.
Combined effect: a run without --test-force-exit deadlocks. The test process cannot exit
while its child holds the IPC channel open, and the child waits for messages that never
come. Observed as three plugin hosts alive for 4h51m under a runner that never finished.
Validation (Hard Rule #18, TDD): the new plugins-loader.test.ts case fails against the old
async unlink ("must delete the host script synchronously, not on a later tick") and passes
with rmSync. It redirects TMPDIR/TEMP/TMP to a private directory before counting, because
test:unit runs at --test-concurrency=20 and a concurrent file's host scripts would
otherwise land in the counted directory and flake the assertion.
After: 19/19 pass across the three files, 0 temp scripts created, 0 orphan processes.
tests/unit/build/** 334/334; typecheck:core and eslint clean.
* docs(changelog): add fragment for plugin host script sync delete
An `auto/*` combo whose first step lands on an uncredentialed backend returns
HTTP 200 with `finish_reason: "stop"`, `content: null` and `error: null`. The
agent sees a clean empty assistant turn, has no error to stop on, and retries to
its cap.
The non-streaming path already refuses this: `isEmptyContentResponse` rewrites a
200-with-no-content into a 502 "Provider returned empty content", which the combo
layer classifies as a model-level transient and fails over on (#5085). The
streaming path had no equivalent, and neither existing guard covers it:
- `ensureStreamReadiness` is a LIVENESS probe, not a content one. Its failure
message says so — "Stream ended before producing a non-ping SSE event" — and
`hasStreamReadinessSignal` returns true for a bare `delta:{"role":"assistant"}`.
- `createDisconnectAwareStream`'s #7699 branch fires on a MISSING terminal marker
and is scoped to the Claude client format, because for other formats a
marker-less close is genuinely ambiguous.
The reported stream trips neither: OpenAI format, terminates with
`finish_reason: "stop"` and `[DONE]`, contains nothing.
"Completed normally but emitted zero content" is not ambiguous the way a missing
marker is, so this guard is format-agnostic. It reuses `hasUsefulStreamContent`,
which already existed in streamReadiness.ts — exported, correct, and wired to
nothing — and which already counts tool-call-only and reasoning-only output as
real (#2520). A watcher wraps it to handle frames split across network chunks and
to spot the terminal states where emptiness is legitimate, kept in step with
errorClassifier.ts's `LEGIT_EMPTY_OPENAI_FINISH` / `LEGIT_EMPTY_CLAUDE_STOP`:
length, tool_calls, content_filter, max_tokens, tool_use.
Two guards keep it from over-firing, one of which caught a real regression while
building this: the check applies only when bytes were forwarded, and only when
the body actually looked like SSE. A plain JSON completion travels through the
same wrapper and has no `data:` frames, so "no content seen" says nothing about
it — without the SSE gate, four existing stream tests failed.
The `if (done)` branch's reasoning moved into `resolveSilentCloseReason()`, which
also drops `pull` back under the function-length ceiling; cyclomatic lands at
2187 against a baseline of 2188.
This surfaces the error rather than failing over. Failing over would mean holding
every stream until its first content token, since the combo has already returned
leg 1's response by then — a much larger change. Surfacing the error satisfies
the issue's stated expectation ("surface the upstream error OR fail over") and
stops the retry loop, which is the reported harm.
Closes#8649
Previously checked directly, ignoring database feature flag overrides configured via settings/UI. This update routes the check through so DB overrides are respected as intended.
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
* fix(db): preserve node sqlite open semantics
* test(db): keep node sqlite coverage off Bun
* chore(changelog): number native sqlite fallback
* test(db): register node sqlite tests only under Node
- KimiSponsorBanner/RiskNoticeBanner: read the localStorage dismissal flag
via useSyncExternalStore (server snapshot = visible) instead of a
useState lazy initializer, so the first client render matches SSR
(which has no localStorage) instead of diverging on hydration.
- usage/analytics route: key the per-model aggregation map by model name
alone instead of `${provider}::${model}`, matching the table's one-row-
per-model display and eliminating duplicate `key={m.model}` rows when a
model is served through multiple provider connections/accounts.
- CommandPalette: look up existing section/subgroup by id across the whole
list instead of only comparing to the previous item, so sections whose
children interleave root items and groups (e.g. omni-proxy) don't produce
two subgroups sharing the same "_root" key.
- Add the missing `compressionOutputStyle.ponytail` label/description to
all 43 locale message files (present in the style catalog but never
added to any locale, causing a MISSING_MESSAGE crash).
Co-authored-by: Gillz <gillz@Gillzs-MacBook-Pro.local>
parseModel can return provider:null when a combo target modelStr lacks a
provider/ prefix; ResolvedComboTarget already carries provider, so use it
before getTokenLimit to avoid null.toUpperCase() during compression.
* fix(oauth): show GitLab Duo setup before authorize error
Surface the OAuth app registration and env-var recipe in the Add
Connection modal before auto-starting authorize, and keep the same
shared copy for catalog authHint and the authorize fallback (#8688).
* fix(oauth): keep OAuthModal under file-size baseline for #8688
Extract waiting/error panels so the GitLab Duo setup step does not
trip the Fast Quality Gates file-size ratchet, and update the retry
Button regression guard for the extracted error step.
* docs(codex): document session affinity and stream idle for long tasks
Operators running multi-hour Codex sessions need both knobs spelled out:
sessionAffinityTtlMs (default off) and STREAM_IDLE_TIMEOUT_MS (10 min),
with a concrete recipe and an explicit keep-defaults decision (#7287).
* fix(ci): keep #7287 docs-only so base-red gates stay skipped
Drop the unit content-guard that classified the PR as code (triggering
i18n/unit/eslint/dast on a red release tip). Sync agent-skills so
check:agent-skills-sync passes (config-codex-cli blank line + stale
cli-backup-sync catalog drift).
* fix(sse): stop thrashing the provider prompt cache for caching-aware clients
- shouldPreserveCacheControl: preserve client cache_control markers for every
combo strategy. The deterministic-strategy gate forced marker rewrites whose
per-request breakpoint positions are not stable turn-over-turn, thrashing the
upstream prompt cache (observed in production as ~200k cache_write tokens per
turn on quota-share combos). Preserving is never worse: on a stable target
the client's breakpoints advance deterministically; on a target switch both
approaches miss equally.
- prepareClaudeRequest: translator-path opt-in fallback — when preserve-mode
has nothing to preserve (client sent no cache_control anywhere), apply the
standard heuristic so requests never ship with zero cache breakpoints. The
claude-code-compatible relay path keeps its no-supplement contract.
- anthropic-beta: forward the client-negotiated context-1m-2025-08-07 through
the allowlist merge so a /model <id>[1m] client keeps its long-context
negotiation behind the proxy (never forced when the client did not send it).
* fix(sse): surface cache tokens in non-streaming OpenAI usage + finalize pending on quota-share block
- translateNonStreamingResponse (claude→openai): fold cache_read into
prompt_tokens and expose prompt_tokens_details.cached_tokens /
cache_creation_tokens, mirroring the streaming contract (#1426/#2215).
Non-streaming OpenAI clients behind a cached Claude upstream previously saw
prompt_tokens=<uncached remainder> (e.g. 23 for a ~9k request) with no cache
visibility.
- chatCore quota-share block: finalize the pending-request slot before
returning the policy 429 — the path never reaches upstream and the orphaned
pending lingered as a status-0 call-log row until the reaper swept it.
Validated live on the staging box (repro: blocked key → orphan row; after:
clean).
---------
Co-authored-by: diegosouzapw <diegosouzapw24@gmail.com>
Monster API shuttered operations on 2026-06-30. Mark monsterapi entry as isDeprecated with deprecationReason in INFERENCE_HOSTS catalog.
Co-authored-by: Austin Liu <austinliu@Austins-MacBook-Air-3.local>
Six declarations spell a byte buffer as bare `Buffer` / `Uint8Array`. Without
its type argument that widens to `ArrayBufferLike`, which also admits
`SharedArrayBuffer` — so the value is rejected at every Web API boundary it is
actually passed to: `BodyInit` for `new Response(...)` and `BlobPart` for
`new Blob([...])`.
Every one of them is already ArrayBuffer-backed at runtime. `hexToBytes()`
allocates with `new Uint8Array(len)`; `synthesizeGtts()` and `pcmToWav()` return
`Buffer.concat(...)`; `fetchRemoteImage()` returns
`Buffer.from(await response.arrayBuffer())`; `readPageResponseBody()` returns
`Buffer.from(body)`, which copies. The declarations were simply less specific
than the values, so this states what the code already guarantees.
Same fix#8533 applied to the multipart and gRPC-web bodies.
Fixes 5 of the 208 `tsc -p open-sse/tsconfig.json` diagnostics with no new ones:
3 in audioSpeech.ts (MiniMax hex, gTTS, Vertex Gemini TTS), 1 in
imageGeneration.ts (Topaz Blob upload) and 1 in browserBackedChat.ts.
Refs #8484
Sort static fitness table patterns by descending length before matching. This prevents shorter substrings like 'gpt-4o' from incorrectly matching longer model IDs like 'gpt-4o-mini' when 'gpt-4o' happens to be listed earlier in JavaScript Object key iteration order.
Co-authored-by: Austin Liu <austinliu@Austins-MacBook-Air-3.local>
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
When looking up models_dev_tier or capability scores for -free alias models (e.g. deepseek-v4-flash-free), strip the -free suffix to query the underlying model's intelligence data if direct lookup yields null.
Co-authored-by: Austin Liu <austinliu@Austins-MacBook-Air-3.local>
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
`kieExecutor.createTask()` returns `JsonObject` (`Record<string, unknown>`), so
`createData.data` is `unknown` and the `createData?.data?.taskId` read that
image, video and music generation each duplicated could not compile. The same
three-line expression appeared verbatim in all three handlers.
`open-sse/utils/kieTask.ts` already holds two helpers with exactly this shape —
`normalizeKieTaskState()` and `parseKieResultJson()` both take `unknown`, guard
with `isJsonObject()` and return a declared type. `getKieTaskId()` follows them,
so the three handlers now share one guarded read instead of three unguarded ones.
`getKieCallbackUrl()` took `KieCallbackBody`, a weak type (all properties
optional). Passing a request body whose declared keys are `prompt` /
`timeout_ms` / `poll_interval_ms` tripped TS2559 "no properties in common" at
both music call sites. It receives arbitrary upstream request bodies, so it now
takes `unknown` and guards the same way its neighbours do; `KieCallbackBody`
had no other reference and is gone.
Behaviour is unchanged. `isJsonObject()` rejects arrays and null exactly where
optional chaining already yielded `undefined`, and the callers' `String(taskId)`
coercion moved inside the helper, so a numeric id still reaches `pollTask()` as
a string and a falsy id still takes the 502 branch.
Fixes 5 of the 208 `tsc -p open-sse/tsconfig.json` diagnostics with no new ones:
3 x TS2339 `taskId` on `unknown`, 2 x TS2559 on `KieCallbackBody`.
Refs #8484
Consolidates PR #8660 (matchesAnyToken with full-match priority over
token-level OR fallback) with the existing Turkish search normalization
shipped on release/v3.8.49.
The PR adds a new matchesAnyToken helper used by the providers search
to allow space-separated queries (e.g. 'pollinations sambanova') to
match when any of the tokens is present. Full query match takes
priority so that an exact 'pollinations sambanova' query still hits
even if a partial token would have been ambiguous.
The unit test file gains 12 PR-side matchesAnyToken tests on top of
the 6 release-side Turkish-normalization tests covering the same
function, totaling 27 tests.
Co-authored-by: maxmad64bis <maxmad64bis@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
buildUnifiedModelsResponseCore builds the entire catalog first — every provider's
models, the auto/* combos with their candidate scoring, the embedding/image/rerank/
audio/moderation/video/music registries, the OpenRouter catalog, custom models —
and only at the very end, once the caller turns out to be scoped to a quota pool
(allowedQuotas non-empty), discards all of it and returns the pool's qtSd/* combos
instead.
Measured on a 1 vCPU shared-quota host: ~1.2s of CPU per cold build to return 10
models, and 4.4-7.1s under contention. Claude Code's gateway model discovery aborts
at 3s, so on a busy host the discovery silently falls back.
Everything the quota path needs (`combos`, `timestamp`, `buildComboCatalogMetadata`)
already exists before the expensive loops start, so resolve the key metadata there
and return early.
Extracts two helpers into ./catalogResponse so the short-circuit and the full build
share one implementation instead of two copies that drift:
- applyCatalogPostFilters — the chain that runs AFTER the API-key filter
(configuredOnly, claude effort variants, no-thinking variants, cc-discovery
mirrors, synced effort variants, dedupe)
- finalizeCatalogResponse — enrichment + the codex `models: []` compatibility
field + the response envelope
That chain is not optional for the quota path: the cc-discovery mirrors are exactly
what lets Claude Code see a quota pool's models, and a first draft of this change
dropped them by returning before it. The regression is now pinned by a test that
was verified to fail without the call.
catalog.ts 1615 -> 1480 lines.
TDD: tests/unit/quota-exclusive-catalog-short-circuit.test.ts uses the OpenRouter
catalog fetch as the observable — it is part of the full build and reaches the
network, so a request that performs it did the whole build. The quota request runs
first, while OpenRouter's 24h cache is still cold, otherwise a cached second call
would make the assertion vacuous. Red before (1 fetch), green after (0), and a
normal key still fetches.
Behaviour guards green: quota-exclusive-catalog-4806 (2), quota-key-models-route
(18), cc-discovery-aliases-catalog (9), catalog-helpers-extraction (12),
cc-compatible-model-catalog (1), instrumentation-warm-catalog-cache (3),
catalog-pricing-surface-8018 (3), apikeypolicy-quota-only (6). typecheck, lint and
the file-size gate clean.
Two defects shipped with #8770, both red on release/v3.8.49.
1. `OMNIROUTE_EXTRA_MIGRATIONS_DIRS` was documented in ENVIRONMENT.md but never
added to .env.example, so the env/docs contract gate and its two tests
(check-env-doc-sync, issue-7793-env-doc-sync-repro) failed. Added, with the
same explanation the docs carry.
2. tests/unit/db-migration-runner-extra-dirs.test.ts passed locally and failed on
CI, for two reasons that only appear under the CI invocation:
- It re-imported migrationRunner.ts under a cache-busting query string to pick
up a fresh `MIGRATIONS_DIR` per test. That is not reliable under the loader
chain CI uses (`--import tsx/esm --import setupPolyfill --import
isolateDataDir`): the second test got a runner still pointing at the REAL
migrations directory and tried to apply migration 127 to an empty in-memory
DB. The core directory is now fixed once, before the first import; only the
extra directories vary per test, and those are resolved at call time by
design.
- Tests were registered with top-level `await`. With synchronous bodies that
drains the event loop between tests and `--test-force-exit` — used by every
CI test script — cancels the remainder of the file.
Both constraints are now written down in the file header so the next edit does
not reintroduce them.
Validated with the exact CI invocation, not the bare runner: 11/11, 0 cancelled.
Neighbouring suites green under the same flags: db-migration-runner (26),
check-migration-numbering (15), db-migrationrunner-constants-split (7),
db-migration-version-uniqueness (2), check-env-doc-sync (13),
issue-7793-env-doc-sync-repro (1).
The runner reads exactly one directory and records the bare numeric prefix as the
version, so the numeric slots are a single global namespace. Any distribution that
ships its own migrations next to the upstream set has to draw from that same range
while upstream keeps appending to it — and when both sides claim a number, the
runner records one name for it and treats the other as already applied. That
migration then never runs, silently, on every already-provisioned database.
OMNIROUTE_EXTRA_MIGRATIONS_DIRS registers additional directories as
`namespace=dir` entries separated by the platform path delimiter. Files found
there are recorded as `<namespace>-<number>` (e.g. `ee-134`), a version space that
cannot collide with the upstream numeric one, and they are applied after the core
set. Unset — the default, and the only case for a plain install — nothing changes:
no filesystem access, identical behaviour.
Misconfiguration throws instead of being skipped. A malformed entry, a namespace
outside [a-z][a-z0-9]*, a duplicate namespace, or a directory that does not exist
aborts startup, because silently missing schema is the exact failure this exists to
prevent. Two files sharing a number inside the SAME namespace still collide and
throw, mirroring the runner's own guard.
Also fixes an inconsistency the tests surfaced: a missing core directory returned
early and took the extra directories down with it. They are an independent set.
The version-namespaced strings need no further plumbing — the applied set, the gap
reconciliation and the name-mismatch check all key on the version string, and the
numeric-only paths (`Number.parseInt`, the "032"/"041"/"042" special cases,
`isSchemaAlreadyApplied`) ignore them by construction.
11 new tests; the 7 neighbouring migration suites stay green (62 tests).
Squash merge from local merge-train (Hard Rule owner-approved). Tip 029cdf4215cf465f0e1716ac9f84a84692b1e881 validated on 192.168.0.113: 26631/26653 pass.
Squash merge from local merge-train (Hard Rule owner-approved). Tip 029cdf4215cf465f0e1716ac9f84a84692b1e881 validated on 192.168.0.113: 26631/26653 pass.
Squash merge from local merge-train (Hard Rule owner-approved). Tip 029cdf4215cf465f0e1716ac9f84a84692b1e881 validated on 192.168.0.113: 26631/26653 pass.
Squash merge from local merge-train (Hard Rule owner-approved). Tip 029cdf4215cf465f0e1716ac9f84a84692b1e881 validated on 192.168.0.113: 26631/26653 pass.
Squash merge from local merge-train (Hard Rule owner-approved). Tip 029cdf4215cf465f0e1716ac9f84a84692b1e881 validated on 192.168.0.113: 26631/26653 pass.
Squash merge from local merge-train (Hard Rule owner-approved). Tip 029cdf4215cf465f0e1716ac9f84a84692b1e881 validated on 192.168.0.113: 26631/26653 pass.
Squash merge from local merge-train (Hard Rule owner-approved). Tip 029cdf4215cf465f0e1716ac9f84a84692b1e881 validated on 192.168.0.113: 26631/26653 pass.
Squash merge from local merge-train (Hard Rule owner-approved). Tip 029cdf4215cf465f0e1716ac9f84a84692b1e881 validated on 192.168.0.113: 26631/26653 pass.
Squash merge from local merge-train (Hard Rule owner-approved). Tip 029cdf4215cf465f0e1716ac9f84a84692b1e881 validated on 192.168.0.113: 26631/26653 pass.
Squash merge from local merge-train (Hard Rule owner-approved). Tip 029cdf4215cf465f0e1716ac9f84a84692b1e881 validated on 192.168.0.113: 26631/26653 pass.
Squash merge from local merge-train (Hard Rule owner-approved). Tip 029cdf4215cf465f0e1716ac9f84a84692b1e881 validated on 192.168.0.113: 26631/26653 pass.
Squash merge from local merge-train (Hard Rule owner-approved). Tip 029cdf4215cf465f0e1716ac9f84a84692b1e881 validated on 192.168.0.113: 26631/26653 pass.
Squash merge from local merge-train (Hard Rule owner-approved). Tip 029cdf4215cf465f0e1716ac9f84a84692b1e881 validated on 192.168.0.113: 26631/26653 pass.
Squash merge from local merge-train (Hard Rule owner-approved). Tip 029cdf4215cf465f0e1716ac9f84a84692b1e881 validated on 192.168.0.113: 26631/26653 pass.