* Fix context-window exhaustion classification
* fix(combo): keep chat.ts/comboStructure.ts under the file-size ratchet + fix context-overflow boundary bug
- Extract getKnownContextOverflow (+ its KnownContextOverflow type) out of
comboStructure.ts into a new open-sse/services/combo/knownContextOverflow.ts
leaf, so the file-size ratchet (cap 800 for new files) passes.
- Extract the skipConnectionDisable predicate out of handleSingleModelChat in
chat.ts into open-sse/services/combo/comboPredicates.ts::shouldSkipConnDisable,
and consolidate the new combo-failure-handling imports, to keep chat.ts under
its frozen file-size baseline (1796) after the #7177 request-scoped-failure
wiring.
- Fix a real boundary bug in getKnownContextOverflow surfaced by the merge:
estimateRequestInputTokens counted a caller-omitted `messages: []` (which some
combo entrypoints default in) as real content, charging a few phantom
"structural" JSON.stringify tokens toward the estimate. That was enough to
falsely trip the new known-context-overflow rejection for a request with no
real input when max_tokens exactly equals the target's context window (a
common config where limit_input === limit_output === limit_context),
regressing tests/unit/combo-routing-engine.test.ts's pre-existing #3587
"non-reasoning model does not get max_tokens buffer" case. Empty
arrays/objects no longer count as estimable content.
- Add a regression test for the exact-boundary empty-content case.
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
* refactor(combo): move overflow logic into knownContextOverflow module (file-size cap)
comboStructure.ts is not frozen in the file-size baseline but is capped at 800
lines; this PR's net +29 on that file alone would push it over once merged.
knownContextOverflow.ts already exists in this PR as the dedicated home for
"known context limit" logic, so move the genuinely new pieces there instead
of leaving them in comboStructure.ts:
- hasEstimableContent (new): its own doc comment already frames it purely in
terms of the known-context-overflow boundary check, so it belongs next to
that check, not in the general request-compatibility file.
- getKnownContextLimit (new, requestedOutputTokens-aware): this *is* the
"how big is a target's known context window" primitive knownContextOverflow
already consumes; hosting it there is a better fit than comboStructure.ts.
- getLegacyKnownContextLimit: kept alongside its sibling rather than split
across two files, since both are alternate implementations of the same
concept (used only by comboStructure.ts's hasKnownCompatibleContextLimit).
comboStructure.ts now imports all three back for its own internal callers
(estimateRequestInputTokens, getTargetCompatibilityFailures,
hasKnownCompatibleContextLimit). deriveRequestCompatibilityRequirements and
the RequestCompatibilityRequirements type stay in comboStructure.ts exactly
as this PR already has them (still consumed internally there), so
knownContextOverflow.ts keeps importing those two, same as before.
No behavior change — pure relocation, verified by the existing PR test
suite (combo-context-window-filter, combo-breaker-429, combo-failure-log-message,
combo-target-exhaustion, diagnostics) plus the pre-existing
combo-vision-aware-routing/combo-context-requirements/combo-roundrobin-compat-fallback-6238
suites, all green.
Net effect on open-sse/services/combo/comboStructure.ts vs. this PR's merge
base: -2 lines (was +29). typecheck:core, lint, and the complexity ratchets
(check:complexity, check:cognitive-complexity) are unchanged from this PR's
current HEAD — the 4 pre-existing complexity/max-lines findings in
valueContainsImagePart/filterTargetsByRequestCompatibility are untouched by
this move (same violations, same total ratchet counts, just shifted line
numbers).
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(combo): derive session stickiness key from Responses API .input, not just .messages (#7270)
* fix(combo): map bare-string .input array items in stickiness key derivation (#7270)
normalizeStickinessMessages()'s Array.isArray(input) branch cast the array
straight through, so a Responses-API `.input` array of PLAIN STRINGS (each
string shorthand for a user message) never matched deriveMessageHash's
`role === "user"` lookup and the key stayed null — the same fail-open bug
#7270 fixed, just for this narrower wire shape. Map bare-string items to
{role: "user", content: item}, mirroring the string-item handling already
established in responsesInputNormalization.ts's normalizeCodexResponsesInputItem.
Adds a regression test case (unit-level normalizeStickinessMessages assertion
+ round-robin re-pin case) so the fix's own suite covers this shape.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* feat(api): route Google AI Studio Imagen through /v1/images/generations
gemini/imagen-4.0-* models were advertised in /v1/models (surfaced live via
Google ListModels) but were unroutable on /v1/images/generations: `gemini` was
not in the image registry, so the route rejected them with "Invalid image
model". They also 404 on the chat route because Imagen uses the dedicated
:predict endpoint, not generateContent.
Wire a `gemini` image provider (format "google-imagen") that POSTs to
{baseUrl}/{model}:predict with x-goog-api-key, sends the instances/parameters
body, and normalizes predictions[].bytesBase64Encoded into the OpenAI image
shape. Only imagen-* models dispatch here (isImagenModel guard) — gemini
flash-image / nano-banana keep routing through /v1/chat/completions.
Note: Imagen requires a billing-enabled Google project; free-tier keys get
403 / quota 0. Request-builder and response-parser are pure and unit-tested;
the live Google call needs a paid key to exercise.
Tests: tests/unit/gemini-imagen-predict.test.ts (7 cases: registry wiring,
parseImageModel resolution, isImagenModel guard, predict body shape +
sampleCount clamp + aspectRatio, response normalization + empty tolerance).
* refactor(images): extract Google Imagen entries to registry module (file-size cap)
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Both device-code providers failed at the upstream request (surfacing in the
companion extension as 'ошибка сервера OmniRoute'). Diagnosed by calling the
upstream endpoints directly:
- qwen: QWEN_CONFIG used the bare host qwen.ai, whose /api/v1/oauth2/device/code
and /token paths return 404 Not Found. The working qwen-code device flow lives
at chat.qwen.ai (verified: 200 + a valid device_code/user_code). Point both
URLs at chat.qwen.ai.
- codebuddy-cn: the Tencent state endpoint reads 'platform' from the QUERY
string, not the JSON body. Sending it only in the body returned
400 {"code":10001,"msg":"platform is empty"}. Passing ?platform=CLI
returned 200 with {code:0, data:{state, authUrl}}. Build the stateUrl with the
platform query param (body kept as-is).
Validation: live upstream calls returned 200 for both corrected requests (device
flow can't be hit from CI). Regression guard: tests/unit/oauth-device-code-endpoints.test.ts.
The dynamic OAuth GET handler swallowed every thrown error into a generic
`{ error: "Internal server error" }` 500, so a device-code upstream failure
(qwen → qwen.ai, codebuddy-cn → copilot.tencent.com — geo-block / outage / bad
client) surfaced in the extension as an indistinguishable 'ошибка сервера
OmniRoute', hiding WHY it failed.
Route the caught error through sanitizeErrorMessage() (already imported, hard
rule #12) so the real reason ("Device code request failed: …", "CodeBuddy
state request failed (403)") reaches the client, falling back to the generic
only when the sanitizer yields nothing.
Regression guard: tests/unit/oauth-device-code-error-transparency.test.ts
(source-level — the route needs the full Next request/auth/upstream graph, so
behavioural validation belongs on a real build/VPS).
Eleven subsystems answered "am I running under a test runner?" with
`process.argv.some((arg) => arg.includes("test"))`. JavaScript agrees that
'latest'.includes('test') is true, so ANY argv carrying a `latest` segment — a release symlink
like /opt/omniroute/latest/server.js, an npm/npx cache path, a `--model=latest` flag — silently
put the process into test mode.
The worst consequence is src/lib/db/backup.ts: isSqliteAutoBackupDisabled() returns true, so
SQLite auto-backup simply never runs — no warning, no log. The same substring decides whether
migrationRunner runs its pending-migration check, whether cloud sync initialises, and whether
the local/token health checks, quota recovery, model-lockout settings and the WS live server
consider themselves live. All of them fail silent, which is the dangerous kind.
Replaces the eleven copies with one helper, src/shared/utils/testProcess.ts:
- env first (NODE_ENV=test, VITEST) — unchanged;
- argv: `test`/`tests` only as a WHOLE token delimited by a path separator, dot or dash, so
`--test`, `tests/unit/x.test.ts` and `src/x.test.ts` still match, while `latest`, `protest`,
`contest` and `attestation` no longer do;
- argv: runner binaries (vitest/jest/mocha/ava/tap), which have no delimiter before "test";
- execArgv as well as argv — `node --test x.js` puts `--test` in execArgv, and
modelLockoutSettings was the only copy that remembered to look there.
argv/env are parameters rather than globals so the negative cases are testable: under a test
runner the globals always say "test", which is precisely why this bug could never be caught.
tests/unit/test-process-detection.test.ts guards the regression (a `latest` path is not a test
run) alongside the positives that must keep working.
* fix(providers): accept m365.cloud.microsoft for copilot-m365-web token (#7078)
* test: regression for #7078 m365.cloud.microsoft token extraction
* fix(7078): match on url.hostname and anchor path with startsWith
* test(7078): cover explicit :443 port via url.hostname
* fix(responses): map mid-conversation system turns to developer role (#6954)
* test(responses): add #6954 mid-conversation system -> developer regression
* fix(6954): keep bare-string content parts in buildResponsesTextParts
* test(6954): cover array-form system content with bare string
PR #7178 — The CSP was blocking the Cloudflare Web Analytics beacon
(static.cloudflareinsights.com). Both dev and prod script-src directives
need the domain for the analytics script to load.
Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
* perf: wrap ComboCard, HeroSection in React.memo
* fix(#7070): add test coverage for React.memo changes; fix selfref test in fork CI
- Add smoke tests for combos page and EvalsTab to satisfy PR Test Policy
requiring tests for production code changes
- Fix selfref test (check-test-masking-selfref-6634) to try upstream/main
first, falling back to origin/main, since origin/main may not exist in
fork CI environments
* fix(#7070): bump frozen baseline for combos/page.tsx 4655->4656 after React.memo wrapping
The file-size checker's split('\n').length convention now counts 4656
for src/app/(dashboard)/dashboard/combos/page.tsx after wrapping
ComboCard in React.memo (+1 effective line).
---------
Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
* feat(perf): add performance.mark/measure to SSE pipeline + request-size metric
- streamingPipeline.ts: mark/measure around assembly of SSE transform
chain — 'omni-pipeline-start'/'omni-pipeline-end'/'omni-pipeline'
- stream.ts: compute JSON body byte count on stream creation, emit as
performance.mark('omni-request-body-size', { detail: bytes })
Marks are visible via performance.getEntriesByType('mark') and
performance.getEntriesByType('measure') for DevTools/monitoring.
* fix(perf): prevent memory leak and TextEncoder allocation on hot path
- Add performance.clearMarks/clearMeasures before creating new marks to
prevent timeline accumulation in long-lived processes.
- Replace new TextEncoder().encode(str).length with Buffer.byteLength to
avoid allocating a full Uint8Array just to measure byte length.
* test(perf): add performance instrumentation tests
* chore(ci): rebaseline stream.ts 2796->2805 for perf instrumentation
Add _rebaseline_ entry documenting the +9 line growth from:
- b48ba21c4: performance.mark/measure instrumentation around SSE dispatch
- c35e8a9b4: TextEncoder hoisting fix
These are irreducible instrumentations at the stream dispatch chokepoint.
* chore: trigger CI re-run
* fix(ci): restore file-size-baseline.json corrupted by prior rebaseline commit
The rebaseline commit (9efdd636d) accidentally replaced the entire
config/quality/file-size-baseline.json with the literal string
"test content" instead of adding the intended stream.ts entry,
breaking JSON.parse() in check:file-size for every subsequent CI run.
Restore the full baseline from origin/release/v3.8.49 and apply the
intended bump: open-sse/utils/stream.ts 2796->2806 (measured LOC,
matching the script's countLines() split("\n").length, not wc -l)
for the performance.mark/measure instrumentation + TextEncoder
hoisting fix added by this PR.
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* fix(perf): clear the omni-request-body-size mark immediately after creation
Addresses review feedback on this PR: the fixed-name "omni-request-body-size"
performance mark was created on every createSSEStream() call and never
cleared, so it accumulated without bound in Node's global performance
timeline over a long-running server's lifetime (unlike the pipeline-assembly
marks in streamingPipeline.ts, which are bounded — cleared at the start of
the next call). A wired PerformanceObserver still receives the entry;
clearMarks() only removes it from getEntriesByName()/getEntriesByType().
Rebaseline file-size-baseline.json to the actual measured LOC (2796->2813)
for the comment + clear call, and add
tests/unit/stream-request-body-size-mark-7045.test.ts covering: the mark
fires with the correct JSON-byte-length detail, it does not accumulate
across repeated calls, and it is skipped when there is no request body.
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* fix(ci): correct file-size-baseline.json off-by-one for stream.ts
check-file-size.mjs counts lines via fs.readFileSync().split("\n").length,
which is wc -l + 1 for a file ending in a trailing newline (the last split
element is an empty string after the final newline). The previous commit
baselined the wc -l value (2813) instead of the script's own metric (2814),
so CI still failed by exactly 1 line.
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
---------
Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* fix(auggie): update model registry to match v0.32.0 CLI model IDs
All previous model IDs (claude-sonnet-4.6, claude-opus-4.6, gpt-5.5-high,
etc.) were synthetic — the actual IDs use a different
naming scheme (sonnet4.6, opus4.6, gpt5.5, etc.).
Replaced the static best-guess registry with the 31 real model IDs
from on v0.32.0, including:
- All Claude variants (fable-5, haiku4.5, sonnet4.x/5, opus4.x/5)
- Gemini 3.1 Pro Preview
- Full GPT-5.x family (gpt5 ~ gpt5.6-terra)
- GLM 5.2, Kimi K2.6/K2.7
- Prism composite routers (prism-a, prism-b)
Removed unused entries that don't exist in v0.32.0 (gemini-3.0-flash,
thinking variants, high/medium split IDs).
Updated unit tests to reference valid model IDs (haiku4.5, sonnet4.6, opus4.6).
* feat(auggie): auto-fetch model IDs on first execute()
* fix(auggie): move sonnet4.6 first in model list, remove duplicate
* fix(tests): update old claude-sonnet-4.6 model ID to sonnet4.6 in auggie test
The registry was updated to use sonnet4.6 but the test at line 352
still referenced the old model ID claude-sonnet-4.6, causing
resolveAuggieModel to reject it.
* test(autoCombo): account for auggie's new glm-5.2 model in auto/glm family test
The v0.32.0 auggie registry update in this PR adds a literal "glm-5.2"
model id. auggie is a no-auth candidate (always in the auto/<family>
pool per open-sse/services/autoCombo/virtualFactory.ts), and the family
filter matches by model-id pattern (open-sse/services/autoCombo/modelFamily.ts),
so it now legitimately joins auto/glm alongside the glm/zai connections —
same documented behavior the "degrades gracefully" test below already
covers for opencode/minimax. Updates the strict-equality assertion to
include it instead of narrowing the pool in production code.
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
* fix(auggie): add backward-compat alias map for v0.32.0 model IDs
Saved combos may reference old model IDs (claude-sonnet-4.6 → sonnet4.6,
gemini-3.1-pro → gemini-3.1-pro-preview, gpt-5.5-high → gpt5.5, etc).
The alias map in resolveAuggieModel() resolves these before the allowlist
check so existing combos continue working after the registry rename.
Refs: #7032
* fix(auggie): use Map.get() for the pre-v0.32.0 alias lookup + changelog
resolveAuggieModel() indexed AUGGIE_MODEL_ALIASES (a Map) with bracket
notation (AUGGIE_MODEL_ALIASES[requested]), which always returns undefined
for a Map instance — the alias branch never actually fired, so every
pre-v0.32.0 saved model id still hit "Unknown Auggie model" after the
v0.32.0 registry rename. Switch to .get(requested), the Map accessor.
Adds a red-first regression test (fails on the old bracket access, passes
with .get()) covering every old->new id pair in the alias map, and a
changelog.d fragment documenting the breaking model-id rename + the
alias fallback that keeps existing combos working.
Refs: #7032
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* perf(db): cap modelLockouts eviction at 1000 entries
- Add MODEL_LOCKOUT_EVICTION_CAP constant set to 1000
- Evict oldest entries in insertion order when cap exceeded
- modelFailureState eviction skips entries still in modelLockouts
- Prevents unbounded memory growth under sustained load
* test(db): add lockout eviction test, export helpers
- Extract evictModelLockoutOverflow() from ensureCleanupTimer for testability
- Add getModelLockoutSize() and export MODEL_LOCKOUT_EVICTION_CAP
- 3 tests: overflow eviction, under-cap idempotent, keeps recent entries
* fix(resilience): never evict a still-active model lockout in evictModelLockoutOverflow()
evictModelLockoutOverflow() walked modelLockouts in raw insertion order
and deleted the oldest N regardless of entry.until. If the map exceeded
1000 entries while some of the oldest were still well within their
active cooldown window, eviction silently deleted them — isModelLocked()
would then report the model as unlocked even though it was still
rate-limited/quota-exhausted, undermining the Model Lockout resilience
layer. Reproduced live: lock a "victim" model first, lock 1000 more
distinct models, call evictModelLockoutOverflow(), and isModelLocked()
on the victim flips from true to false despite ~60s of cooldown left.
Fix: only entries whose `until` has already elapsed are eviction
candidates. ensureCleanupTimer()'s tick already runs
cleanupModelLockKey() on every key immediately before calling this
function, which removes genuinely-expired entries — so anything active
left over the cap is, by construction, a real in-progress cooldown and
must never be silently dropped. If the map is still over cap purely
from active entries, the cap becomes a (rare-case) soft bound rather
than trading away correctness.
The 3 existing tests only asserted Map.size shrank to the cap, which
is exactly the buggy behavior being fixed (they created only
active/never-expiring locks and expected mass eviction regardless).
Rewrote them to use lockModel()'s cooldownMs sign to construct
deterministic active vs. already-expired entries (no real sleeps
needed), and added a direct regression test asserting a specific
still-active key survives eviction via isModelLocked() while an
overflow of expired fillers is correctly evicted down to the cap.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* refactor(resilience): extract lockout eviction to module (file-size cap)
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* perf(db): add temp_store=MEMORY pragma to SQLite init
Store temp tables/indices in memory instead of disk for faster
query execution (GROUP BY, ORDER BY, subquery materialization).
The two other optimized PRAGMAs (synchronous=NORMAL, cache_size=-16384)
were already set.
* test(db): add temp_store MEMORY pragma test
Verifies PRAGMA temp_store = 2 (MEMORY) after initDb() runs.
---------
Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
* perf(startup): warm model catalog cache at module init
Fire-and-forget call to getUnifiedModelsResponse after DB ready so first
GET /v1/models request doesn't pay cold-build cost (15-30s). Non-fatal —
if warmup fails the next request builds fresh.
* test: add warm catalog cache source-pattern test
Verifies registerNodejs() includes the model catalog warmup import
and call to getUnifiedModelsResponse.
* fix(perf): warm the durable OpenRouter catalog cache, not just the 1.5s TTL Response cache
The warmup called getUnifiedModelsResponse() with no Authorization header,
so it only ever populated the top-level per-key Response cache
(catalogCache in catalog.ts) at key "|0|" — a real client sending an
apiKey gets a different key and misses that cache entry. But that cache
also has only a 1.5s TTL (CATALOG_CACHE_TTL_MS, a #6408 burst-dedup
window for concurrent requests, not a startup-warm cache), so even a
perfectly key-matched entry would almost always have expired before real
traffic arrives regardless.
The one genuinely durable, apiKey-independent cost in the catalog build
is getOpenRouterCatalog()'s 24h disk-cached network fetch
(src/lib/catalog/openrouterCatalog.ts) — buildUnifiedModelsResponseCore()
calls it unconditionally whenever an OpenRouter connection is configured,
fully decoupled from the per-key Response cache. Extract the warmup into
an exported warmModelCatalogCache() (testable in isolation, without
exercising all of registerNodejs()) that explicitly warms this cache too,
guarded on an OpenRouter connection actually existing so deployments that
never use OpenRouter don't pay an unconditional third-party network call
at every boot.
Replace the source-text-grep test with a behavioral one: warm once with a
mocked fetch, confirm exactly one network call, then confirm a real
request using a DIFFERENT apiKey than the warmup reuses the cache instead
of re-fetching — the actual, durable, apiKey-independent benefit. Also
covers the no-connection-configured and fetch-failure-is-non-fatal cases.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* perf(db): project columns + composite index in getProviderConnections
- Add param to avoid (scans ~200MB/query)
- Add WHERE clause support (was silently ignored)
- Add composite index on
(auth_type, is_active, refresh_token)
- Update health check caller to request only needed columns
- Test: authType filter, column projection, default full fetch
* fix(db): dedupe authType filter, allowlist columns projection in getProviderConnections
The branch was rebased on top of #6946 (already merged, same author,
same authType-filter fix), leaving a duplicate `if (filter.authType)`
block in getProviderConnections. Harmless at runtime (SQLite tolerates
the repeated named param) but dead code — remove the newer duplicate,
keep the one already merged via #6946.
The `columns` projection param is interpolated directly into the SQL
SELECT clause via `.join(", ")` with no validation. No current caller
passes untrusted input, but it's a live SQL-injection footgun for
whichever future caller wires it up: reproduced a working exfiltration
via a single-statement subquery column name (no semicolon/stacked-query
needed, so better-sqlite3's single-statement restriction doesn't help)
that leaked an unrelated connection's api_key through the response.
Add an allowlist validated against the real provider_connections schema
(core.ts's SCHEMA_SQL) — rejects any non-listed column, and re-quotes
the reserved "group" keyword so it stays usable. Verified the fix
blocks the exact reproduced exfiltration.
Add a regression test asserting invalid/injection-shaped column names
are rejected, a mixed valid+invalid list still rejects (fail-closed,
not a silent partial projection), and the legitimate "group" column
still round-trips correctly when requested.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix: add re-entrancy guard to token health check sweep
Adds an in-flight guard to prevent overlapping sweep() executions.
Uses global state sweeping flag that is set before the first await
and cleared in a finally block. Subsequent calls while sweeping
return early with a debug log line.
Test coverage:
- skips when a previous sweep is still in flight
- resets sweeping flag after normal completion
- resets sweeping flag on empty connections
* fix(test): move sweep re-entrancy test to node:test, wire CI correctly
tests/unit/token-health-check-sweep.test.ts used vitest syntax while
living directly under tests/unit/, which is exactly the glob
`npm run test:unit` (node's native runner) scans — running it there
threw "Vitest mocker was not initialized" and failed the file outright.
Separately, the vitest.config.ts include-array edit didn't wire the
test into any CI-blocking script either: `npm run test:vitest` runs
vitest.mcp.config.ts (a different config, not this path), and
test:vitest:ui is scoped to tests/unit/ui only — so the 3 tests never
ran in CI at all while node's runner actively failed on the file.
Rewrite the test to node:test, matching the
tests/unit/apikey-connection-health-check.test.ts /
tests/unit/token-health-check.test.ts convention (real temp-dir SQLite
DB rather than vi.mock, since mock.module() is unavailable in this
tsx/ESM + Node native test-runner setup). The re-entrancy scenario now
drives the real, unmocked sweep() with real OAuth connections
(healthCheckInterval: 0 keeps checkConnection() a fast no-op) and
asserts on wall-clock elapsed time + the shared sweeping flag instead
of a mocked call count. Verified this fails without the guard (909ms,
~3x the stagger) and passes with it restored.
Remove the now-unused vitest.config.ts include entry since the test no
longer needs it.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* refactor(health): compact sweep guard + restore one-line stagger delay (file-size cap)
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Base-red unblock (CI Unit shard 2/4 red on EVERY PR since #7653). Validated locally: test 6/6 under the exact shard harness; persist module proven to load without the UI chain; full static-gate set green (complexity 2056≤2058, cognitive 889≤890, file-size/test-discovery/dashboard-typecheck/changelog OK).
Validated in merge-train 2026-07-18 @ 9084b408b: 12198/12201 pass; single red = earlyStreamKeepalive timer test, confirmed load-flake (6/6 green isolated on release tip AND the merged tree; no boarded PR touches keepalive)
Validated in merge-train 2026-07-18 @ 9084b408b: 12198/12201 pass; single red = earlyStreamKeepalive timer test, confirmed load-flake (6/6 green isolated on release tip AND the merged tree; no boarded PR touches keepalive)
Validated in merge-train 2026-07-18 @ 9084b408b: 12198/12201 pass; single red = earlyStreamKeepalive timer test, confirmed load-flake (6/6 green isolated on release tip AND the merged tree; no boarded PR touches keepalive)
Validated in merge-train 2026-07-18 @ 9084b408b: 12198/12201 pass; single red = earlyStreamKeepalive timer test, confirmed load-flake (6/6 green isolated on release tip AND the merged tree; no boarded PR touches keepalive)