Files
OmniRoute/tests/unit/chatcore-stream-error-result.test.ts
Bob.Hou d6f315018a fix(chat): continue after a server-owned tool on Chat Completions (#12867)
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.
2026-09-07 09:15:00 -03:00

93 lines
4.3 KiB
TypeScript

// tests/unit/chatcore-stream-error-result.test.ts
// Characterization of isSemaphoreCapacityError / createStreamingErrorResult /
// getUpstreamErrorIdentifier — streaming error-result helpers extracted from handleChatCore
// (chatCore god-file decomposition, #3501). Locks the semaphore code matching, the SSE error
// envelope shape (status, headers, `data: {...}\n\ndata: [DONE]\n\n` body, optional code/type), and
// the string-code extraction.
import { test } from "node:test";
import assert from "node:assert/strict";
import {
isSemaphoreCapacityError,
createStreamingErrorResult,
getUpstreamErrorIdentifier,
} from "../../open-sse/handlers/chatCore/streamErrorResult.ts";
test("isSemaphoreCapacityError matches the two semaphore codes only", () => {
assert.equal(isSemaphoreCapacityError({ code: "SEMAPHORE_TIMEOUT" }), true);
assert.equal(isSemaphoreCapacityError({ code: "SEMAPHORE_QUEUE_FULL" }), true);
assert.equal(isSemaphoreCapacityError({ code: "OTHER" }), false);
assert.equal(isSemaphoreCapacityError(null), false);
assert.equal(isSemaphoreCapacityError("SEMAPHORE_TIMEOUT"), false);
});
test("createStreamingErrorResult builds an SSE error envelope with [DONE] terminator", async () => {
const result = createStreamingErrorResult(503, "boom");
assert.equal(result.success, false);
assert.equal(result.status, 503);
assert.equal(result.error, "boom");
assert.equal(result.response.status, 503);
assert.equal(result.response.headers.get("Content-Type"), "text/event-stream");
assert.equal(result.response.headers.get("X-Accel-Buffering"), "no");
const body = await result.response.text();
assert.ok(body.startsWith("data: "));
assert.ok(body.endsWith("data: [DONE]\n\n"));
const json = JSON.parse(body.slice("data: ".length, body.indexOf("\n\n")));
assert.equal(json.error.message, "boom");
});
test("createStreamingErrorResult attaches optional code and type", async () => {
const result = createStreamingErrorResult(429, "slow down", "rate_limited", "rate_limit_error");
const body = await result.response.text();
const json = JSON.parse(body.slice("data: ".length, body.indexOf("\n\n")));
assert.equal(json.error.code, "rate_limited");
assert.equal(json.error.type, "rate_limit_error");
});
test("createStreamingErrorResult sanitizes code and type at the SSE boundary", async () => {
const result = createStreamingErrorResult(
502,
"upstream failed",
"sk-live-secret-value",
"server_error\nX-Leak: yes"
);
const body = await result.response.text();
const json = JSON.parse(body.slice("data: ".length, body.indexOf("\n\n"))) as {
error: { code: string; type: string };
};
assert.equal(json.error.code, "bad_gateway");
assert.equal(json.error.type, "server_error");
assert.doesNotMatch(body, /sk-live-secret-value|X-Leak/);
});
test("getUpstreamErrorIdentifier returns a non-empty string code or undefined", () => {
assert.equal(getUpstreamErrorIdentifier({ code: "ECONNRESET" }), "ECONNRESET");
assert.equal(getUpstreamErrorIdentifier({ code: "" }), undefined);
assert.equal(getUpstreamErrorIdentifier({ code: 123 }), undefined);
assert.equal(getUpstreamErrorIdentifier(null), undefined);
assert.equal(getUpstreamErrorIdentifier("ECONNRESET"), undefined);
});
test("non-streaming runNonStreamingProviderLeg is inside a try that maps semaphore errors", async () => {
const fs = await import("node:fs");
const src = fs.readFileSync("open-sse/handlers/chatCore.ts", "utf8");
// `let`, not `const`, since 6077b9dd (#12867) made the finalization step reassign
// legResult. The guard is about the try/catch that wraps the call, not the keyword.
const idx = src.search(/(?:const|let) legResult = await runNonStreamingProviderLeg/);
assert.ok(idx >= 0, "non-streaming branch must exist");
const start = src.lastIndexOf("if (!stream)", idx);
const end = src.indexOf("// Streaming response", idx);
assert.ok(start >= 0 && end > start, "non-stream block bounds");
const block = src.slice(start, end);
assert.match(
block,
/try\s*\{[\s\S]*runNonStreamingProviderLeg/,
"non-stream leg must sit in a try so SEMAPHORE_TIMEOUT cannot escape handleChatCore"
);
assert.match(
block,
/isSemaphoreCapacityError/,
"same catch that maps stream semaphore errors must cover the non-stream leg"
);
});