mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-25 08:32:11 +03:00
test(kimi): stop drawing the refresh window inside the assertion (#11380)
Validado em lote combinado (batch-0824g, junto de #11388/#11397/#11415/#11418) contra o tip de release/v3.8.50: typecheck:core limpo, file-size/changelog/complexity/cognitive-complexity OK, 62/62 testes focados passando. Diagnóstico correto e bem documentado: a falha do nightly Node 26 era um teste que sorteia um número e depende do resultado, não uma quebra de compatibilidade. Comportamento de produção inalterado (a janela de jitter continua aleatória; só o teste ganhou controle sobre ela). Obrigado pela investigação detalhada!
This commit is contained in:
@@ -0,0 +1 @@
|
||||
- **test(kimi):** the Kimi background health sweep no longer draws its refresh window inside the assertion. `checkKimiWebConnectionIfNeeded` spreads the refresh over `[60, 240)` seconds before expiry so a fleet of connections does not stampede the token endpoint, and the test used a token expiring in 90 seconds and asserted that a refresh happened — which is true only when the draw lands at 90 or above, i.e. 150 of the 180 possible values. Measured: the test fails 1 run in 6 (16.7% by construction; 4 of 20 local runs), and it is what the Node 26 nightly hit and reported as a Node-compat break (#11361). The spread is now `defaultKimiRefreshJitterSec()` and the window is injectable as `jitterSecFn`, so the test decides it instead of rolling for it; production behaviour is unchanged. Cases were added for a token outside the window and for the default spread's range.
|
||||
@@ -2,6 +2,21 @@ import { isKimiTokenExpiringSoon } from "@omniroute/open-sse/utils/kimiJwt.ts";
|
||||
import { exchangeKimiRefreshToken } from "@/lib/kimi/tokenRefresh";
|
||||
import { updateProviderConnection } from "@/lib/db/providers";
|
||||
|
||||
/**
|
||||
* Refresh window, spread over [60, 240) seconds before expiry so a fleet of
|
||||
* connections does not stampede the token endpoint at the same instant.
|
||||
*
|
||||
* Kept as a named export rather than inline: it is the only nondeterminism in this
|
||||
* path, and a caller that needs a decision it can predict — a test — has to be able
|
||||
* to replace it. `tests/unit/token-health-check-kimi.test.ts` used a token expiring
|
||||
* in 90 s and asserted a refresh, which is a coin the draw loses 1 in 6 times
|
||||
* (a refresh needs `jitter >= 90`, i.e. 150 of the 180 possible values). It failed
|
||||
* that way on the Node 26 nightly and was triaged as a Node-compat break.
|
||||
*/
|
||||
export function defaultKimiRefreshJitterSec(): number {
|
||||
return 60 + Math.floor(Math.random() * 180);
|
||||
}
|
||||
|
||||
export async function checkKimiWebConnectionIfNeeded(params: {
|
||||
conn: any;
|
||||
now: string;
|
||||
@@ -12,6 +27,12 @@ export async function checkKimiWebConnectionIfNeeded(params: {
|
||||
logPrefix: string;
|
||||
exchangeFn?: typeof exchangeKimiRefreshToken;
|
||||
persistFn?: typeof updateProviderConnection;
|
||||
/**
|
||||
* Seconds before expiry at which a refresh is triggered. Defaults to the random
|
||||
* spread below; injectable so a caller — a test above all — can decide the window
|
||||
* instead of drawing it.
|
||||
*/
|
||||
jitterSecFn?: () => number;
|
||||
}): Promise<boolean> {
|
||||
const { conn, log, logWarn, getConnectionLogLabel, logPrefix } = params;
|
||||
const provider = String(conn?.provider || "").toLowerCase();
|
||||
@@ -21,20 +42,23 @@ export async function checkKimiWebConnectionIfNeeded(params: {
|
||||
if (!refreshToken) return true; // Handled, but cannot refresh without refresh_token
|
||||
|
||||
const token = conn.apiKey || conn.accessToken;
|
||||
// Calculate jitter: random value between 60 and 240 seconds (1 to 4 min before expiry)
|
||||
const jitterSec = 60 + Math.floor(Math.random() * 180);
|
||||
const jitterSec = (params.jitterSecFn ?? defaultKimiRefreshJitterSec)();
|
||||
const expiringSoon = isKimiTokenExpiringSoon(token, jitterSec);
|
||||
|
||||
if (!expiringSoon) return true;
|
||||
|
||||
log(`${logPrefix} Kimi Web connection ${getConnectionLogLabel(conn)} token expiring soon; refreshing in background...`);
|
||||
log(
|
||||
`${logPrefix} Kimi Web connection ${getConnectionLogLabel(conn)} token expiring soon; refreshing in background...`
|
||||
);
|
||||
|
||||
const exchange = params.exchangeFn || exchangeKimiRefreshToken;
|
||||
const persist = params.persistFn || updateProviderConnection;
|
||||
|
||||
const res = await exchange(refreshToken);
|
||||
if (res.success && res.accessToken) {
|
||||
log(`${logPrefix} Kimi Web connection ${getConnectionLogLabel(conn)} token refreshed successfully.`);
|
||||
log(
|
||||
`${logPrefix} Kimi Web connection ${getConnectionLogLabel(conn)} token refreshed successfully.`
|
||||
);
|
||||
await persist(conn.id, {
|
||||
apiKey: res.accessToken,
|
||||
accessToken: res.accessToken,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { checkKimiWebConnectionIfNeeded } from "../../src/lib/tokenHealthCheckKimi.ts";
|
||||
import {
|
||||
checkKimiWebConnectionIfNeeded,
|
||||
defaultKimiRefreshJitterSec,
|
||||
} from "../../src/lib/tokenHealthCheckKimi.ts";
|
||||
|
||||
describe("Kimi Background Health Sweep", () => {
|
||||
it("skips non-kimi-web connections", async () => {
|
||||
@@ -16,9 +19,12 @@ describe("Kimi Background Health Sweep", () => {
|
||||
assert.equal(handled, false);
|
||||
});
|
||||
|
||||
it("triggers refresh when Kimi token is within jittered expiration window", async () => {
|
||||
it("triggers refresh when the token is inside the refresh window", async () => {
|
||||
const nowSec = Math.floor(Date.now() / 1000);
|
||||
// Token expiring in 90 seconds (within 60-240s window)
|
||||
// Token expiring in 90 seconds. The window is decided by the caller here, not
|
||||
// drawn: with the default spread of [60, 240) a 90 s token is refreshed only
|
||||
// when the draw lands >= 90, which is 150 of 180 values — so this assertion
|
||||
// used to fail 1 run in 6, and did so on the Node 26 nightly (#11361).
|
||||
const token =
|
||||
"eyJhbGciOiJIUzUxMiJ9." +
|
||||
Buffer.from(JSON.stringify({ exp: nowSec + 90, iat: nowSec })).toString("base64url") +
|
||||
@@ -38,6 +44,7 @@ describe("Kimi Background Health Sweep", () => {
|
||||
logError: () => {},
|
||||
getConnectionLogLabel: () => "kimi-web-1",
|
||||
logPrefix: "[Test]",
|
||||
jitterSecFn: () => 120,
|
||||
exchangeFn: async () => {
|
||||
calledRefresh = true;
|
||||
return {
|
||||
@@ -53,4 +60,51 @@ describe("Kimi Background Health Sweep", () => {
|
||||
assert.equal(handled, true);
|
||||
assert.equal(calledRefresh, true);
|
||||
});
|
||||
|
||||
it("leaves a token outside the window alone", async () => {
|
||||
const nowSec = Math.floor(Date.now() / 1000);
|
||||
const token =
|
||||
"eyJhbGciOiJIUzUxMiJ9." +
|
||||
Buffer.from(JSON.stringify({ exp: nowSec + 900, iat: nowSec })).toString("base64url") +
|
||||
".sig";
|
||||
|
||||
let calledRefresh = false;
|
||||
const handled = await checkKimiWebConnectionIfNeeded({
|
||||
conn: {
|
||||
id: "kimi-conn-2",
|
||||
provider: "kimi-web",
|
||||
apiKey: token,
|
||||
refreshToken: "refresh_123",
|
||||
},
|
||||
now: new Date().toISOString(),
|
||||
log: () => {},
|
||||
logWarn: () => {},
|
||||
logError: () => {},
|
||||
getConnectionLogLabel: () => "kimi-web-2",
|
||||
logPrefix: "[Test]",
|
||||
jitterSecFn: () => 240,
|
||||
exchangeFn: async () => {
|
||||
calledRefresh = true;
|
||||
return {
|
||||
success: true,
|
||||
accessToken: "new_token",
|
||||
refreshToken: "new_refresh",
|
||||
expiresAtSec: nowSec + 900,
|
||||
};
|
||||
},
|
||||
persistFn: async () => {},
|
||||
});
|
||||
|
||||
// Handled (it is a kimi-web connection) but not refreshed.
|
||||
assert.equal(handled, true);
|
||||
assert.equal(calledRefresh, false);
|
||||
});
|
||||
|
||||
it("the default spread stays inside [60, 240)", () => {
|
||||
for (let i = 0; i < 2_000; i++) {
|
||||
const jitter = defaultKimiRefreshJitterSec();
|
||||
assert.ok(Number.isInteger(jitter), `jitter must be whole seconds, got ${jitter}`);
|
||||
assert.ok(jitter >= 60 && jitter < 240, `jitter out of range: ${jitter}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user