mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-21 14:22:14 +03:00
* fix(quality): drain the 09-18 base-reds, part 1 — thinking gate parity, inventory, webpack externals Reproduced on the clean tip7cc454d9before 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
317 lines
9.6 KiB
TypeScript
317 lines
9.6 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { afterEach, describe, it } from "node:test";
|
|
|
|
import {
|
|
acquire,
|
|
acquireMany,
|
|
buildAccountSemaphoreKey,
|
|
getStats,
|
|
markBlocked,
|
|
reset,
|
|
resetAll,
|
|
} from "../../open-sse/services/accountSemaphore";
|
|
|
|
afterEach(() => {
|
|
resetAll();
|
|
});
|
|
|
|
describe("accountSemaphore acquireMany", () => {
|
|
it("atomically acquires every enabled gate and releases them once", async () => {
|
|
const release = await acquireMany([
|
|
{ key: "global", maxConcurrency: 2 },
|
|
{ key: "provider:codex", maxConcurrency: 1 },
|
|
{ key: "account:codex:one", maxConcurrency: 1 },
|
|
{ key: "disabled", maxConcurrency: 0 },
|
|
]);
|
|
|
|
assert.deepEqual(getStats(), {
|
|
global: { running: 1, queued: 0, maxConcurrency: 2, blockedUntil: null },
|
|
"provider:codex": { running: 1, queued: 0, maxConcurrency: 1, blockedUntil: null },
|
|
"account:codex:one": { running: 1, queued: 0, maxConcurrency: 1, blockedUntil: null },
|
|
});
|
|
|
|
release();
|
|
release();
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
assert.deepEqual(getStats(), {});
|
|
});
|
|
|
|
it("queues one atomic request without partially reserving free gates", async () => {
|
|
const releaseProvider = await acquire("provider:codex", { maxConcurrency: 1 });
|
|
const waiting = acquireMany(
|
|
[
|
|
{ key: "global", maxConcurrency: 1 },
|
|
{ key: "provider:codex", maxConcurrency: 1 },
|
|
{ key: "account:codex:two", maxConcurrency: 1 },
|
|
],
|
|
{ timeoutMs: 200 }
|
|
);
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
|
|
assert.equal(getStats().global?.running ?? 0, 0);
|
|
assert.equal(getStats()["account:codex:two"]?.running ?? 0, 0);
|
|
assert.equal(getStats()["provider:codex"]?.queued, 1);
|
|
|
|
releaseProvider();
|
|
const release = await waiting;
|
|
assert.equal(getStats().global?.running, 1);
|
|
assert.equal(getStats()["provider:codex"]?.running, 1);
|
|
assert.equal(getStats()["account:codex:two"]?.running, 1);
|
|
release();
|
|
});
|
|
|
|
it("removes an atomic waiter from every gate on abort", async () => {
|
|
const releaseGlobal = await acquire("global", { maxConcurrency: 1 });
|
|
const controller = new AbortController();
|
|
const waiting = acquireMany(
|
|
[
|
|
{ key: "global", maxConcurrency: 1 },
|
|
{ key: "provider:codex", maxConcurrency: 1 },
|
|
],
|
|
{ signal: controller.signal, timeoutMs: 200 }
|
|
);
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
controller.abort();
|
|
await assert.rejects(waiting, { name: "AbortError" });
|
|
assert.equal(getStats().global?.queued, 0);
|
|
assert.equal(getStats()["provider:codex"]?.queued ?? 0, 0);
|
|
releaseGlobal();
|
|
});
|
|
|
|
it("times out an atomic waiter without leaking reservations", async () => {
|
|
const releaseGlobal = await acquire("global", { maxConcurrency: 1 });
|
|
await assert.rejects(
|
|
acquireMany(
|
|
[
|
|
{ key: "global", maxConcurrency: 1 },
|
|
{ key: "provider:codex", maxConcurrency: 1 },
|
|
],
|
|
{ timeoutMs: 10 }
|
|
),
|
|
(error: Error & { code?: string }) => error.code === "SEMAPHORE_TIMEOUT"
|
|
);
|
|
assert.equal(getStats().global?.queued, 0);
|
|
assert.equal(getStats()["provider:codex"]?.running ?? 0, 0);
|
|
releaseGlobal();
|
|
});
|
|
|
|
it("rejects an atomic waiter when any required gate queue is full", async () => {
|
|
const releaseGlobal = await acquire("global", { maxConcurrency: 1 });
|
|
const queued = acquire("global", { maxConcurrency: 1, maxQueueSize: 1, timeoutMs: 200 });
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
|
|
await assert.rejects(
|
|
acquireMany(
|
|
[
|
|
{ key: "global", maxConcurrency: 1 },
|
|
{ key: "provider:codex", maxConcurrency: 1 },
|
|
],
|
|
{ maxQueueSize: 1, timeoutMs: 200 }
|
|
),
|
|
(error: Error & { code?: string }) => error.code === "SEMAPHORE_QUEUE_FULL"
|
|
);
|
|
assert.equal(getStats()["provider:codex"]?.queued ?? 0, 0);
|
|
|
|
releaseGlobal();
|
|
(await queued)();
|
|
});
|
|
|
|
it("fails immediately when failFast is set", async () => {
|
|
const release = await acquire("codex:account-a", { maxConcurrency: 1 });
|
|
|
|
await assert.rejects(
|
|
acquire("codex:account-a", {
|
|
maxConcurrency: 1,
|
|
failFast: true,
|
|
timeoutMs: 200,
|
|
}),
|
|
(error: Error & { code?: string }) => error.code === "SEMAPHORE_QUEUE_FULL"
|
|
);
|
|
assert.equal(getStats()["codex:account-a"]?.queued ?? 0, 0);
|
|
|
|
release();
|
|
});
|
|
|
|
it("maxQueueSize 0 means no queue limit, not fail-fast (#6593 contract)", async () => {
|
|
// chatCore forwards resilienceSettings.requestQueue.maxQueueDepth, whose documented
|
|
// default is `0 = disabled`. #12911 briefly read 0 as "reject when busy", which
|
|
// turned every busy account slot into a 429 under default settings.
|
|
const release = await acquire("codex:account-b", { maxConcurrency: 1 });
|
|
|
|
const queued = acquire("codex:account-b", {
|
|
maxConcurrency: 1,
|
|
maxQueueSize: 0,
|
|
timeoutMs: 500,
|
|
});
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
assert.equal(getStats()["codex:account-b"]?.queued ?? 0, 1, "must wait in the queue");
|
|
|
|
release();
|
|
const releaseQueued = await queued;
|
|
releaseQueued();
|
|
});
|
|
});
|
|
|
|
describe("accountSemaphore", async () => {
|
|
it("queues requests beyond the account cap and drains on release", async () => {
|
|
const key = buildAccountSemaphoreKey({
|
|
provider: "alibaba",
|
|
accountKey: "acct-1",
|
|
});
|
|
|
|
const releaseA = await acquire(key, { maxConcurrency: 2, timeoutMs: 200 });
|
|
const releaseB = await acquire(key, { maxConcurrency: 2, timeoutMs: 200 });
|
|
const queued = acquire(key, { maxConcurrency: 2, timeoutMs: 200 });
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
|
|
assert.deepEqual(getStats()[key], {
|
|
running: 2,
|
|
queued: 1,
|
|
maxConcurrency: 2,
|
|
blockedUntil: null,
|
|
});
|
|
|
|
releaseA();
|
|
const releaseC = await queued;
|
|
|
|
assert.deepEqual(getStats()[key], {
|
|
running: 2,
|
|
queued: 0,
|
|
maxConcurrency: 2,
|
|
blockedUntil: null,
|
|
});
|
|
|
|
releaseA();
|
|
releaseB();
|
|
releaseC();
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
assert.equal(getStats()[key], undefined);
|
|
});
|
|
|
|
it("returns a no-op release when concurrency is bypassed", async () => {
|
|
const key = buildAccountSemaphoreKey({
|
|
provider: "alibaba",
|
|
accountKey: "acct-bypass",
|
|
});
|
|
|
|
const release = await acquire(key, { maxConcurrency: 0, timeoutMs: 50 });
|
|
|
|
assert.deepEqual(getStats(), {});
|
|
|
|
release();
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
assert.equal(getStats()[key], undefined);
|
|
});
|
|
|
|
it("uses SEMAPHORE_TIMEOUT for timed out queued requests", async () => {
|
|
const key = buildAccountSemaphoreKey({
|
|
provider: "alibaba",
|
|
accountKey: "acct-timeout",
|
|
});
|
|
|
|
const releaseA = await acquire(key, { maxConcurrency: 1, timeoutMs: 200 });
|
|
const queued = acquire(key, { maxConcurrency: 1, timeoutMs: 200 });
|
|
const keepAlive = setTimeout(() => {}, 250);
|
|
|
|
try {
|
|
await queued;
|
|
assert.fail("Expected timeout error");
|
|
} catch (err: unknown) {
|
|
assert.ok(err instanceof Error);
|
|
const error = err as Error & { code?: string };
|
|
assert.equal(error.code, "SEMAPHORE_TIMEOUT");
|
|
} finally {
|
|
clearTimeout(keepAlive);
|
|
}
|
|
|
|
releaseA();
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
assert.equal(getStats()[key], undefined);
|
|
});
|
|
|
|
it("keeps release idempotent for finally blocks", async () => {
|
|
const key = buildAccountSemaphoreKey({
|
|
provider: "alibaba",
|
|
accountKey: "acct-idempotent",
|
|
});
|
|
|
|
const releaseA = await acquire(key, { maxConcurrency: 1, timeoutMs: 200 });
|
|
|
|
// Simulate a finally block calling release twice
|
|
releaseA();
|
|
releaseA();
|
|
releaseA();
|
|
|
|
// The second acquire should succeed immediately (slot was released)
|
|
const releaseB = await acquire(key, { maxConcurrency: 1, timeoutMs: 200 });
|
|
|
|
assert.deepEqual(getStats()[key], {
|
|
running: 1,
|
|
queued: 0,
|
|
maxConcurrency: 1,
|
|
blockedUntil: null,
|
|
});
|
|
|
|
releaseB();
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
assert.equal(getStats()[key], undefined);
|
|
});
|
|
|
|
it("supports temporary blocking and explicit reset hooks", async () => {
|
|
const key = buildAccountSemaphoreKey({
|
|
provider: "alibaba",
|
|
accountKey: "acct-blocked",
|
|
});
|
|
|
|
await acquire(key, { maxConcurrency: 1, timeoutMs: 200 });
|
|
|
|
assert.deepEqual(getStats()[key], {
|
|
running: 1,
|
|
queued: 0,
|
|
maxConcurrency: 1,
|
|
blockedUntil: null,
|
|
});
|
|
|
|
markBlocked(key, 50);
|
|
|
|
// Should block even though slot is available
|
|
const acquired = acquire(key, { maxConcurrency: 1, timeoutMs: 100 });
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
|
|
// Should still be queued because the gate is blocked
|
|
const stats = getStats()[key];
|
|
assert.equal(stats.running, 1);
|
|
assert.equal(stats.queued, 1);
|
|
assert.equal(stats.maxConcurrency, 1);
|
|
assert.ok(stats.blockedUntil !== null, "blockedUntil should be set");
|
|
|
|
reset(key);
|
|
|
|
await assert.rejects(async () => {
|
|
await acquired;
|
|
});
|
|
});
|
|
|
|
it("preserves existing maxConcurrency when markBlocked is applied", async () => {
|
|
const key = buildAccountSemaphoreKey({
|
|
provider: "alibaba",
|
|
accountKey: "acct-preserve",
|
|
});
|
|
|
|
await acquire(key, { maxConcurrency: 2, timeoutMs: 200 });
|
|
markBlocked(key, 50);
|
|
|
|
const stats = getStats()[key];
|
|
assert.equal(stats.running, 1);
|
|
assert.equal(stats.queued, 0);
|
|
assert.equal(stats.maxConcurrency, 2);
|
|
assert.ok(stats.blockedUntil !== null, "blockedUntil should be set");
|
|
});
|
|
});
|