From 131aebcde5142f2053247e9abbf79964e8e3223d Mon Sep 17 00:00:00 2001 From: Xiangzhe Date: Tue, 18 Aug 2026 07:56:56 -0300 Subject: [PATCH] =?UTF-8?q?fix(compression):=20gate=20de=20est=C3=A1gio=20?= =?UTF-8?q?n=C3=A3o=20derruba=20o=20pipeline=20com=20engine=20sem=20metada?= =?UTF-8?q?ta?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `assertValidEngine()` valida id, apply, compress, getConfigSchema e validateConfig — não exige `metadata`. Uma engine registrada sem esse campo é, portanto, um registro legal. Mas `canRunAtCompressionStage` lia `engine.metadata.executionStages` sem guarda, então essa engine legal derrubava o pipeline inteiro com `TypeError: Cannot read properties of undefined` em vez de falhar aberto, que é o contrato da compressão. Metadata ausente é o mesmo caso de "não declarou executionStages" e passa a cair no mesmo fallback documentado: só pre-translation. Isso destravava também `tests/unit/compression/pipeline-circuit-breaker.test.ts`, que registra uma engine de teste sem metadata e vinha 8/9 na base — agora 9/9. O teste novo torna o contrato explícito, em vez de deixá-lo dependendo de uma reprodução incidental noutro arquivo. --- .../services/compression/strategySelector.ts | 6 +- .../engine-stage-gate-metadata.test.ts | 72 +++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 tests/unit/compression/engine-stage-gate-metadata.test.ts diff --git a/open-sse/services/compression/strategySelector.ts b/open-sse/services/compression/strategySelector.ts index 76a67d5fcb..7fd80a0694 100644 --- a/open-sse/services/compression/strategySelector.ts +++ b/open-sse/services/compression/strategySelector.ts @@ -800,7 +800,11 @@ function canRunAtCompressionStage( stage: CompressionStage | undefined ): boolean { const effectiveStage = stage ?? "pre-translation"; - const stages = engine.metadata.executionStages; + // `assertValidEngine` não exige `metadata`, então uma engine registrada sem + // esse campo é legal — e sem a guarda derrubava o pipeline inteiro com + // TypeError em vez de falhar aberto. Metadata ausente é o mesmo caso de "não + // declarou estágio" e cai no mesmo fallback: só pre-translation. + const stages = engine.metadata?.executionStages; return stages ? stages.includes(effectiveStage) : effectiveStage === "pre-translation"; } diff --git a/tests/unit/compression/engine-stage-gate-metadata.test.ts b/tests/unit/compression/engine-stage-gate-metadata.test.ts new file mode 100644 index 0000000000..b0456fcae3 --- /dev/null +++ b/tests/unit/compression/engine-stage-gate-metadata.test.ts @@ -0,0 +1,72 @@ +/** + * Gate de estágio × engine sem `metadata`. + * + * `assertValidEngine()` (engines/registry.ts) valida `id`, `apply`, `compress`, + * `getConfigSchema` e `validateConfig` — NÃO exige `metadata`. Uma engine sem + * esse campo é, portanto, um registro legal. Mas `canRunAtCompressionStage` + * lia `engine.metadata.executionStages` sem guarda, então essa engine legal + * derrubava o pipeline inteiro com `TypeError: Cannot read properties of + * undefined` — em vez de falhar aberto, que é o contrato da compressão. + * + * O fallback documentado para quem não declara `executionStages` é "só + * pre-translation". Metadata ausente é o mesmo caso de "não declarou", e tem de + * cair no mesmo fallback. + */ +import { describe, it, beforeEach, afterEach } from "node:test"; +import assert from "node:assert/strict"; + +import { + registerCompressionEngine, + unregisterCompressionEngine, +} from "../../../open-sse/services/compression/engines/registry.ts"; +import { applyStackedCompression } from "../../../open-sse/services/compression/strategySelector.ts"; +import type { CompressionEngine } from "../../../open-sse/services/compression/engines/types.ts"; + +const ENGINE_ID = "metadata-less-test-engine"; + +const body = () => ({ messages: [{ role: "user", content: "hello world" }] }); + +describe("stage gate — engine sem metadata", () => { + beforeEach(() => { + // Registro deliberadamente sem `metadata`: é o que assertValidEngine aceita. + registerCompressionEngine({ + id: ENGINE_ID, + name: "metadata-less test engine", + targets: ["messages"], + stackable: true, + apply(input: Record) { + return { body: input, compressed: false, stats: null }; + }, + compress() { + return { text: "", stats: null }; + }, + getConfigSchema() { + return []; + }, + validateConfig() { + return { valid: true, errors: [] }; + }, + } as unknown as CompressionEngine); + }); + + afterEach(() => unregisterCompressionEngine(ENGINE_ID)); + + it("não derruba o pipeline no estágio pré-tradução (default)", () => { + const r = applyStackedCompression(body(), [{ engine: ENGINE_ID }]); + assert.equal(r.compressed, false); + assert.deepEqual(r.body, body()); + }); + + it("não derruba o pipeline no estágio pós-tradução — pula pelo fallback", () => { + const r = applyStackedCompression(body(), [{ engine: ENGINE_ID }], { + compressionStage: "post-translation", + }); + assert.equal(r.compressed, false); + assert.deepEqual(r.body, body()); + // Fallback documentado: quem não declara estágio só roda pre-translation. + assert.ok( + r.stats?.validationWarnings?.some((w) => w.includes(ENGINE_ID) && w.includes("skipped")), + `esperado aviso de skip por estágio, veio ${JSON.stringify(r.stats?.validationWarnings)}` + ); + }); +});