mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-16 20:02:45 +03:00
Validado numa worktree combinada com as 16 PRs desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-file-size e check-changelog-integrity OK, complexity 2788/3218 e cognitive 1261/1437, ESLint 0 erros nos 152 arquivos alterados, e a suíte vitest:ui completa (2149) verde. Sobre esta PR especificamente: rodei os **23 arquivos de teste** que ela toca sobre o tip final, depois do merge da base — **392/392**. A migration `174_server_tool_executions.sql` não colide (o tip está em 173, e você já a renumerou em `c35f0fd7`). O dono foi consultado antes do merge, porque o loop está atrás da flag `SERVER_OWNED_TOOL_LOOP_ENABLED` mas o primeiro send não-streaming mudou de dono sem flag, e a verificação manual em combo com Memory continuava desmarcada. A condição dele foi: entra se os testes focados passarem aqui. Passaram. O lock de passthrough (`fetchCalls.length === 1`) é a parte que mais me convenceu — o double-dispatch que um `if (stream)` em volta do send existente causaria é exatamente o tipo de regressão que não aparece em teste de comportamento, só em contagem de chamada. **Três ajustes meus na sua branch:** 1. `tests/unit/chatcore-stream-error-result.test.ts` procurava `"const legResult = await runNonStreamingProviderLeg"`, mas o seu commit final `6077b9dd` passou a reatribuir `legResult` e trocou para `let`. O guard falhava na sua própria branch (confirmei que o arquivo e o `chatCore.ts` eram byte-idênticos ao head da PR, então não era efeito da leva). Passou a aceitar `const|let` — a intenção do guard é o try/catch em volta da chamada, não a palavra-chave. 2. `tests/integration/skills-pipeline.test.ts` foi de 1156 para 1338 linhas e estourou o `testCap` de 1200. Segui o mesmo caminho que você já tinha tomado em `a1d2d20d` para os testes unitários: extraí os três casos do server-owned tool loop para `tests/integration/server-owned-tool-loop-pipeline.test.ts` (259 linhas), com instância própria do harness. O glob `tests/integration/*.test.ts` pega o arquivo novo sem registro adicional. 3/3 verdes isolados. 3. O arquivo novo herdou cinco `any` do original — que só passavam por estarem congelados no `eslint-suppressions.json` sob o nome antigo. Tipei como `Record<string, unknown>`. E `tests/unit/non-streaming-finalization.test.ts` tinha dois argumentos não usados em `trackPendingRequest`, agora prefixados com `_`. Nada disso toca produção nem enfraquece asserção.
633 lines
22 KiB
TypeScript
633 lines
22 KiB
TypeScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import type {
|
|
ChatCoreExecutorResult,
|
|
PipelineConnectionContext,
|
|
PipelineStateHooks,
|
|
PipelineTargetContext,
|
|
PipelineWireState,
|
|
ProviderExecutionPipelineInput,
|
|
ProviderExecutionPolicy,
|
|
} from "../../open-sse/handlers/chatCore/providerExecutionPipeline.ts";
|
|
|
|
test("runProviderExecutionPipeline is importable", async () => {
|
|
const mod = await import("../../open-sse/handlers/chatCore/providerExecutionPipeline.ts");
|
|
assert.equal(typeof mod.runProviderExecutionPipeline, "function");
|
|
});
|
|
|
|
function jsonResponse(body: unknown, status: number, extraHeaders: Record<string, string> = {}) {
|
|
return new Response(JSON.stringify(body), {
|
|
status,
|
|
headers: { "content-type": "application/json", ...extraHeaders },
|
|
});
|
|
}
|
|
|
|
function makeAttempt(
|
|
body: unknown,
|
|
status: number,
|
|
extra: Partial<ChatCoreExecutorResult> = {}
|
|
): ChatCoreExecutorResult {
|
|
const response = jsonResponse(body, status, extra.headers as Record<string, string> | undefined);
|
|
return {
|
|
response,
|
|
url: extra.url ?? "https://upstream.test/v1/chat/completions",
|
|
headers: extra.headers ?? { "content-type": "application/json" },
|
|
transformedBody: extra.transformedBody ?? { model: "gpt-5" },
|
|
...extra,
|
|
};
|
|
}
|
|
|
|
function noopState(): PipelineStateHooks {
|
|
return {
|
|
updatePendingStage: () => {},
|
|
recordRateLimitHeaders: () => {},
|
|
recordRateLimitBody: () => {},
|
|
writeTerminalStatus: async () => {},
|
|
persistConnectionPatch: () => {},
|
|
setConnectionRateLimitedUntil: () => {},
|
|
lockModel: () => {},
|
|
recordAntigravityQuotaState: async () => {},
|
|
markAccountSemaphoreBlocked: () => {},
|
|
isolateProbeFailures: () => false,
|
|
};
|
|
}
|
|
|
|
function makeInput(opts: {
|
|
policy: ProviderExecutionPolicy;
|
|
provider: string;
|
|
model?: string;
|
|
stream?: boolean;
|
|
connectionId?: string;
|
|
send: (model: string, allowDedup: boolean) => Promise<ChatCoreExecutorResult>;
|
|
getProviderCredentials?: PipelineConnectionContext["getProviderCredentials"];
|
|
replaceCredentials?: PipelineConnectionContext["replaceCredentials"];
|
|
getCurrentConnectionId?: () => string | undefined;
|
|
refreshCredentials?: PipelineConnectionContext["refreshCredentials"];
|
|
onCredentialsRefreshed?: PipelineConnectionContext["onCredentialsRefreshed"];
|
|
getNextFamilyFallback?: ProviderExecutionPipelineInput["getNextFamilyFallback"];
|
|
state?: Partial<PipelineStateHooks>;
|
|
}): ProviderExecutionPipelineInput {
|
|
const model = opts.model ?? "gpt-5";
|
|
const connectionId = opts.connectionId ?? "conn-a";
|
|
let currentId: string | undefined = connectionId;
|
|
let credentials: Record<string, unknown> = { connectionId };
|
|
const target: PipelineTargetContext = {
|
|
provider: opts.provider,
|
|
requestedModel: model,
|
|
sourceFormat: "openai",
|
|
targetFormat: "openai",
|
|
stream: opts.stream ?? false,
|
|
};
|
|
const wire: PipelineWireState = {
|
|
body: { model, messages: [{ role: "user", content: "hi" }] },
|
|
currentModel: model,
|
|
triedModels: new Set([model]),
|
|
setBodyAndModel: (body, nextModel) => {
|
|
wire.body = body;
|
|
wire.currentModel = nextModel;
|
|
wire.triedModels.add(nextModel);
|
|
},
|
|
};
|
|
const connection: PipelineConnectionContext = {
|
|
initialConnectionId: connectionId,
|
|
getCurrentConnectionId: opts.getCurrentConnectionId ?? (() => currentId),
|
|
getCredentials: () => credentials,
|
|
replaceCredentials:
|
|
opts.replaceCredentials ??
|
|
((next) => {
|
|
credentials = next;
|
|
currentId = typeof next.connectionId === "string" ? next.connectionId : currentId;
|
|
}),
|
|
onCredentialsRefreshed: opts.onCredentialsRefreshed ?? (() => {}),
|
|
assertManagedLeaseFence: () => {},
|
|
refreshCredentials: opts.refreshCredentials,
|
|
getProviderCredentials:
|
|
opts.getProviderCredentials ??
|
|
(async () => {
|
|
throw new Error("getProviderCredentials must not be called in this fixture");
|
|
}),
|
|
};
|
|
return {
|
|
policy: opts.policy,
|
|
target,
|
|
connection,
|
|
wire,
|
|
state: { ...noopState(), ...(opts.state || {}) },
|
|
sendProviderAttempt: opts.send,
|
|
getNextFamilyFallback: opts.getNextFamilyFallback,
|
|
};
|
|
}
|
|
|
|
test("initial Codex 429: rotation resolver>=1 and successful retry", async () => {
|
|
const { runProviderExecutionPipeline } = await import(
|
|
"../../open-sse/handlers/chatCore/providerExecutionPipeline.ts"
|
|
);
|
|
let sendCount = 0;
|
|
let resolverCallCount = 0;
|
|
const input = makeInput({
|
|
policy: { allowAccountRotation: true, allowModelFallback: true, expectedConnectionId: undefined },
|
|
provider: "codex",
|
|
connectionId: "conn-a",
|
|
send: async () => {
|
|
sendCount += 1;
|
|
if (sendCount === 1) {
|
|
return makeAttempt({ error: { message: "rate limited", type: "rate_limit_error" } }, 429, {
|
|
headers: { "retry-after": "1" },
|
|
});
|
|
}
|
|
return makeAttempt({
|
|
id: "chatcmpl-ok",
|
|
choices: [{ message: { role: "assistant", content: "rotated" }, finish_reason: "stop" }],
|
|
}, 200);
|
|
},
|
|
getProviderCredentials: (async () => {
|
|
resolverCallCount += 1;
|
|
return { connectionId: "conn-b", allRateLimited: false };
|
|
}) as PipelineConnectionContext["getProviderCredentials"],
|
|
});
|
|
|
|
const outcome = await runProviderExecutionPipeline(input);
|
|
assert.equal(resolverCallCount >= 1, true, "resolver must run on initial Codex 429");
|
|
assert.equal(sendCount, 2, "second send after rotation");
|
|
assert.equal(outcome.kind, "response");
|
|
if (outcome.kind === "response") {
|
|
assert.equal(outcome.connectionId, "conn-b");
|
|
assert.equal(outcome.response.status, 200);
|
|
}
|
|
});
|
|
|
|
test("initial Antigravity 422 gcp_project_required: rotation resolver>=1 and successful retry", async () => {
|
|
const { runProviderExecutionPipeline } = await import(
|
|
"../../open-sse/handlers/chatCore/providerExecutionPipeline.ts"
|
|
);
|
|
let sendCount = 0;
|
|
let resolverCallCount = 0;
|
|
const input = makeInput({
|
|
policy: { allowAccountRotation: true, allowModelFallback: true },
|
|
provider: "antigravity",
|
|
connectionId: "agy-a",
|
|
send: async () => {
|
|
sendCount += 1;
|
|
if (sendCount === 1) {
|
|
return makeAttempt({ error: { message: "gcp_project_required", type: "invalid_request" } }, 422);
|
|
}
|
|
return makeAttempt(
|
|
{
|
|
id: "chatcmpl-ok",
|
|
choices: [{ message: { role: "assistant", content: "rotated" }, finish_reason: "stop" }],
|
|
},
|
|
200
|
|
);
|
|
},
|
|
getProviderCredentials: (async () => {
|
|
resolverCallCount += 1;
|
|
return { connectionId: "agy-b", allRateLimited: false };
|
|
}) as PipelineConnectionContext["getProviderCredentials"],
|
|
});
|
|
|
|
const outcome = await runProviderExecutionPipeline(input);
|
|
assert.equal(resolverCallCount >= 1, true, "resolver must run on initial Antigravity BYOP 422");
|
|
assert.equal(sendCount, 2, "second send after BYOP rotation");
|
|
assert.equal(outcome.kind, "response");
|
|
if (outcome.kind === "response") {
|
|
assert.equal(outcome.connectionId, "agy-b");
|
|
assert.equal(outcome.response.status, 200);
|
|
}
|
|
});
|
|
|
|
test("follow-up rotation blocks resolver on Antigravity 422", async () => {
|
|
const { runProviderExecutionPipeline } = await import(
|
|
"../../open-sse/handlers/chatCore/providerExecutionPipeline.ts"
|
|
);
|
|
let sendCount = 0;
|
|
let resolverCallCount = 0;
|
|
const input = makeInput({
|
|
policy: {
|
|
allowAccountRotation: false,
|
|
allowModelFallback: false,
|
|
expectedConnectionId: "agy-a",
|
|
},
|
|
provider: "antigravity",
|
|
connectionId: "agy-a",
|
|
send: async () => {
|
|
sendCount += 1;
|
|
return makeAttempt({ error: { message: "gcp_project_required", type: "invalid_request" } }, 422);
|
|
},
|
|
getProviderCredentials: (async () => {
|
|
resolverCallCount += 1;
|
|
return { connectionId: "agy-b", allRateLimited: false };
|
|
}) as PipelineConnectionContext["getProviderCredentials"],
|
|
});
|
|
|
|
const outcome = await runProviderExecutionPipeline(input);
|
|
assert.equal(resolverCallCount, 0, "follow-up must not call credentials resolver");
|
|
assert.equal(sendCount, 1, "follow-up sends once");
|
|
assert.equal(outcome.kind, "error");
|
|
if (outcome.kind === "error") {
|
|
assert.equal(outcome.result.status, 422);
|
|
assert.equal(outcome.connectionId, "agy-a");
|
|
}
|
|
});
|
|
|
|
test("follow-up rotation blocks resolver on Codex 429", async () => {
|
|
const { runProviderExecutionPipeline } = await import(
|
|
"../../open-sse/handlers/chatCore/providerExecutionPipeline.ts"
|
|
);
|
|
let sendCount = 0;
|
|
let resolverCallCount = 0;
|
|
const input = makeInput({
|
|
policy: {
|
|
allowAccountRotation: false,
|
|
allowModelFallback: false,
|
|
expectedConnectionId: "conn-a",
|
|
},
|
|
provider: "codex",
|
|
connectionId: "conn-a",
|
|
send: async () => {
|
|
sendCount += 1;
|
|
return makeAttempt({ error: { message: "rate limited", type: "rate_limit_error" } }, 429);
|
|
},
|
|
getProviderCredentials: (async () => {
|
|
resolverCallCount += 1;
|
|
return { connectionId: "conn-b", allRateLimited: false };
|
|
}) as PipelineConnectionContext["getProviderCredentials"],
|
|
});
|
|
|
|
const outcome = await runProviderExecutionPipeline(input);
|
|
assert.equal(resolverCallCount, 0, "follow-up must not call credentials resolver");
|
|
assert.equal(sendCount, 1, "follow-up sends once");
|
|
assert.equal(outcome.kind, "error");
|
|
if (outcome.kind === "error") {
|
|
assert.equal(outcome.result.status, 429);
|
|
assert.equal(outcome.connectionId, "conn-a");
|
|
}
|
|
});
|
|
|
|
test("401 refresh succeeds then retries once on same connection", async () => {
|
|
const { runProviderExecutionPipeline } = await import(
|
|
"../../open-sse/handlers/chatCore/providerExecutionPipeline.ts"
|
|
);
|
|
let sendCount = 0;
|
|
let refreshCount = 0;
|
|
let persistCount = 0;
|
|
let resolverCallCount = 0;
|
|
const input = makeInput({
|
|
policy: { allowAccountRotation: true, allowModelFallback: true },
|
|
provider: "openai",
|
|
connectionId: "conn-a",
|
|
send: async () => {
|
|
sendCount += 1;
|
|
if (sendCount === 1) {
|
|
return makeAttempt({ error: { message: "invalid_api_key", type: "authentication_error" } }, 401);
|
|
}
|
|
return makeAttempt(
|
|
{
|
|
id: "chatcmpl-ok",
|
|
choices: [{ message: { role: "assistant", content: "refreshed" }, finish_reason: "stop" }],
|
|
},
|
|
200
|
|
);
|
|
},
|
|
refreshCredentials: async (creds) => {
|
|
refreshCount += 1;
|
|
return { ...creds, accessToken: "new-token" };
|
|
},
|
|
onCredentialsRefreshed: async () => {
|
|
persistCount += 1;
|
|
},
|
|
getProviderCredentials: (async () => {
|
|
resolverCallCount += 1;
|
|
return { connectionId: "conn-b", allRateLimited: false };
|
|
}) as PipelineConnectionContext["getProviderCredentials"],
|
|
});
|
|
|
|
const outcome = await runProviderExecutionPipeline(input);
|
|
assert.equal(refreshCount, 1, "refresh once");
|
|
assert.equal(persistCount, 1, "onCredentialsRefreshed once");
|
|
assert.equal(resolverCallCount, 0, "401 refresh must not rotate accounts");
|
|
assert.equal(sendCount, 2, "retry once after refresh");
|
|
assert.equal(outcome.kind, "response");
|
|
if (outcome.kind === "response") {
|
|
assert.equal(outcome.connectionId, "conn-a");
|
|
assert.equal(outcome.response.status, 200);
|
|
}
|
|
});
|
|
|
|
test("status restatement rewrites agentrouter 403 quota exhaustion to 429 before classification", async () => {
|
|
const { runProviderExecutionPipeline } = await import(
|
|
"../../open-sse/handlers/chatCore/providerExecutionPipeline.ts"
|
|
);
|
|
let sendCount = 0;
|
|
const input = makeInput({
|
|
policy: { allowAccountRotation: true, allowModelFallback: true },
|
|
provider: "agentrouter",
|
|
connectionId: "ar-a",
|
|
send: async () => {
|
|
sendCount += 1;
|
|
return makeAttempt({ error: { message: "用户额度不足", type: "forbidden" } }, 403);
|
|
},
|
|
});
|
|
|
|
const outcome = await runProviderExecutionPipeline(input);
|
|
assert.equal(sendCount, 1);
|
|
assert.equal(outcome.kind, "error");
|
|
if (outcome.kind === "error") {
|
|
assert.equal(outcome.result.status, 429, "restated before classification");
|
|
assert.equal(outcome.connectionId, "ar-a");
|
|
}
|
|
});
|
|
|
|
test("thinking-signature recovery returns winning response", async () => {
|
|
const { runProviderExecutionPipeline } = await import(
|
|
"../../open-sse/handlers/chatCore/providerExecutionPipeline.ts"
|
|
);
|
|
let sendCount = 0;
|
|
const input = makeInput({
|
|
policy: { allowAccountRotation: true, allowModelFallback: true },
|
|
provider: "claude",
|
|
connectionId: "cl-a",
|
|
send: async () => {
|
|
sendCount += 1;
|
|
if (sendCount === 1) {
|
|
return makeAttempt(
|
|
{ error: { message: "invalid signature in thinking block", type: "invalid_request_error" } },
|
|
400
|
|
);
|
|
}
|
|
return makeAttempt(
|
|
{
|
|
id: "msg-ok",
|
|
type: "message",
|
|
role: "assistant",
|
|
content: [{ type: "text", text: "recovered" }],
|
|
},
|
|
200
|
|
);
|
|
},
|
|
});
|
|
input.wire.body = {
|
|
model: "gpt-5",
|
|
messages: [
|
|
{ role: "user", content: "q1" },
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "thinking", thinking: "old" },
|
|
{ type: "text", text: "a1" },
|
|
],
|
|
},
|
|
{ role: "user", content: "q2" },
|
|
],
|
|
};
|
|
|
|
const outcome = await runProviderExecutionPipeline(input);
|
|
assert.equal(sendCount, 2, "one recovery send after signature error");
|
|
assert.equal(outcome.kind, "response");
|
|
if (outcome.kind === "response") {
|
|
assert.equal(outcome.response.status, 200);
|
|
assert.equal(outcome.connectionId, "cl-a");
|
|
}
|
|
});
|
|
|
|
test("initial model-unavailable falls back to sibling model", async () => {
|
|
const { runProviderExecutionPipeline } = await import(
|
|
"../../open-sse/handlers/chatCore/providerExecutionPipeline.ts"
|
|
);
|
|
let sendCount = 0;
|
|
let fallbackLookupCount = 0;
|
|
const sentModels: string[] = [];
|
|
const input = makeInput({
|
|
policy: { allowAccountRotation: true, allowModelFallback: true },
|
|
provider: "openai",
|
|
model: "gpt-5",
|
|
connectionId: "conn-a",
|
|
send: async (model) => {
|
|
sendCount += 1;
|
|
sentModels.push(model);
|
|
if (model === "gpt-5") {
|
|
return makeAttempt(
|
|
{ error: { message: "model is not available", type: "invalid_request_error" } },
|
|
404
|
|
);
|
|
}
|
|
return makeAttempt(
|
|
{
|
|
id: "chatcmpl-ok",
|
|
choices: [{ message: { role: "assistant", content: "fallback" }, finish_reason: "stop" }],
|
|
},
|
|
200
|
|
);
|
|
},
|
|
getNextFamilyFallback: (current) => {
|
|
fallbackLookupCount += 1;
|
|
return current === "gpt-5" ? "gpt-5-mini" : null;
|
|
},
|
|
});
|
|
|
|
const outcome = await runProviderExecutionPipeline(input);
|
|
assert.equal(fallbackLookupCount >= 1, true, "family fallback consulted");
|
|
assert.deepEqual(sentModels, ["gpt-5", "gpt-5-mini"]);
|
|
assert.equal(sendCount, 2);
|
|
assert.equal(outcome.kind, "response");
|
|
if (outcome.kind === "response") {
|
|
assert.equal(outcome.model, "gpt-5-mini");
|
|
assert.equal(outcome.response.status, 200);
|
|
}
|
|
});
|
|
|
|
test("follow-up allowModelFallback=false blocks model-unavailable fallback", async () => {
|
|
const { runProviderExecutionPipeline } = await import(
|
|
"../../open-sse/handlers/chatCore/providerExecutionPipeline.ts"
|
|
);
|
|
let sendCount = 0;
|
|
let fallbackLookupCount = 0;
|
|
const input = makeInput({
|
|
policy: {
|
|
allowAccountRotation: false,
|
|
allowModelFallback: false,
|
|
expectedConnectionId: "conn-a",
|
|
},
|
|
provider: "openai",
|
|
model: "gpt-5",
|
|
connectionId: "conn-a",
|
|
send: async () => {
|
|
sendCount += 1;
|
|
return makeAttempt(
|
|
{ error: { message: "model is not available", type: "invalid_request_error" } },
|
|
404
|
|
);
|
|
},
|
|
getNextFamilyFallback: () => {
|
|
fallbackLookupCount += 1;
|
|
return "gpt-5-mini";
|
|
},
|
|
});
|
|
|
|
const outcome = await runProviderExecutionPipeline(input);
|
|
assert.equal(fallbackLookupCount, 0, "follow-up must not consult family fallback");
|
|
assert.equal(sendCount, 1);
|
|
assert.equal(outcome.kind, "error");
|
|
if (outcome.kind === "error") {
|
|
assert.equal(outcome.result.status, 404);
|
|
assert.equal(outcome.model, "gpt-5");
|
|
}
|
|
});
|
|
|
|
test("Codex 429 rotation calls scope-rate-limit, affinity-clear, and audit hooks", async () => {
|
|
const { runProviderExecutionPipeline } = await import(
|
|
"../../open-sse/handlers/chatCore/providerExecutionPipeline.ts"
|
|
);
|
|
const rateLimited: Array<Record<string, unknown>> = [];
|
|
const affinityCleared: string[] = [];
|
|
const audits: Array<Record<string, unknown>> = [];
|
|
let sendCount = 0;
|
|
const input = makeInput({
|
|
policy: { allowAccountRotation: true, allowModelFallback: true },
|
|
provider: "codex",
|
|
connectionId: "conn-a",
|
|
send: async () => {
|
|
sendCount += 1;
|
|
if (sendCount === 1) {
|
|
return makeAttempt({ error: { message: "rate limited", type: "rate_limit_error" } }, 429, {
|
|
headers: { "retry-after": "2" },
|
|
});
|
|
}
|
|
return makeAttempt(
|
|
{
|
|
id: "chatcmpl-ok",
|
|
choices: [{ message: { role: "assistant", content: "rotated" }, finish_reason: "stop" }],
|
|
},
|
|
200
|
|
);
|
|
},
|
|
getProviderCredentials: (async () => ({
|
|
connectionId: "conn-b",
|
|
allRateLimited: false,
|
|
})) as PipelineConnectionContext["getProviderCredentials"],
|
|
state: {
|
|
onCodexScopeRateLimited: (params) => {
|
|
rateLimited.push(params as unknown as Record<string, unknown>);
|
|
},
|
|
onClearSessionAffinity: (params) => {
|
|
affinityCleared.push(params.failedConnectionId);
|
|
},
|
|
onAuditAccountRotation: (params) => {
|
|
audits.push(params as unknown as Record<string, unknown>);
|
|
},
|
|
},
|
|
});
|
|
|
|
const outcome = await runProviderExecutionPipeline(input);
|
|
assert.equal(outcome.kind, "response");
|
|
assert.equal(rateLimited.length, 1, "must persist Codex model-scope cooldown");
|
|
assert.equal(rateLimited[0]?.failedConnectionId, "conn-a");
|
|
assert.deepEqual(affinityCleared, ["conn-a"]);
|
|
assert.equal(audits.length, 1);
|
|
assert.equal(audits[0]?.action, "codex.account_rotation");
|
|
assert.equal(audits[0]?.failedConnectionId, "conn-a");
|
|
assert.equal(audits[0]?.newConnectionId, "conn-b");
|
|
});
|
|
|
|
test("Codex 429 cooldown reads Retry-After from the response, not request headers", async () => {
|
|
const { runProviderExecutionPipeline } = await import(
|
|
"../../open-sse/handlers/chatCore/providerExecutionPipeline.ts"
|
|
);
|
|
const rateLimited: Array<Record<string, unknown>> = [];
|
|
let sendCount = 0;
|
|
const input = makeInput({
|
|
policy: { allowAccountRotation: true, allowModelFallback: true },
|
|
provider: "codex",
|
|
connectionId: "conn-a",
|
|
send: async () => {
|
|
sendCount += 1;
|
|
if (sendCount === 1) {
|
|
// BaseExecutor puts REQUEST headers on attempt.headers (Authorization).
|
|
// Upstream Retry-After lives on the Response. Mixing the two bags is the
|
|
// extract regression: cooldown silently falls back to 60s.
|
|
return {
|
|
response: jsonResponse(
|
|
{ error: { message: "rate limited", type: "rate_limit_error" } },
|
|
429,
|
|
{ "Retry-After": "5" }
|
|
),
|
|
url: "https://upstream.test/v1/chat/completions",
|
|
headers: { Authorization: "Bearer request-token", "content-type": "application/json" },
|
|
transformedBody: { model: "gpt-5" },
|
|
};
|
|
}
|
|
return makeAttempt(
|
|
{
|
|
id: "chatcmpl-ok",
|
|
choices: [{ message: { role: "assistant", content: "rotated" }, finish_reason: "stop" }],
|
|
},
|
|
200
|
|
);
|
|
},
|
|
getProviderCredentials: (async () => ({
|
|
connectionId: "conn-b",
|
|
allRateLimited: false,
|
|
})) as PipelineConnectionContext["getProviderCredentials"],
|
|
state: {
|
|
onCodexScopeRateLimited: (params) => {
|
|
rateLimited.push(params as unknown as Record<string, unknown>);
|
|
},
|
|
},
|
|
});
|
|
|
|
const outcome = await runProviderExecutionPipeline(input);
|
|
assert.equal(outcome.kind, "response");
|
|
assert.equal(rateLimited.length, 1, "must persist Codex model-scope cooldown");
|
|
const until = new Date(String(rateLimited[0]?.rateLimitedUntil)).getTime();
|
|
const delta = until - Date.now();
|
|
assert.ok(
|
|
delta > 4_000 && delta < 8_000,
|
|
`Retry-After: 5 must yield ~5s cooldown, got ${delta}ms (60s = still reading request headers)`
|
|
);
|
|
});
|
|
|
|
test("Antigravity BYOP 422 rotation persists cooldown via setConnectionRateLimitedUntil", async () => {
|
|
const { runProviderExecutionPipeline } = await import(
|
|
"../../open-sse/handlers/chatCore/providerExecutionPipeline.ts"
|
|
);
|
|
const cooldowns: Array<{ id: string; untilMs: number | null }> = [];
|
|
let sendCount = 0;
|
|
const input = makeInput({
|
|
policy: { allowAccountRotation: true, allowModelFallback: true },
|
|
provider: "antigravity",
|
|
connectionId: "agy-a",
|
|
send: async () => {
|
|
sendCount += 1;
|
|
if (sendCount === 1) {
|
|
return makeAttempt(
|
|
{ error: { message: "gcp_project_required", type: "invalid_request" } },
|
|
422
|
|
);
|
|
}
|
|
return makeAttempt(
|
|
{
|
|
id: "chatcmpl-ok",
|
|
choices: [{ message: { role: "assistant", content: "rotated" }, finish_reason: "stop" }],
|
|
},
|
|
200
|
|
);
|
|
},
|
|
getProviderCredentials: (async () => ({
|
|
connectionId: "agy-b",
|
|
allRateLimited: false,
|
|
})) as PipelineConnectionContext["getProviderCredentials"],
|
|
state: {
|
|
setConnectionRateLimitedUntil: (id, untilMs) => {
|
|
cooldowns.push({ id, untilMs });
|
|
},
|
|
},
|
|
});
|
|
|
|
const outcome = await runProviderExecutionPipeline(input);
|
|
assert.equal(outcome.kind, "response");
|
|
assert.equal(sendCount, 2);
|
|
assert.equal(cooldowns.length, 1, "BYOP rotate must persist cooldown before picking sibling");
|
|
assert.equal(cooldowns[0]?.id, "agy-a");
|
|
assert.equal(typeof cooldowns[0]?.untilMs, "number");
|
|
assert.equal((cooldowns[0]?.untilMs ?? 0) > Date.now(), true);
|
|
});
|