mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-26 09:02:11 +03:00
* fix(tests): drain three base-reds left by the SSE-comment default and a locale gap All three reproduce on a pristine tip; none is caused by the branch that found them. 1. i18n vi — six keys landed in en.json without a Vietnamese counterpart (settings.reasoningTokenBuffer*, settings.zeroLatencyOptimizations*, settings.compressionOutputStyle.i-have-adhd.*). The vi locale is held to strict parity, so the whole i18n-vi suite went red. Translated; no existing key reordered. 2. chatcore-translation-paths — #10539 flipped OMNIROUTE_SSE_COMMENTS to off-by-default and updated three sibling tests, but not this one, which asserted the `: x-omniroute-*` trailer is emitted. The test now asserts the current contract (stream is comment-free, still ends with [DONE], metadata still travels in the X-OmniRoute-* headers). The opt-in half stays covered by sse-comments-optout-9305.test.ts, which drives the env var through all three states. Enabling the flag inside this file instead leaks process.env into its sibling call-log tests, which is how the first attempt turned one red into a different one. 3. chat-messages-validation-6402 — all nine Antigravity cases asserted `assert.match(body, /ok/)` against the mocked model output. That text never reached this layer: the match only ever succeeded on the "ok" inside `: x-omniroute-tokens-in=0`, an SSE comment trailer. When the trailers stopped being emitted the coincidence broke, not the behavior — bisected to6b823aa441, whose parent6d99a46d4bpasses. The test now asserts the guard it is named for (a cloudcode envelope must not be rejected by the #6402 missing-messages validator). Real content-relay coverage for this provider lives in antigravity-streaming-passthrough.test.ts, which passes. Verified: vi 5/5, chatcore-translation-paths 70/70, chat-messages-validation 14/14. * chore(changelog): correct the fragment to the real PR number (#10704) --------- Co-authored-by: Xiangzhe <bakryun0718@proton.me>
206 lines
7.4 KiB
TypeScript
206 lines
7.4 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import {
|
|
ANTIGRAVITY_MODEL_ALIASES,
|
|
ANTIGRAVITY_PUBLIC_MODELS,
|
|
} from "../../open-sse/config/antigravityModelAliases.ts";
|
|
import { AGY_PUBLIC_MODELS } from "../../open-sse/config/agyModels.ts";
|
|
import { createChatPipelineHarness } from "../integration/_chatPipelineHarness.ts";
|
|
|
|
// Regression tests for #6402 — schema-invalid `messages` fields fell through
|
|
// the existing empty-array guard and reached model resolution, where an
|
|
// unresolvable model surfaced as a misleading 404 `model_not_found` from
|
|
// chatHelpers.ts (`No active credentials for provider: <p>`).
|
|
//
|
|
// The guard at src/sse/handlers/chat.ts now rejects three additional cases with
|
|
// a clear OmniRoute-level 400 before any routing or upstream call:
|
|
// - present-but-null messages
|
|
// - present-but-non-array messages (number, string, object)
|
|
// - missing messages when the Responses-API `input` discriminator is also
|
|
// absent.
|
|
|
|
const harness = await createChatPipelineHarness("chat-messages-validation-6402");
|
|
const { handleChat, buildRequest, resetStorage, seedConnection } = harness;
|
|
|
|
test.beforeEach(async () => {
|
|
await resetStorage();
|
|
});
|
|
|
|
test.after(async () => {
|
|
await harness.cleanup();
|
|
});
|
|
|
|
test("#6402: messages: null is rejected with a clear 400 before model resolution", async () => {
|
|
await seedConnection("anthropic", { apiKey: "sk-ant" });
|
|
|
|
let upstreamCalled = false;
|
|
globalThis.fetch = async () => {
|
|
upstreamCalled = true;
|
|
return new Response("{}", { status: 200, headers: { "content-type": "application/json" } });
|
|
};
|
|
|
|
const response = await handleChat(
|
|
buildRequest({
|
|
body: {
|
|
model: "anthropic/claude-haiku-4-5",
|
|
messages: null,
|
|
},
|
|
})
|
|
);
|
|
|
|
assert.equal(response.status, 400, "null messages must be a 400, not a 404 model_not_found");
|
|
const body = (await response.json()) as { error?: { message?: string } };
|
|
assert.match(body.error?.message ?? "", /messages.*Expected array/i);
|
|
assert.equal(upstreamCalled, false, "must not forward a null-messages request upstream");
|
|
});
|
|
|
|
test("#6402: messages: <number> is rejected with a clear 400 before model resolution", async () => {
|
|
await seedConnection("anthropic", { apiKey: "sk-ant" });
|
|
|
|
let upstreamCalled = false;
|
|
globalThis.fetch = async () => {
|
|
upstreamCalled = true;
|
|
return new Response("{}", { status: 200, headers: { "content-type": "application/json" } });
|
|
};
|
|
|
|
const response = await handleChat(
|
|
buildRequest({
|
|
body: {
|
|
model: "anthropic/claude-haiku-4-5",
|
|
messages: 123 as unknown as [],
|
|
},
|
|
})
|
|
);
|
|
|
|
assert.equal(response.status, 400, "non-array messages must be a 400, not a 404 model_not_found");
|
|
const body = (await response.json()) as { error?: { message?: string } };
|
|
assert.match(body.error?.message ?? "", /messages.*Expected array/i);
|
|
assert.equal(upstreamCalled, false, "must not forward a non-array-messages request upstream");
|
|
});
|
|
|
|
test("#6402: missing messages (and no Responses-API input) is rejected with a clear 400", async () => {
|
|
await seedConnection("anthropic", { apiKey: "sk-ant" });
|
|
|
|
let upstreamCalled = false;
|
|
globalThis.fetch = async () => {
|
|
upstreamCalled = true;
|
|
return new Response("{}", { status: 200, headers: { "content-type": "application/json" } });
|
|
};
|
|
|
|
const response = await handleChat(
|
|
buildRequest({
|
|
body: {
|
|
model: "anthropic/claude-haiku-4-5",
|
|
},
|
|
})
|
|
);
|
|
|
|
assert.equal(response.status, 400, "missing messages must be a 400, not a 404 model_not_found");
|
|
const body = (await response.json()) as { error?: { message?: string } };
|
|
assert.match(body.error?.message ?? "", /messages.*Expected array/i);
|
|
assert.equal(upstreamCalled, false, "must not forward a missing-messages request upstream");
|
|
});
|
|
|
|
test("#6402: Responses-API input passes the guard (messages discriminator preserved)", async () => {
|
|
await seedConnection("openai", { apiKey: "sk-openai" });
|
|
|
|
const response = await handleChat(
|
|
buildRequest({
|
|
url: "http://localhost/v1/responses",
|
|
body: {
|
|
model: "openai/gpt-4.1",
|
|
input: [{ role: "user", content: "Hello" }],
|
|
},
|
|
})
|
|
);
|
|
|
|
// The guard must not fire for Responses-API requests; whatever downstream
|
|
// status this produces (200, 404, etc.), it MUST NOT be the guard's
|
|
// "messages: Expected array" 400.
|
|
if (response.status === 400) {
|
|
const body = (await response.json()) as { error?: { message?: string } };
|
|
assert.doesNotMatch(
|
|
body.error?.message ?? "",
|
|
/messages.*Expected array/i,
|
|
"Responses-API request must not be caught by the messages guard"
|
|
);
|
|
}
|
|
});
|
|
|
|
const ANTIGRAVITY_GEMINI_MODELS = Array.from(
|
|
new Set(
|
|
[
|
|
...ANTIGRAVITY_PUBLIC_MODELS.map((model) => model.id),
|
|
...AGY_PUBLIC_MODELS.map((model) => model.id),
|
|
...Object.keys(ANTIGRAVITY_MODEL_ALIASES),
|
|
...Object.values(ANTIGRAVITY_MODEL_ALIASES),
|
|
].filter((model) => /^(gemini(?!-claude)|rev19)/.test(model))
|
|
)
|
|
).sort();
|
|
|
|
for (const model of ANTIGRAVITY_GEMINI_MODELS) {
|
|
test(`Antigravity cloudcode envelope routes without the missing-messages 400: ${model}`, async () => {
|
|
await seedConnection("antigravity", {
|
|
accessToken: "ag-access",
|
|
providerSpecificData: { projectId: "test-project" },
|
|
});
|
|
|
|
let upstreamCalled = false;
|
|
globalThis.fetch = async () => {
|
|
upstreamCalled = true;
|
|
return new Response(
|
|
'data: {"response":{"candidates":[{"content":{"parts":[{"text":"ok"}]},"finishReason":"STOP"}]}}\n\n',
|
|
{ status: 200, headers: { "Content-Type": "text/event-stream" } }
|
|
);
|
|
};
|
|
|
|
const response = await handleChat(
|
|
buildRequest({
|
|
url: "http://localhost/v1/antigravity",
|
|
body: {
|
|
model: `antigravity/${model}`,
|
|
project: "projects/test",
|
|
request: {
|
|
contents: [{ role: "user", parts: [{ text: "Hello" }] }],
|
|
},
|
|
},
|
|
})
|
|
);
|
|
|
|
assert.equal(upstreamCalled, true, "Antigravity request should reach the upstream executor");
|
|
assert.equal(response.status, 200, "Antigravity cloudcode envelopes must not return 400");
|
|
|
|
// This assertion is the guard the test is named for: a cloudcode envelope has no
|
|
// `messages` key, so it must not be rejected by the #6402 validator.
|
|
//
|
|
// It used to read `assert.match(body, /ok/)`, aimed at the mocked upstream's
|
|
// "ok" text — but that text never reached this layer. The match only ever
|
|
// succeeded on the "ok" inside `: x-omniroute-tokens-in=0`, an SSE *comment*
|
|
// trailer. When #10539 flipped OMNIROUTE_SSE_COMMENTS to off-by-default the
|
|
// trailers stopped being emitted, the body went empty, and the coincidence —
|
|
// not the behavior — broke. Content relay for this provider is covered for
|
|
// real, at the executor level, by antigravity-streaming-passthrough.test.ts.
|
|
const body = await response.text();
|
|
assert.doesNotMatch(
|
|
body,
|
|
/messages.*Expected array/i,
|
|
"cloudcode envelope must not be caught by the missing-messages guard"
|
|
);
|
|
});
|
|
}
|
|
|
|
test("Antigravity Gemini-family regression covers every current callable tier", () => {
|
|
const coveredModels = new Set(ANTIGRAVITY_GEMINI_MODELS);
|
|
for (const model of [
|
|
"gemini-pro-agent",
|
|
"gemini-3.1-pro-low",
|
|
"gemini-3.7-flash-high",
|
|
"gemini-3.7-flash-medium",
|
|
"gemini-3.7-flash-low",
|
|
"gemini-3.1-flash-lite",
|
|
]) {
|
|
assert.ok(coveredModels.has(model), `expected regression coverage for ${model}`);
|
|
}
|
|
});
|