Files
OmniRoute/tests/unit/test-all-model-status.test.ts
Jan Leon ea650253af Stream model health probes for slow providers (#7377)
* fix(model-test): stream slow chat probes

* fix(model-test): handle JSON responses for streaming probes

* fix(model-test): preserve transient errors from streaming probes

* fix(model-test): keep transient probe failures visible

* chore(ci): rerun pull request checks

* docs(model-test): clarify transient failure handling

* chore(ci): rerun pull request checks

* test(model-test): cover slow timeout response path

* chore(quality): register base-branch mutation tests

* fix(sse): stop real-network leak and DOMException crash in model-test-runner timeout path

Two new tests added by this PR fail against the current release tip:

- tests/unit/model-test-runner.test.ts's slow-timeout regression test races
  the cold-start cost of the chat-completions pipeline (SSE translators,
  compression settings, etc. all lazily init on the first real request in a
  process). With a 1s AbortController timeout, the abort can fire before
  chatCore ever reaches the executor's fetch() call; the mocked fetch is then
  invoked after the test's own `finally` block has already restored the real
  fetch, so the assertion on the mock never fires and the request leaks onto
  the real network. Warm up the pipeline with one fast, resolving mock call
  before timing the 1s scenario.

- Once the warm-up unblocks that race, a second, real bug surfaces:
  withRateLimit's abort handling (open-sse/services/rateLimitManager.ts)
  mutates `reason.name = "AbortError"` in place. When `AbortController.abort()`
  is called with no explicit reason (as modelTestRunner's timeout path does),
  the default reason is a native DOMException, whose `name` is a read-only
  getter — the mutation throws `TypeError: Cannot set property name of
  [object DOMException] which has only a getter` instead of rejecting
  cleanly. Build a fresh Error instead of mutating the caller-supplied
  reason, preserving the original as `.cause`.

Adds a focused regression test in tests/unit/rate-limit-manager.test.ts that
reproduces the DOMException crash directly against withRateLimit.

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>
2026-07-18 21:18:42 -03:00

75 lines
3.0 KiB
TypeScript

// Regression for the "Test all models" status-icon bug:
// individual ▶ test turns each model's icon green/red (onTestModel sets
// modelTestStatus), but "Test all models" only showed a toast and left every
// icon blank — the user could not tell which model passed or failed.
//
// Both handleTestAll implementations (ProviderDetailPageClient + PassthroughModelsSection)
// now route each model's result through evaluateTestAllEntry() and apply the returned
// status to modelTestStatus. This pure helper captures the status + auto-hide decision
// so it is testable in the gating node suite (matches the #3610 helper-extraction idiom).
import { test } from "node:test";
import assert from "node:assert/strict";
import { evaluateTestAllEntry } from "../../src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts";
test("ok entry maps to status 'ok' and is never hidden", () => {
assert.deepEqual(evaluateTestAllEntry({ status: "ok" }, true), {
status: "ok",
shouldHide: false,
});
assert.deepEqual(evaluateTestAllEntry({ status: "ok" }, false), {
status: "ok",
shouldHide: false,
});
});
test("failed entry maps to status 'error' and hides only when autoHide is on", () => {
assert.deepEqual(evaluateTestAllEntry({ status: "error" }, true), {
status: "error",
shouldHide: true,
});
assert.deepEqual(evaluateTestAllEntry({ status: "error" }, false), {
status: "error",
shouldHide: false,
});
});
test("rate-limited / timeout failures show 'error' but are NOT auto-hidden", () => {
// Rate-limited and timeout are TRANSIENT — the model itself is fine, the
// provider was just throttled during a parallel Test All. Hiding it would
// silently remove a working model from /v1/models with no recovery path
// short of manual DB edit or per-row eye-toggle. We surface the failure on
// the row icon (status: 'error') but keep the model visible.
assert.deepEqual(evaluateTestAllEntry({ status: "error", rateLimited: true }, true), {
status: "error",
shouldHide: false,
});
assert.deepEqual(evaluateTestAllEntry({ status: "error", isTimeout: true }, true), {
status: "error",
shouldHide: false,
});
assert.deepEqual(evaluateTestAllEntry({ status: "error", isTransient: true }, true), {
status: "error",
shouldHide: false,
});
// Toggle off → still not hidden, of course.
assert.deepEqual(evaluateTestAllEntry({ status: "error", rateLimited: true }, false), {
status: "error",
shouldHide: false,
});
});
test("slow batch probes remain visible and unconfirmed", () => {
assert.deepEqual(evaluateTestAllEntry({ status: "slow", isTimeout: true }, true), {
status: "error",
shouldHide: false,
});
});
test("missing / null / empty entry is treated as a failure", () => {
for (const entry of [undefined, null, {}]) {
const out = evaluateTestAllEntry(entry, true);
assert.equal(out.status, "error", `entry=${JSON.stringify(entry)} → error`);
assert.equal(out.shouldHide, true);
}
});