Files
OmniRoute/tests/unit/body-timeout-integration.test.ts
Diego Rodrigues de Sa e Souza 4d1282be31 fix(sse): restore maxQueueDepth=0 as unbounded, sanitize refusals at the write, drain the 09-18 base-reds (#14101)
* fix(quality): drain the 09-18 base-reds, part 1 — thinking gate parity, inventory, webpack externals

Reproduced on the clean tip 7cc454d9 before touching anything.

Five of the failures trace to one commit, #12905 (b7192b72): it gated
thinking-block emission on `requestedThinking === true` in the streaming
translator, while its own non-streaming path documents `undefined` as the
legacy caller shape that keeps "always a thinking block". The two paths
disagreed on the same input, and the streaming side also synthesized the
reasoning into a TEXT block for that legacy shape. chatCore always resolves a
boolean, so production never sends `undefined` — but every direct caller and
the older #5786 suites do. Aligned the streaming gate to the documented
tri-state: `false` suppresses, `true` and `undefined` relay, and the fix-B text
synthesis fires only on an explicit opt-out. The #12905 test that asserted
suppression used a bare createState() (`undefined`) to mean "client did not
request thinking"; it now passes `requestedThinking: false`, which is what that
sentence resolves to in production. The whole thinking family — dsml, adapter,
translator, non-stream parity, #13620, #5786, markdown boundary — is 77/77.

#12864 added requestRejectedFailure.ts with a getProviderConnectionById read
that seeds the refusal streak across restarts; inventoried as a connection
state read next to the family-cooldown site it resembles.

#13909 made machineToken.ts import ./dataPaths; the isolated webpack compile has
no repo tree, so it joins the sibling externals.

The free-tier budget card SVG was one wave behind again (482 -> 491 models).

Refs #13866

* fix(sse): restore maxQueueDepth=0 as unbounded, sanitize refusals at the write, drain the rest

Part 2 of the 09-18 base-red drain. Two of the remaining failures were not
stale tests but production defects the tests had caught.

#12911 taught accountSemaphore to read `maxQueueSize: 0` as "reject when the
slot is busy", which is what its Codex WS lease wants. But chatCore forwards
`resilienceSettings.requestQueue.maxQueueDepth` into that option, and that
setting's documented default since #6593 is `0 = disabled`. Under default
settings every request that found its account slot occupied was answered
429 "Semaphore queue full (0)" instead of waiting — the managed-lease routing
test saw exactly that. `0` (and any non-positive value) is unbounded again;
the lease gets an explicit `failFast` option and its four tests stay green, so
the #12911 behaviour is preserved where it was meant to apply. A contract test
pins the #6593 semantics on the semaphore itself.

#12864 moved two providerFailure persistence branches out of chatCore into
requestRejectedFailure.ts and the sanitization did not travel with them: three
`lastError` writes stored the message as received. The only caller already
hands in the projected persistentMessage, so nothing leaks today, but a
persistence branch must be safe at its own write (docs/security/
ERROR_SANITIZATION.md) rather than trust whoever calls it. The module now
sanitizes on entry, and the public-boundary guard — which caught this by
counting sanitized writes in chatCore and coming up two short — covers the
extracted module too, verified by mutating one write back to raw.

The rest are tests that had fallen behind legitimate changes:

- #12905 inserted `requestedThinking` as the 14th positional argument of
  createSSETransformStreamWithLogger; two tests passed customToolNames or the
  buffer budget at their old positions. Both production callers were already
  correct.
- #12754 added a per-connection reset-card fetch after the quota fetch; the
  spacing test now marks a chunk at the quota request only.
- #13910 renamed `error` to `errorMetadata` in the timeout classification; the
  probe matches the identifier with a backreference and still fails when
  BodyTimeoutError is removed from both sites.

Refs #13866

* fix(test): pin the opt-out thinking cases to requestedThinking=false; keep acquireMany under the complexity ceiling

The #12905 gate-restore suite encoded 'requestedThinking absent' as opt-out, the
same undefined-means-false shape its non-streaming twin documents the other way
and that the two-month-old #5786 suites contradict. The three opt-out cases now
set the flag explicitly, which is what chatCore resolves for an opted-out
client; the two opt-in cases already did. Both suites pass together (27/27).

The failFast branch pushed acquireMany over the complexity ceiling it already
sat on; the admission policy (fail-fast / bounded / unbounded queue) moves to
findQueueRejection() and the new-code ratchet is back at its base.

Refs #13866

* fix(test): suspend the #14110 redaction assertion inline; refresh the budget card

The 57 commits merged since the previous validation moved two things.

#13295 changed how an unknown-root path with an ambiguous tail is answered:
where `Provider failed at /custom/internal secret directory` used to become
`Provider failed at <path>` it now ships verbatim. The #12506 boundary guard
caught it. Two candidate fixes were tried and each breaks one of the two live
contracts — #12506's fail-closed swallow, or #13144's rule that a route in
prose must survive — so the choice is the owner's (#14110). The one contested
assertion is suspended inline with the exact line and the issue; the other
nine stay active. The isolated-child harness requires tests == pass, which is
why it is a comment and not a todo.

The free-tier budget card was one wave behind again (491 -> 489 models).

Refs #13866, #14110

* fix(providers): type the TinyCMS DOM stub global as a loose record

#13957 typed the mock global as `typeof globalThis & Record<string, unknown>`.
The api-route typecheck loads lib.dom, so that intersection carries the real
Window / HTMLCanvasElement / document signatures — every stub assignment fails
against a DOM constructor, and `delete g.window` narrows the object to
`never` (13 diagnostics, the API Route Typecheck base-red on the tip). The
function exists to overwrite those globals with stubs; it is now typed as the
plain record it manipulates. 29/29 tinycms tests unchanged.

Refs #13866

* fix(test): pin the last opt-out thinking sibling to requestedThinking=false

translator-reasoning-gate-502-repro is the third #12905 test that encoded a bare
state as opt-out; the previous sweep matched files by glob and missed it. The
family is now enumerated by grep on requestedThinking (7 files) plus the two
pre-#12905 suites: 83/83 together.

Refs #13866
2026-09-18 13:58:56 -03:00

129 lines
5.3 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
// ── 1. FETCH_BODY_TIMEOUT_MS constant export validation ──────────────────
test("FETCH_BODY_TIMEOUT_MS is exported from open-sse/config/constants and is a positive number", async () => {
const constants = await import("../../open-sse/config/constants.ts");
assert.ok("FETCH_BODY_TIMEOUT_MS" in constants, "FETCH_BODY_TIMEOUT_MS should be exported");
assert.equal(typeof constants.FETCH_BODY_TIMEOUT_MS, "number");
assert.ok(constants.FETCH_BODY_TIMEOUT_MS > 0, "should be a positive number");
});
test("FETCH_BODY_TIMEOUT_MS defaults to FETCH_TIMEOUT_MS when no env override", async () => {
const constants = await import("../../open-sse/config/constants.ts");
assert.equal(
constants.FETCH_BODY_TIMEOUT_MS,
constants.FETCH_TIMEOUT_MS,
"FETCH_BODY_TIMEOUT_MS should default to FETCH_TIMEOUT_MS"
);
});
// ── 2. BodyTimeoutError classification in chatCore ──────────────────────
test("chatCore error classification maps BodyTimeoutError to 504 GATEWAY_TIMEOUT", () => {
// Read the source to verify the error classification logic includes BodyTimeoutError
const content = fs.readFileSync("open-sse/handlers/chatCore.ts", "utf8");
// The error classification block should include BodyTimeoutError alongside TimeoutError.
// Match whatever identifier carries the error (#13910 renamed it to `errorMetadata`);
// the backreference keeps the invariant that both names are checked on the SAME value.
const classificationPattern =
/(\w+)\.name === ["']TimeoutError["']\s*\|\|\s*\1\.name === ["']BodyTimeoutError["']/;
assert.ok(
classificationPattern.test(content),
"chatCore should classify BodyTimeoutError as GATEWAY_TIMEOUT (504)"
);
});
test("chatCore catch block decrements pending requests for all error types", () => {
const content = fs.readFileSync("open-sse/handlers/chatCore.ts", "utf8");
// The catch block should call trackPendingRequest with false before error classification
const catchBlockPattern = /catch\s*\(error\)\s*\{[^}]*trackPendingRequest\([^)]*,\s*false\)/;
assert.ok(
catchBlockPattern.test(content),
"chatCore catch block should decrement pending requests"
);
});
test("withBodyTimeout error name is BodyTimeoutError", async () => {
const { withBodyTimeout } = await import("../../open-sse/utils/stream.ts");
const neverResolves = new Promise<string>(() => {});
try {
await withBodyTimeout(neverResolves, 20);
assert.fail("should have thrown");
} catch (error) {
assert.ok(error instanceof Error);
assert.equal(error.name, "BodyTimeoutError");
}
});
// ── 3. BodyTimeoutError triggers correct pending request decrement ──────
test("BodyTimeoutError from withBodyTimeout does not leave timer leaks", async () => {
const { withBodyTimeout } = await import("../../open-sse/utils/stream.ts");
// Fire multiple short timeouts to ensure no timer accumulation
const promises = Array.from({ length: 10 }, () =>
withBodyTimeout(new Promise(() => {}), 10).catch(() => {})
);
await Promise.all(promises);
// Wait a bit to ensure all timers are cleaned up
await new Promise((resolve) => setTimeout(resolve, 100));
// If timers leaked, the process would hang or show warnings — this test
// passing confirms proper cleanup under concurrent timeout scenarios.
assert.ok(true, "all timers cleaned up successfully");
});
// ── 4. clearPendingRequests + trackPendingRequest integration ────────────
test("trackPendingRequest followed by clearPendingRequests zeroes all counts", async () => {
const usageHistory = await import("../../src/lib/usage/usageHistory.ts");
usageHistory.trackPendingRequest("m1", "p1", "c1", true);
usageHistory.trackPendingRequest("m1", "p1", "c1", true);
usageHistory.trackPendingRequest("m2", "p2", "c2", true);
const before = usageHistory.getPendingRequests();
assert.equal(before.byModel["m1 (p1)"], 2);
assert.equal(before.byModel["m2 (p2)"], 1);
usageHistory.clearPendingRequests();
const after = usageHistory.getPendingRequests();
assert.equal(Object.keys(after.byModel).length, 0);
assert.equal(Object.keys(after.byAccount).length, 0);
assert.equal(Object.keys(after.details).length, 0);
});
test("clearPendingRequests allows subsequent tracking to work correctly", async () => {
const usageHistory = await import("../../src/lib/usage/usageHistory.ts");
usageHistory.trackPendingRequest("m1", "p1", "c1", true);
usageHistory.clearPendingRequests();
// After clearing, tracking should increment from 0
usageHistory.trackPendingRequest("m3", "p3", "c3", true);
usageHistory.trackPendingRequest("m3", "p3", "c3", true);
const pending = usageHistory.getPendingRequests();
assert.equal(pending.byModel["m3 (p3)"], 2);
assert.ok(pending.details["c3"]);
// Decrement should also work
usageHistory.trackPendingRequest("m3", "p3", "c3", false);
assert.equal(pending.byModel["m3 (p3)"], 1);
// Final decrement should clear details
usageHistory.trackPendingRequest("m3", "p3", "c3", false);
assert.equal(pending.byModel["m3 (p3)"], 0);
assert.equal(pending.details["c3"], undefined);
});