test/chat-p2-preflight-admission.test.js
Rama master · réplica de lectura del repositorio original.
// P2-7 (evidence/ReviewChatCore.md §P2-7): the DEVIN_CONNECT fast-fail
// preflight only asked the cooldown/RPM questions
// (isAllRateLimited / isAllTemporarilyUnavailable). It was blind to the
// dimensions the scheduler's own admission predicate adds — account
// maintenance (held for a background / language-server swap; live by default)
// and the per-account in-flight cap — and to the selector latch.
//
// So a pool that was unselectable for one of those reasons passed the
// preflight, spent the full queue wait in acquireConnectAccount, and then
// acquired null — which the connect path cannot tell apart from "no pool at
// all" and answers with the un-accounted env token. That is the P0-2
// conflation the preflight exists to prevent (chat.js's own comment: those two
// cases "must NOT be conflated").
//
// The fix reuses the scheduler's admission conjunction read-only in preflight.
// These exercise the real handler, so the assertion is the client-visible
// outcome: a fast 429 and ZERO upstream calls, never an env-token turn.
import { afterEach, beforeEach, describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { handleChatCompletions, __setConnectDeps, __resetConnectDeps } from '../src/handlers/chat.js';
import { createRequestBudget } from '../src/request-budget.js';
import {
addAccountByKey, removeAccount, getAccountInternal, setAccountTier, markSelectorIneligible,
markRateLimited,
} from '../src/auth.js';
const FREE_SELECTOR = 'swe-1-6-slow';
const SAVED = {};
const ENV_KEYS = [
'DEVIN_CONNECT', 'DEVIN_CONNECT_TOKEN', 'WINDSURF_API_KEY',
'WINDSURFAPI_STRICT_MODEL', 'DEVIN_CONNECT_MAX_INFLIGHT_PER_ACCOUNT',
'WINDSURFAPI_ERROR_RECOVERY_MS',
];
for (const k of ENV_KEYS) SAVED[k] = process.env[k];
const created = [];
function seed(label) {
const a = addAccountByKey(`devin-session-token$p27-${label}-${Math.random().toString(36).slice(2)}`, label);
created.push(a.id);
setAccountTier(a.id, 'pro');
return getAccountInternal(a.id);
}
/** Stub the connect client so an env-token fallback is observable: the handler
* only puts a token on `params` for a POOLED account. */
function stubUpstream() {
const state = { hits: 0, pooledToken: undefined };
__setConnectDeps({
toChatCompletion: async (params) => {
state.hits++;
state.pooledToken = params.token;
return {
status: 200,
body: {
id: 'x', object: 'chat.completion', created: 0,
choices: [{ index: 0, message: { role: 'assistant', content: 'SERVED' }, finish_reason: 'stop' }],
},
};
},
});
return state;
}
function ask() {
return handleChatCompletions(
{ model: FREE_SELECTOR, stream: false, messages: [{ role: 'user', content: 'hi' }] },
{ callerKey: '' },
);
}
beforeEach(() => {
process.env.DEVIN_CONNECT = '1';
// An env token is present on purpose: before the fix it is exactly what a
// blocked pool silently falls through to (devin-connect.js `token || env`).
process.env.DEVIN_CONNECT_TOKEN = 'env-token-shape-only';
delete process.env.WINDSURF_API_KEY;
delete process.env.WINDSURFAPI_STRICT_MODEL;
delete process.env.DEVIN_CONNECT_MAX_INFLIGHT_PER_ACCOUNT;
});
afterEach(() => {
__resetConnectDeps();
while (created.length) removeAccount(created.pop());
for (const k of ENV_KEYS) {
if (SAVED[k] === undefined) delete process.env[k];
else process.env[k] = SAVED[k];
}
});
describe('P2-7: preflight applies the scheduler admission conjunction', () => {
it('fails fast with 429 when every entitled account is in maintenance', async () => {
const a = seed('maint-a');
const b = seed('maint-b');
a._maintenance = 1; // background / language-server swap in progress
b._maintenance = 1;
const up = stubUpstream();
const result = await ask();
assert.equal(result.status, 429,
'a pool that cannot be selected must fail fast, not queue for 30s and then use the env token');
assert.equal(result.body.error.type, 'rate_limit_exceeded');
assert.ok(result.body.error.retry_after_ms > 0, 'carries a retry_after hint');
assert.ok(result.headers['Retry-After'], 'sets the Retry-After header');
assert.equal(up.hits, 0, 'never touched upstream (the env token must not serve a pool request)');
});
it('fails fast when the selector is latched on A and the only peer is in maintenance', async () => {
const a = seed('lat-a');
const b = seed('lat-b');
assert.equal(markSelectorIneligible(a.apiKey, FREE_SELECTOR), true,
'precondition: B is admitted, so the latch is written');
b._maintenance = 1;
const up = stubUpstream();
const result = await ask();
assert.equal(result.status, 429, 'latch(A) + maintenance(B) must not degrade into an env-token turn');
assert.equal(up.hits, 0);
});
it('fails fast when every entitled account is at its in-flight cap', async () => {
process.env.DEVIN_CONNECT_MAX_INFLIGHT_PER_ACCOUNT = '1';
const a = seed('cap-a');
a._inflight = 1;
a._inflightAt = Date.now();
const up = stubUpstream();
const result = await ask();
assert.equal(result.status, 429, 'the in-flight cap is an admission dimension, not a queue hint');
assert.equal(up.hits, 0);
});
it('still serves normally when one peer is in maintenance (no over-trigger)', async () => {
const a = seed('ok-a');
seed('ok-b')._maintenance = 1;
const up = stubUpstream();
const result = await ask();
assert.equal(result.status, 200, 'a selectable account must still serve');
assert.equal(result.body.choices[0].message.content, 'SERVED');
assert.equal(up.hits, 1);
assert.equal(up.pooledToken, a.apiKey, 'served by the pooled account, not the env token');
});
// getApiKey half-opens an 'error' account whose recovery TTL elapsed BEFORE it
// applies the admission predicate. The preflight must model that step, or a
// pool whose only remaining candidate is due for the trial would be 429'd
// forever — the recovery runs inside getApiKey, so the 429 would never let it.
it('does not fast-fail when the remaining peer is DUE for half-open recovery', async () => {
process.env.WINDSURFAPI_ERROR_RECOVERY_MS = '60000';
seed('maint-a')._maintenance = 1;
const b = seed('recover-b');
b.status = 'error';
b._errorAt = Date.now() - 120_000; // past the 60s TTL → the next selection recovers it
const up = stubUpstream();
const result = await ask();
assert.equal(result.status, 200, 'the next getApiKey would recover B — do not 429 the trial away');
assert.equal(up.pooledToken, b.apiKey, 'served by the recovered account');
assert.equal(getAccountInternal(b.id).status, 'active', 'the trial really ran');
});
it('fast-fails while the errored peer is still inside its recovery TTL', async () => {
process.env.WINDSURFAPI_ERROR_RECOVERY_MS = '60000';
seed('maint-a')._maintenance = 1;
const b = seed('recover-b');
b.status = 'error';
b._errorAt = Date.now() - 1_000; // not due yet
const up = stubUpstream();
const result = await ask();
assert.equal(result.status, 429, 'no account is selectable and none is due for a trial');
assert.equal(up.hits, 0);
});
it('still falls back to the env token when the pool is genuinely EMPTY', async () => {
const up = stubUpstream();
const result = await ask();
assert.equal(result.status, 200, 'an empty pool is the env-token deploy shape, not exhaustion');
assert.equal(result.body.choices[0].message.content, 'SERVED');
assert.equal(up.hits, 1);
assert.equal(up.pooledToken, undefined, 'the empty-pool shape hands no pooled token to the client');
});
it('still fails fast for a fully rate-limited pool (existing arm unchanged)', async () => {
const a = seed('rl-a');
const b = seed('rl-b');
markRateLimited(a.apiKey, 5 * 60 * 1000);
markRateLimited(b.apiKey, 5 * 60 * 1000);
const up = stubUpstream();
const result = await ask();
assert.equal(result.status, 429);
assert.ok(result.body.error.retry_after_ms >= 60 * 1000, 'the precise cooldown is still advertised');
assert.equal(up.hits, 0);
});
it('fast-fails 429 when the only account is cooled for the requested selector', async () => {
// A model-scoped 429 with an upstream reset — the shape finalizeConnectAccount
// writes for "Reached message rate limit for this model ... Resets in: 3h".
// It benches the account for THIS selector only, so the preflight must read
// BOTH dimensions: rl.allLimited and tu.allUnavailable true. If tu stayed
// selector-blind, `degradable` would be true and the request would spend the
// queue wait (and fall through to the env token) instead of a fast 429.
const a = seed('sel-rl');
markRateLimited(a.apiKey, 3 * 60 * 60 * 1000, FREE_SELECTOR, 'r');
const up = stubUpstream();
const result = await handleChatCompletions(
{ model: FREE_SELECTOR, stream: false, messages: [{ role: 'user', content: 'hi' }] },
// Bound the regression path: without the fix this test would wait the full
// 30s queue before answering.
{ callerKey: '', budget: createRequestBudget({ deadlineMs: 2_000 }) },
);
assert.equal(result.status, 429, 'a selector-cooled pool must fast-fail, not queue');
assert.equal(result.body.error.type, 'rate_limit_exceeded');
assert.ok(result.body.error.retry_after_ms >= 60 * 1000, 'the upstream reset is advertised');
assert.ok(result.headers['Retry-After'], 'sets the Retry-After header');
assert.equal(up.hits, 0, 'no upstream turn and no env-token fallback');
});
});