Files
diegosouzapw--omniroute/tests/unit/chat-non-string-model-6407.test.ts
2026-07-13 13:39:12 +08:00

124 lines
3.9 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import { createChatPipelineHarness } from "../integration/_chatPipelineHarness.ts";
// Regression test for #6407 — a `model` field of a non-string type
// (`number`/`boolean`/`array`/`object`) crashed downstream string ops
// (`.toLowerCase()`/`.split()`/`.startsWith()`) and returned HTTP 500 with an
// empty body, bypassing the error sanitizer (Hard Rule #12).
//
// The guard rejects non-string `model` with a clean 400 + typed error message
// BEFORE the model resolver runs, matching the #5110 empty-messages precedent.
const harness = await createChatPipelineHarness("chat-non-string-model-6407");
const { handleChat, buildRequest, resetStorage, seedConnection } = harness;
test.beforeEach(async () => {
await resetStorage();
});
test.after(async () => {
await harness.cleanup();
});
for (const [label, value, expectedType] of [
["number", 123, "number"],
["boolean", true, "boolean"],
["array", [], "array"],
["object", {}, "object"],
] as const) {
test(`#6407: model as ${label} → 400 with typed error, no upstream call`, async () => {
await seedConnection("openai", { apiKey: "sk-openai" });
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: value,
messages: [{ role: "user", content: "hi" }],
},
})
);
assert.equal(response.status, 400, `${label} model must be a 400, not 500`);
const body = (await response.json()) as { error?: { message?: string } };
assert.match(
body.error?.message ?? "",
/model:\s*Expected string, received/i,
`error should say "model: Expected string, received ${expectedType}"`
);
assert.ok(
body.error?.message?.includes(expectedType),
`error should include the received type "${expectedType}"`
);
// Sanitized: no leaked stack frames per Hard Rule #12 / #6407 impact 3.
assert.ok(
!(body.error?.message ?? "").includes("at /"),
"error must not leak stack trace frames"
);
assert.equal(upstreamCalled, false, "must not forward a non-string-model request upstream");
});
}
test("#6407: string model still routes normally (guard is not over-broad)", async () => {
await seedConnection("openai", { apiKey: "sk-openai" });
let upstreamCalled = false;
globalThis.fetch = async () => {
upstreamCalled = true;
return Response.json({
id: "x",
object: "chat.completion",
choices: [
{ index: 0, message: { role: "assistant", content: "hi" }, finish_reason: "stop" },
],
});
};
const response = await handleChat(
buildRequest({
body: {
model: "openai/gpt-4.1",
stream: false,
messages: [{ role: "user", content: "Hello" }],
},
})
);
assert.notEqual(response.status, 400, "a valid string model must not be caught by the guard");
assert.equal(upstreamCalled, true, "a valid request must still reach upstream");
});
test("#6407: null model still routed to the existing 'Missing model' 400 (not the new guard)", async () => {
await seedConnection("openai", { apiKey: "sk-openai" });
globalThis.fetch = async () =>
new Response("{}", { status: 200, headers: { "content-type": "application/json" } });
const response = await handleChat(
buildRequest({
body: {
model: null,
messages: [{ role: "user", content: "hi" }],
},
})
);
assert.equal(response.status, 400, "null model stays a 400");
const body = (await response.json()) as { error?: { message?: string } };
assert.match(
body.error?.message ?? "",
/missing model/i,
"null model keeps the existing 'Missing model' message (not the new type guard)"
);
});