Files
OmniRoute/tests/unit/custom-endpoint-system-prompt.test.ts
Xore 1c19e2c034 fix(compression): place output-style instruction in top-level system, not messages[0] (#13383)
* fix(compression): place output-style instruction in top-level system, not messages[0]

Anthropic-shaped bodies reject a synthetic system-role entry unshifted at
messages[0] (the claude passthrough forwards messages unchanged, and the
upstream API requires system content in the top-level system parameter).
Output styles and the caveman output mode now land their instruction in
the top-level system field when it exists (string or content-block array)
and only fall back to a trailing system message for OpenAI-shaped bodies,
which the claude system-role extraction hoists. Custom endpoint system
prompts skip the unshift whenever a top-level system field is present.

All compression options stay enabled; placement-only fix.

Fixes #12584

* test(compression): cover system-instruction placement branches 4 and 6

Adds direct unit cases for the placement ladder branches that had no
coverage, asserting where the instruction lands:

- branch 4: merge into a system message at index >= 1, leaving
  messages[0] untouched (plus the skip-over-a-block-content system
  message variant).
- branch 6: trailing append when the body has neither a `system` field
  nor a string-content system message.
- block-array `system`: appends one block and is idempotent on a second
  pass (first coverage of the array path of the marker check).

Also normalizes a malformed non-string/non-array top-level `system`
(e.g. `null`) to "" before the format branch in injectSystemPrompt and
injectCustomSystemPrompt. Previously such a body entered the `system`
branch, matched neither format, and silently dropped the prompt without
falling back to the messages path. Both new guards fail without this
change.

* chore(compression): add changelog fragment for top-level system placement
2026-09-18 11:31:27 -03:00

173 lines
7.3 KiB
TypeScript

/**
* TDD test for per-endpoint custom system prompt injection (port of upstream #2063).
*
* Validates that injectCustomSystemPrompt():
* - Appends the custom prompt to an existing system message (string content)
* - Appends the custom prompt to an existing system message (array content)
* - Creates a new system message when none exists
* - Injects into Claude-style `system` field (string)
* - Injects into Claude-style `system` field (array)
* - Does NOT inject when prompt is empty string
* - Does NOT inject when body has _skipSystemPrompt flag
* - Leaves body unchanged when prompt is falsy
*
* Also validates settings defaults include customSystemPromptEnabled / customSystemPrompt.
*/
import test from "node:test";
import assert from "node:assert/strict";
const { injectCustomSystemPrompt } = await import("../../open-sse/services/systemPrompt.ts");
// ─── injectCustomSystemPrompt ────────────────────────────────────────────────
test("injectCustomSystemPrompt: appends to existing string system message", () => {
const body = {
messages: [
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: "Hello" },
],
};
const result = injectCustomSystemPrompt(body, "Always respond formally.");
const sysMsg = result.messages.find((m) => m.role === "system");
assert.ok(sysMsg, "system message must exist");
assert.ok(
typeof sysMsg.content === "string" && sysMsg.content.includes("You are a helpful assistant."),
"original content preserved"
);
assert.ok(
typeof sysMsg.content === "string" && sysMsg.content.includes("Always respond formally."),
"custom prompt appended"
);
});
test("injectCustomSystemPrompt: appends to existing array-content system message", () => {
const body = {
messages: [
{ role: "system", content: [{ type: "text", text: "Base instructions." }] },
{ role: "user", content: "Hi" },
],
};
const result = injectCustomSystemPrompt(body, "Speak like a pirate.");
const sysMsg = result.messages.find((m) => m.role === "system");
assert.ok(sysMsg, "system message must exist");
assert.ok(Array.isArray(sysMsg.content), "content must remain array");
const texts = (sysMsg.content as Array<{ type: string; text: string }>).map((c) => c.text);
assert.ok(texts.includes("Base instructions."), "original block preserved");
assert.ok(texts.includes("Speak like a pirate."), "custom prompt block appended");
});
test("injectCustomSystemPrompt: creates system message when none exists", () => {
const body = {
messages: [{ role: "user", content: "What is 2+2?" }],
};
const result = injectCustomSystemPrompt(body, "Think step by step.");
assert.equal(result.messages[0].role, "system", "system message prepended");
assert.equal(result.messages[0].content, "Think step by step.", "prompt is the content");
assert.equal(result.messages[1].role, "user", "user message preserved after system");
});
test("injectCustomSystemPrompt: injects into Claude-style string system field", () => {
const body = {
system: "You are Claude.",
messages: [{ role: "user", content: "Hello" }],
};
const result = injectCustomSystemPrompt(body, "Be concise.");
assert.ok(typeof result.system === "string", "system field still a string");
assert.ok(result.system.includes("You are Claude."), "original system preserved");
assert.ok(result.system.includes("Be concise."), "custom prompt appended to system");
});
test("injectCustomSystemPrompt: injects into Claude-style array system field", () => {
const body = {
system: [{ type: "text", text: "You are Claude." }],
messages: [{ role: "user", content: "Hello" }],
};
const result = injectCustomSystemPrompt(body, "Be concise.");
assert.ok(Array.isArray(result.system), "system field still an array");
const texts = (result.system as Array<{ type: string; text: string }>).map((c) => c.text);
assert.ok(texts.includes("You are Claude."), "original system block preserved");
assert.ok(texts.includes("Be concise."), "custom prompt block appended");
});
test("injectCustomSystemPrompt: no-op when prompt is empty string", () => {
const body = {
messages: [{ role: "user", content: "Hello" }],
};
const result = injectCustomSystemPrompt(body, "");
assert.deepEqual(result, body, "body unchanged when prompt is empty");
});
test("injectCustomSystemPrompt: no-op when body has _skipSystemPrompt flag", () => {
const body = {
_skipSystemPrompt: true,
messages: [{ role: "user", content: "Hello" }],
};
const result = injectCustomSystemPrompt(body as Record<string, unknown>, "Be formal.");
assert.deepEqual(result, body, "body unchanged when _skipSystemPrompt is set");
});
test("injectCustomSystemPrompt: does not mutate input body (immutable)", () => {
const body = {
messages: [
{ role: "system", content: "Original." },
{ role: "user", content: "Hi" },
],
};
const original = JSON.stringify(body);
injectCustomSystemPrompt(body, "New instruction.");
assert.equal(JSON.stringify(body), original, "input body not mutated");
});
// ─── Settings defaults ───────────────────────────────────────────────────────
// Verify that getSettings() returns the expected defaults for the new keys.
// Uses an in-memory DB (DATA_DIR set to a temp path so no production DB is touched).
import { tmpdir } from "node:os";
import { join } from "node:path";
import { mkdirSync } from "node:fs";
const tmpDir = join(tmpdir(), `omniroute-test-${Date.now()}`);
mkdirSync(tmpDir, { recursive: true });
process.env["DATA_DIR"] = tmpDir;
test("settings defaults include customSystemPromptEnabled=false and customSystemPrompt=''", async (t) => {
// Dynamic import after setting DATA_DIR to avoid polluting other tests
const { resetDbInstance } = await import("../../src/lib/db/core.ts");
const { getSettings } = await import("../../src/lib/db/settings.ts");
const settings = await getSettings();
assert.equal(
settings.customSystemPromptEnabled,
false,
"customSystemPromptEnabled default is false"
);
assert.equal(settings.customSystemPrompt, "", "customSystemPrompt default is empty string");
t.after(() => {
resetDbInstance();
});
});
test("injectCustomSystemPrompt: claude-shaped body keeps prompt out of messages[0] (#12584)", () => {
const body = {
system: "You are Claude Code.",
messages: [{ role: "user", content: "Hello" }],
};
const result = injectCustomSystemPrompt(body, "Always respond formally.");
assert.equal(result.messages.length, 1);
assert.equal(result.messages[0].role, "user");
assert.ok(String(result.system).includes("Always respond formally."));
});
test("injectCustomSystemPrompt: malformed null system still receives the prompt (#12584)", () => {
const body = {
system: null,
messages: [{ role: "user", content: "Hello" }],
};
const result = injectCustomSystemPrompt(body, "Always respond formally.");
assert.equal(result.system, "Always respond formally.", "prompt is not silently dropped");
assert.equal(result.messages.length, 1, "prompt does not leak into messages");
assert.equal(result.messages[0].role, "user");
});