fix(rtk): make system prompt injection format-safe and idempotent
Caveman/Ponytail injection now matches each target wire format instead of assuming an OpenAI-shaped body: - Chat arrays append a text block; Responses arrays append input_text and create typed message items - Claude inserts before the final cache-control block; Gemini preserves the snake/camel systemInstruction wrapper - Kiro updates systemPrompt and its mirrored first-user prefix atomically, rolling back if the pair fails to converge - Format label decides Claude/Gemini before the wire-shape sniff, since their bodies also carry messages[]/contents[] and Anthropic rejects a "system" role inside messages[] - Delimiter-aware dedup makes injection exact-idempotent across retries, so distinct prompts sharing a long prefix are no longer collapsed - Every write is fail-open on frozen or proxied bodies Saver order and X-9Router-Token-Saver: off behavior are unchanged. Fixes #3202.
This commit is contained in:
@@ -3,96 +3,335 @@
|
||||
// native-passthrough flows. Used by caveman.js and ponytail.js.
|
||||
|
||||
import { FORMATS } from "../translator/formats.js";
|
||||
import { OPENAI_BLOCK, CLAUDE_BLOCK, RESPONSES_ITEM } from "../translator/schema/blocks.js";
|
||||
import { ROLE } from "../translator/schema/roles.js";
|
||||
|
||||
const SEP = "\n\n";
|
||||
|
||||
export function injectSystemPrompt(body, format, prompt) {
|
||||
if (!body || !prompt) return;
|
||||
try {
|
||||
if (!body || !prompt) return;
|
||||
if (typeof body !== "object") return;
|
||||
|
||||
switch (format) {
|
||||
case FORMATS.CLAUDE:
|
||||
// Kiro wire shape is unique (conversationState/systemPrompt) — handle directly.
|
||||
if (isKiroBody(body) || format === FORMATS.KIRO) {
|
||||
injectKiroSystem(body, prompt);
|
||||
return;
|
||||
}
|
||||
|
||||
// Claude/Gemini own a dedicated system field, yet their bodies also carry
|
||||
// messages[]/contents[] — decide by format label before the shape sniff below.
|
||||
// Anthropic rejects a "system" role inside messages[] (no such input role).
|
||||
if (format === FORMATS.CLAUDE) {
|
||||
injectClaudeSystem(body, prompt);
|
||||
return;
|
||||
case FORMATS.GEMINI:
|
||||
case FORMATS.GEMINI_CLI:
|
||||
case FORMATS.VERTEX:
|
||||
case FORMATS.ANTIGRAVITY:
|
||||
}
|
||||
if (format === FORMATS.GEMINI || format === FORMATS.GEMINI_CLI
|
||||
|| format === FORMATS.VERTEX || format === FORMATS.ANTIGRAVITY) {
|
||||
// Antigravity wraps Gemini shape in body.request → injectGeminiSystem handles it
|
||||
injectGeminiSystem(body, prompt);
|
||||
return;
|
||||
default:
|
||||
// OpenAI and OpenAI-shaped formats (responses/codex/cursor/kiro/ollama)
|
||||
injectMessagesSystem(body, prompt);
|
||||
}
|
||||
}
|
||||
|
||||
// OpenAI-shaped: messages[] (chat) or input[] (responses) or instructions (responses string)
|
||||
function injectMessagesSystem(body, prompt) {
|
||||
// OpenAI Responses API: top-level string field
|
||||
if (typeof body.instructions === "string") {
|
||||
body.instructions = body.instructions
|
||||
? `${body.instructions}${SEP}${prompt}`
|
||||
: prompt;
|
||||
return;
|
||||
}
|
||||
|
||||
const arr = Array.isArray(body.messages) ? body.messages
|
||||
: Array.isArray(body.input) ? body.input
|
||||
: null;
|
||||
if (!arr) return;
|
||||
|
||||
const idx = arr.findIndex(m => m && (m.role === "system" || m.role === "developer"));
|
||||
if (idx >= 0) {
|
||||
appendToOpenAIMessage(arr[idx], prompt);
|
||||
} else {
|
||||
arr.unshift({ role: "system", content: prompt });
|
||||
}
|
||||
}
|
||||
|
||||
function appendToOpenAIMessage(msg, prompt) {
|
||||
if (typeof msg.content === "string") {
|
||||
msg.content = `${msg.content}${SEP}${prompt}`;
|
||||
} else if (Array.isArray(msg.content)) {
|
||||
// Responses-style array of parts {type:"input_text"|"text", text}
|
||||
msg.content.push({ type: "input_text", text: prompt });
|
||||
} else {
|
||||
msg.content = prompt;
|
||||
}
|
||||
}
|
||||
|
||||
// Claude shape: body.system as string | array of {type:"text", text}
|
||||
// Insert before the last cache_control block to keep injection inside the cached prefix.
|
||||
function injectClaudeSystem(body, prompt) {
|
||||
if (typeof body.system === "string" && body.system.length > 0) {
|
||||
body.system = `${body.system}${SEP}${prompt}`;
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(body.system)) {
|
||||
const block = { type: "text", text: prompt };
|
||||
let lastCacheIdx = -1;
|
||||
for (let i = body.system.length - 1; i >= 0; i--) {
|
||||
if (body.system[i]?.cache_control) { lastCacheIdx = i; break; }
|
||||
}
|
||||
if (lastCacheIdx >= 0) {
|
||||
body.system.splice(lastCacheIdx, 0, block);
|
||||
|
||||
// Dispatch by actual wire shape for OpenAI-shaped formats.
|
||||
// instructions string takes precedence; messages[] means Chat; input[] means Responses.
|
||||
if (typeof body.instructions === "string") {
|
||||
injectInstructionsSystem(body, prompt);
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(body.messages)) {
|
||||
injectChatSystem(body, prompt);
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(body.input)) {
|
||||
// Responses input[]: empty array already normalized elsewhere; string stays untouched here
|
||||
injectResponsesInputSystem(body, prompt);
|
||||
return;
|
||||
}
|
||||
if (typeof body.input === "string") {
|
||||
// string input must stay untouched
|
||||
return;
|
||||
}
|
||||
|
||||
// OpenAI-shaped but no array (e.g. empty body) — no-op
|
||||
} catch (_) {
|
||||
// fail-open
|
||||
}
|
||||
}
|
||||
|
||||
function isKiroBody(body) {
|
||||
if (!body || typeof body !== "object") return false;
|
||||
if (typeof body.systemPrompt !== "string") return false;
|
||||
const cs = body.conversationState;
|
||||
if (!cs || typeof cs !== "object") return false;
|
||||
return Array.isArray(cs.history) || !!(cs.currentMessage && typeof cs.currentMessage === "object");
|
||||
}
|
||||
|
||||
// Exact idempotency: prompt present as its own SEP-delimited segment (or the
|
||||
// whole string), not as a substring of unrelated text.
|
||||
function hasPrompt(haystack, prompt) {
|
||||
if (!haystack || typeof haystack !== "string") return false;
|
||||
if (haystack === prompt) return true;
|
||||
return haystack.split(SEP).includes(prompt);
|
||||
}
|
||||
|
||||
function dedupStringAppend(curr, prompt) {
|
||||
if (!curr) return prompt;
|
||||
if (hasPrompt(curr, prompt)) return curr;
|
||||
return `${curr}${SEP}${prompt}`;
|
||||
}
|
||||
|
||||
// ---- OpenAI instructions string ----
|
||||
function injectInstructionsSystem(body, prompt) {
|
||||
try {
|
||||
const curr = body.instructions;
|
||||
if (typeof curr !== "string") return;
|
||||
if (hasPrompt(curr, prompt)) return;
|
||||
const next = curr ? `${curr}${SEP}${prompt}` : prompt;
|
||||
try { body.instructions = next; } catch (_) { /* frozen/proxy fail-open */ }
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// ---- Chat messages[] ----
|
||||
function injectChatSystem(body, prompt) {
|
||||
try {
|
||||
const arr = body.messages;
|
||||
if (!Array.isArray(arr)) return;
|
||||
// Exact idempotency: scan existing system/developer content for full prompt
|
||||
if (containsPromptInMessages(arr, prompt)) return;
|
||||
let idx = -1;
|
||||
try { idx = arr.findIndex(m => m && (m.role === ROLE.SYSTEM || m.role === ROLE.DEVELOPER)); } catch (_) { return; }
|
||||
if (idx >= 0) {
|
||||
appendToChatMessage(arr[idx], prompt);
|
||||
} else {
|
||||
body.system.push(block);
|
||||
// create typed system message at index 0; fail-open on frozen/proxy
|
||||
try { arr.unshift({ role: ROLE.SYSTEM, content: prompt }); } catch (_) {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
body.system = prompt;
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// Gemini shape: body.system_instruction | body.systemInstruction | body.request.systemInstruction
|
||||
// Each shape: { parts: [{ text }] }
|
||||
function injectGeminiSystem(body, prompt) {
|
||||
const target = body.request && typeof body.request === "object" ? body.request : body;
|
||||
const useSnake = Object.prototype.hasOwnProperty.call(target, "system_instruction");
|
||||
const key = useSnake ? "system_instruction" : "systemInstruction";
|
||||
const sys = target[key];
|
||||
if (sys && Array.isArray(sys.parts)) {
|
||||
sys.parts.push({ text: prompt });
|
||||
return;
|
||||
}
|
||||
target[key] = { parts: [{ text: prompt }] };
|
||||
function containsPromptInMessages(arr, prompt) {
|
||||
try {
|
||||
for (const m of arr) {
|
||||
if (!m || (m.role !== ROLE.SYSTEM && m.role !== ROLE.DEVELOPER)) continue;
|
||||
const c = m.content;
|
||||
if (typeof c === "string" && hasPrompt(c, prompt)) return true;
|
||||
if (Array.isArray(c)) {
|
||||
for (const part of c) {
|
||||
if (part && typeof part.text === "string" && hasPrompt(part.text, prompt)) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
function appendToChatMessage(msg, prompt) {
|
||||
try {
|
||||
if (!msg || typeof msg !== "object") return;
|
||||
const c = msg.content;
|
||||
if (typeof c === "string") {
|
||||
const next = dedupStringAppend(c, prompt);
|
||||
if (next === c) return;
|
||||
// avoid partial mutation: try assignment, bail if setter throws
|
||||
try { msg.content = next; } catch (_) {}
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(c)) {
|
||||
// already deduped at message level; but guard block-level too
|
||||
try {
|
||||
if (c.some(b => b && b.text === prompt)) return;
|
||||
} catch (_) {}
|
||||
try { c.push({ type: OPENAI_BLOCK.TEXT, text: prompt }); } catch (_) {}
|
||||
return;
|
||||
}
|
||||
try { msg.content = prompt; } catch (_) {}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// ---- Responses input[] ----
|
||||
function injectResponsesInputSystem(body, prompt) {
|
||||
try {
|
||||
const arr = body.input;
|
||||
if (!Array.isArray(arr)) return;
|
||||
// instructions already handled above
|
||||
if (containsPromptInResponsesInput(arr, prompt)) return;
|
||||
// find system/developer message items only (type === message)
|
||||
let idx = -1;
|
||||
try {
|
||||
idx = arr.findIndex(m => m && m.type === RESPONSES_ITEM.MESSAGE && (m.role === ROLE.SYSTEM || m.role === ROLE.DEVELOPER));
|
||||
} catch (_) { return; }
|
||||
if (idx >= 0) {
|
||||
appendToResponsesMessage(arr[idx], prompt);
|
||||
} else {
|
||||
const msg = { type: RESPONSES_ITEM.MESSAGE, role: ROLE.SYSTEM, content: [{ type: RESPONSES_ITEM.INPUT_TEXT, text: prompt }] };
|
||||
try { arr.unshift(msg); } catch (_) {}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function containsPromptInResponsesInput(arr, prompt) {
|
||||
try {
|
||||
for (const item of arr) {
|
||||
if (!item || item.type !== RESPONSES_ITEM.MESSAGE) continue;
|
||||
if (item.role !== ROLE.SYSTEM && item.role !== ROLE.DEVELOPER) continue;
|
||||
const c = item.content;
|
||||
if (typeof c === "string" && hasPrompt(c, prompt)) return true;
|
||||
if (Array.isArray(c)) {
|
||||
for (const part of c) {
|
||||
if (part && typeof part.text === "string" && hasPrompt(part.text, prompt)) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
function appendToResponsesMessage(msg, prompt) {
|
||||
try {
|
||||
if (!msg || typeof msg !== "object") return;
|
||||
const c = msg.content;
|
||||
if (typeof c === "string") {
|
||||
const next = dedupStringAppend(c, prompt);
|
||||
if (next === c) return;
|
||||
try { msg.content = next; } catch (_) {}
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(c)) {
|
||||
try { if (c.some(b => b && b.text === prompt)) return; } catch (_) {}
|
||||
try { c.push({ type: RESPONSES_ITEM.INPUT_TEXT, text: prompt }); } catch (_) {}
|
||||
return;
|
||||
}
|
||||
try { msg.content = [{ type: RESPONSES_ITEM.INPUT_TEXT, text: prompt }]; } catch (_) {}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// ---- Claude ----
|
||||
function injectClaudeSystem(body, prompt) {
|
||||
try {
|
||||
const sys = body.system;
|
||||
if (typeof sys === "string") {
|
||||
if (hasPrompt(sys, prompt)) return;
|
||||
const next = sys.length > 0 ? `${sys}${SEP}${prompt}` : prompt;
|
||||
try { body.system = next; } catch (_) {}
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(sys)) {
|
||||
try { if (sys.some(b => b && b.text === prompt)) return; } catch (_) {}
|
||||
const block = { type: CLAUDE_BLOCK.TEXT, text: prompt };
|
||||
let lastCacheIdx = -1;
|
||||
try {
|
||||
for (let i = sys.length - 1; i >= 0; i--) {
|
||||
if (sys[i]?.cache_control) { lastCacheIdx = i; break; }
|
||||
}
|
||||
} catch (_) {}
|
||||
try {
|
||||
if (lastCacheIdx >= 0) sys.splice(lastCacheIdx, 0, block);
|
||||
else sys.push(block);
|
||||
} catch (_) {}
|
||||
return;
|
||||
}
|
||||
// absent/null
|
||||
try { body.system = prompt; } catch (_) {}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// ---- Gemini ----
|
||||
function injectGeminiSystem(body, prompt) {
|
||||
try {
|
||||
let target = body;
|
||||
try {
|
||||
if (body.request && typeof body.request === "object") target = body.request;
|
||||
} catch (_) {}
|
||||
let useSnake = false;
|
||||
try { useSnake = Object.prototype.hasOwnProperty.call(target, "system_instruction"); } catch (_) {}
|
||||
const key = useSnake ? "system_instruction" : "systemInstruction";
|
||||
let sys;
|
||||
try { sys = target[key]; } catch (_) { sys = undefined; }
|
||||
if (sys && Array.isArray(sys.parts)) {
|
||||
try { if (sys.parts.some(p => p && p.text === prompt)) return; } catch (_) {}
|
||||
try { sys.parts.push({ text: prompt }); } catch (_) {}
|
||||
return;
|
||||
}
|
||||
try { target[key] = { parts: [{ text: prompt }] }; } catch (_) {}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// ---- Kiro ----
|
||||
// Updates top-level systemPrompt and only the mirrored leading prefix of the
|
||||
// first user history turn, else current user. next = old + SEP + prompt.
|
||||
// Replace old leading prefix only; preserve time context and user tail.
|
||||
function injectKiroSystem(body, prompt) {
|
||||
try {
|
||||
let oldPrompt = typeof body.systemPrompt === "string" ? body.systemPrompt : "";
|
||||
// Repair path: a previous partial write left systemPrompt updated but user
|
||||
// content still mirroring the pre-write prefix. Re-derive the effective old
|
||||
// prefix from content so this pass converges instead of early-returning.
|
||||
const cs0 = body.conversationState;
|
||||
let firstUser0 = cs0 && Array.isArray(cs0.history)
|
||||
? (cs0.history.find(it => it && it.userInputMessage)?.userInputMessage ?? null)
|
||||
: null;
|
||||
if (!firstUser0 && cs0?.currentMessage?.userInputMessage) firstUser0 = cs0.currentMessage.userInputMessage;
|
||||
|
||||
if (firstUser0 && typeof firstUser0.content === "string" && oldPrompt && !hasPrompt(oldPrompt, prompt)) {
|
||||
const c0 = firstUser0.content;
|
||||
if (c0 === oldPrompt || (c0.startsWith(oldPrompt) && !c0.startsWith(`${oldPrompt}${SEP}`))) {
|
||||
// systemPrompt advanced past mirrored prefix → stale; treat as un-mirrored
|
||||
oldPrompt = "";
|
||||
}
|
||||
}
|
||||
if (oldPrompt && hasPrompt(oldPrompt, prompt)) return;
|
||||
const next = oldPrompt ? `${oldPrompt}${SEP}${prompt}` : prompt;
|
||||
|
||||
// Atomicity: write user content first, then systemPrompt only if content
|
||||
// write succeeded (or was a no-op). If systemPrompt write then fails, the
|
||||
// repair heuristic above re-derives from content on retry — no permanent
|
||||
// half-applied state.
|
||||
const cs = body.conversationState;
|
||||
let targetMsg = null;
|
||||
try {
|
||||
const hist = Array.isArray(cs?.history) ? cs.history : null;
|
||||
if (hist) {
|
||||
for (const item of hist) {
|
||||
if (item && item.userInputMessage) { targetMsg = item.userInputMessage; break; }
|
||||
}
|
||||
}
|
||||
if (!targetMsg && cs?.currentMessage?.userInputMessage) {
|
||||
targetMsg = cs.currentMessage.userInputMessage;
|
||||
}
|
||||
} catch (_) { targetMsg = null; }
|
||||
|
||||
let sysWritten = false;
|
||||
try { body.systemPrompt = next; sysWritten = true; } catch (_) {}
|
||||
|
||||
const applyContent = () => {
|
||||
const content = typeof targetMsg.content === "string" ? targetMsg.content : "";
|
||||
if (oldPrompt === "") {
|
||||
// Empty old prompt: prepend unless already at head (exact, not substring)
|
||||
if (content.startsWith(prompt) || content.startsWith(next)) return;
|
||||
const newContent = content ? `${next}${SEP}${content}` : next;
|
||||
try { targetMsg.content = newContent; } catch (_) {}
|
||||
return;
|
||||
}
|
||||
if (!content.startsWith(oldPrompt)) return; // not mirrored at head — leave alone
|
||||
if (content.startsWith(next)) return; // already applied → idempotent
|
||||
const tail = content.slice(oldPrompt.length);
|
||||
try { targetMsg.content = `${next}${tail}`; } catch (_) {}
|
||||
};
|
||||
|
||||
try {
|
||||
if (targetMsg) applyContent();
|
||||
} catch (_) {}
|
||||
if (sysWritten && targetMsg) {
|
||||
// verify convergence: content should now start with next (or be un-mirrored)
|
||||
let ok = false;
|
||||
try {
|
||||
const c = targetMsg.content;
|
||||
ok = typeof c !== "string" || c.startsWith(next) || !c.startsWith(oldPrompt);
|
||||
} catch (_) {}
|
||||
if (!ok) {
|
||||
try { body.systemPrompt = oldPrompt; } catch (_) {} // rollback
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
491
tests/unit/system-inject.test.js
Normal file
491
tests/unit/system-inject.test.js
Normal file
@@ -0,0 +1,491 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { injectSystemPrompt } from "../../open-sse/rtk/systemInject.js";
|
||||
import { FORMATS } from "../../open-sse/translator/formats.js";
|
||||
import { OPENAI_BLOCK, CLAUDE_BLOCK, RESPONSES_ITEM } from "../../open-sse/translator/schema/blocks.js";
|
||||
import { ROLE } from "../../open-sse/translator/schema/roles.js";
|
||||
import { injectCaveman } from "../../open-sse/rtk/caveman.js";
|
||||
import { injectPonytail } from "../../open-sse/rtk/ponytail.js";
|
||||
import { CAVEMAN_PROMPTS } from "../../open-sse/rtk/cavemanPrompts.js";
|
||||
import { PONYTAIL_PROMPTS } from "../../open-sse/rtk/ponytailPrompt.js";
|
||||
|
||||
const SEP = "\n\n";
|
||||
const P1 = "CAVEMAN_TEST_PROMPT_AAA";
|
||||
const P2 = "PONYTAIL_TEST_PROMPT_BBB";
|
||||
|
||||
describe("system-inject chat messages", () => {
|
||||
it("appends TEXT block to existing system string with SEP", () => {
|
||||
const body = { messages: [{ role: ROLE.SYSTEM, content: "hello" }, { role: ROLE.USER, content: "hi" }] };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
expect(body.messages[0].content).toBe(`hello${SEP}${P1}`);
|
||||
});
|
||||
|
||||
it("appends TEXT block to existing system array with OPENAI_BLOCK.TEXT never input_text", () => {
|
||||
const body = { messages: [{ role: ROLE.SYSTEM, content: [{ type: OPENAI_BLOCK.TEXT, text: "hello" }] }] };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
const arr = body.messages[0].content;
|
||||
expect(arr[arr.length - 1]).toEqual({ type: OPENAI_BLOCK.TEXT, text: P1 });
|
||||
expect(arr.some(c => c.type === "input_text")).toBe(false);
|
||||
});
|
||||
|
||||
it("unshifts system message when no system/developer present", () => {
|
||||
const body = { messages: [{ role: ROLE.USER, content: "hi" }] };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
expect(body.messages[0]).toEqual({ role: ROLE.SYSTEM, content: P1 });
|
||||
expect(body.messages[1].role).toBe(ROLE.USER);
|
||||
});
|
||||
|
||||
it("handles developer role as system", () => {
|
||||
const body = { messages: [{ role: ROLE.DEVELOPER, content: "dev" }] };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
expect(body.messages[0].content).toBe(`dev${SEP}${P1}`);
|
||||
});
|
||||
|
||||
it("exact full-prompt idempotency for chat string", () => {
|
||||
const body = { messages: [{ role: ROLE.SYSTEM, content: "hello" }] };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
expect(body.messages[0].content).toBe(`hello${SEP}${P1}`);
|
||||
// different prompt both apply
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P2);
|
||||
expect(body.messages[0].content).toBe(`hello${SEP}${P1}${SEP}${P2}`);
|
||||
});
|
||||
|
||||
it("exact full-prompt idempotency for chat array", () => {
|
||||
const body = { messages: [{ role: ROLE.SYSTEM, content: [{ type: OPENAI_BLOCK.TEXT, text: "hello" }] }] };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
const texts = body.messages[0].content.filter(c => c.text === P1);
|
||||
expect(texts.length).toBe(1);
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P2);
|
||||
expect(body.messages[0].content.filter(c => c.text === P2).length).toBe(1);
|
||||
});
|
||||
|
||||
it("never uses first-100 fingerprint: long prompt exact idempotency", () => {
|
||||
const longA = "X".repeat(150) + "_A";
|
||||
const longB = "X".repeat(150) + "_B";
|
||||
const body = { messages: [{ role: ROLE.SYSTEM, content: "base" }] };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, longA);
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, longB);
|
||||
expect(body.messages[0].content).toContain(longA);
|
||||
expect(body.messages[0].content).toContain(longB);
|
||||
// retry same longA is idempotent
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, longA);
|
||||
const countA = body.messages[0].content.split(longA).length - 1;
|
||||
expect(countA).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("system-inject responses input[]", () => {
|
||||
it("modifies only type: message system/developer and preserves non-message order", () => {
|
||||
const body = {
|
||||
input: [
|
||||
{ type: RESPONSES_ITEM.FUNCTION_CALL, call_id: "c1", name: "fn" },
|
||||
{ type: RESPONSES_ITEM.MESSAGE, role: ROLE.SYSTEM, content: [{ type: RESPONSES_ITEM.INPUT_TEXT, text: "sys" }] },
|
||||
{ type: RESPONSES_ITEM.REASONING, summary: "x" },
|
||||
{ type: RESPONSES_ITEM.FUNCTION_CALL_OUTPUT, call_id: "c1", output: "ok" },
|
||||
],
|
||||
};
|
||||
const before = JSON.parse(JSON.stringify(body.input));
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
// length unchanged except injection inside message
|
||||
expect(body.input.length).toBe(before.length);
|
||||
expect(body.input[0]).toEqual(before[0]);
|
||||
expect(body.input[2]).toEqual(before[2]);
|
||||
expect(body.input[3]).toEqual(before[3]);
|
||||
// system message got INPUT_TEXT appended
|
||||
const sys = body.input[1];
|
||||
expect(sys.content[sys.content.length - 1]).toEqual({ type: RESPONSES_ITEM.INPUT_TEXT, text: P1 });
|
||||
});
|
||||
|
||||
it("appends INPUT_TEXT to array content", () => {
|
||||
const body = { input: [{ type: RESPONSES_ITEM.MESSAGE, role: ROLE.USER, content: [{ type: RESPONSES_ITEM.INPUT_TEXT, text: "hi" }] }, { type: RESPONSES_ITEM.MESSAGE, role: ROLE.SYSTEM, content: [{ type: RESPONSES_ITEM.INPUT_TEXT, text: "base" }] }] };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
const sys = body.input.find(m => m.role === ROLE.SYSTEM);
|
||||
expect(sys.content[sys.content.length - 1].type).toBe(RESPONSES_ITEM.INPUT_TEXT);
|
||||
expect(sys.content[sys.content.length - 1].text).toBe(P1);
|
||||
});
|
||||
|
||||
it("creates typed message at index 0 if absent preserving order", () => {
|
||||
const body = { input: [{ type: RESPONSES_ITEM.MESSAGE, role: ROLE.USER, content: [{ type: RESPONSES_ITEM.INPUT_TEXT, text: "hi" }] }, { type: RESPONSES_ITEM.FUNCTION_CALL, call_id: "1", name: "a" }] };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
expect(body.input[0]).toEqual({ type: RESPONSES_ITEM.MESSAGE, role: ROLE.SYSTEM, content: [{ type: RESPONSES_ITEM.INPUT_TEXT, text: P1 }] });
|
||||
expect(body.input[1].role).toBe(ROLE.USER);
|
||||
});
|
||||
|
||||
it("instructions string takes precedence over input[]", () => {
|
||||
const body = { instructions: "instr", input: [{ type: RESPONSES_ITEM.MESSAGE, role: ROLE.USER, content: [{ type: RESPONSES_ITEM.INPUT_TEXT, text: "hi" }] }] };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
expect(body.instructions).toBe(`instr${SEP}${P1}`);
|
||||
expect(body.input.length).toBe(1);
|
||||
expect(body.input[0].content[0].text).toBe("hi");
|
||||
});
|
||||
|
||||
it("does not coerce string input", () => {
|
||||
const body = { input: "hello string" };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
expect(body.input).toBe("hello string");
|
||||
expect(body.instructions).toBeUndefined();
|
||||
});
|
||||
|
||||
it("exact idempotency for responses input", () => {
|
||||
const body = { input: [{ type: RESPONSES_ITEM.MESSAGE, role: ROLE.SYSTEM, content: [{ type: RESPONSES_ITEM.INPUT_TEXT, text: "base" }] }] };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
const sys = body.input[0];
|
||||
expect(sys.content.filter(c => c.text === P1).length).toBe(1);
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P2);
|
||||
expect(sys.content.filter(c => c.text === P2).length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("system-inject instructions", () => {
|
||||
it("appends to instructions string with idempotency", () => {
|
||||
const body = { instructions: "base" };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
expect(body.instructions).toBe(`base${SEP}${P1}`);
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
expect(body.instructions).toBe(`base${SEP}${P1}`);
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P2);
|
||||
expect(body.instructions).toBe(`base${SEP}${P1}${SEP}${P2}`);
|
||||
});
|
||||
|
||||
it("creates instructions when empty", () => {
|
||||
const body = { instructions: "" };
|
||||
// empty string still taken as string field, should become prompt
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
expect(body.instructions).toBe(P1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("system-inject dispatch by wire shape", () => {
|
||||
it("messages[] means Chat even when format is openai-responses label", () => {
|
||||
const body = { messages: [{ role: ROLE.SYSTEM, content: "hi" }] };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI_RESPONSES, P1);
|
||||
// should still treat as Chat because messages present
|
||||
expect(body.messages[0].content).toBe(`hi${SEP}${P1}`);
|
||||
});
|
||||
it("input[] means Responses even when format is openai", () => {
|
||||
const body = { input: [{ type: RESPONSES_ITEM.MESSAGE, role: ROLE.USER, content: [{ type: RESPONSES_ITEM.INPUT_TEXT, text: "hi" }] }] };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
expect(body.input[0].role).toBe(ROLE.SYSTEM);
|
||||
expect(body.input[0].content[0].type).toBe(RESPONSES_ITEM.INPUT_TEXT);
|
||||
});
|
||||
});
|
||||
|
||||
describe("system-inject claude", () => {
|
||||
it("string system appends with SEP and idempotent", () => {
|
||||
const body = { system: "base" };
|
||||
injectSystemPrompt(body, FORMATS.CLAUDE, P1);
|
||||
expect(body.system).toBe(`base${SEP}${P1}`);
|
||||
injectSystemPrompt(body, FORMATS.CLAUDE, P1);
|
||||
expect(body.system).toBe(`base${SEP}${P1}`);
|
||||
injectSystemPrompt(body, FORMATS.CLAUDE, P2);
|
||||
expect(body.system).toBe(`base${SEP}${P1}${SEP}${P2}`);
|
||||
});
|
||||
|
||||
it("array system uses CLAUDE_BLOCK.TEXT and inserts before last cache_control", () => {
|
||||
const body = { system: [{ type: CLAUDE_BLOCK.TEXT, text: "a" }, { type: CLAUDE_BLOCK.TEXT, text: "b", cache_control: { type: "ephemeral" } }, { type: CLAUDE_BLOCK.TEXT, text: "c", cache_control: { type: "ephemeral" } }] };
|
||||
injectSystemPrompt(body, FORMATS.CLAUDE, P1);
|
||||
// should be inserted before last cache_control (index 2)
|
||||
expect(body.system[2]).toEqual({ type: CLAUDE_BLOCK.TEXT, text: P1 });
|
||||
expect(body.system[3].text).toBe("c");
|
||||
expect(body.system[3].cache_control).toBeDefined();
|
||||
});
|
||||
|
||||
it("array without cache_control appends", () => {
|
||||
const body = { system: [{ type: CLAUDE_BLOCK.TEXT, text: "a" }] };
|
||||
injectSystemPrompt(body, FORMATS.CLAUDE, P1);
|
||||
expect(body.system[body.system.length - 1]).toEqual({ type: CLAUDE_BLOCK.TEXT, text: P1 });
|
||||
});
|
||||
|
||||
it("exact idempotency for claude array", () => {
|
||||
const body = { system: [{ type: CLAUDE_BLOCK.TEXT, text: "a" }] };
|
||||
injectSystemPrompt(body, FORMATS.CLAUDE, P1);
|
||||
injectSystemPrompt(body, FORMATS.CLAUDE, P1);
|
||||
expect(body.system.filter(b => b.text === P1).length).toBe(1);
|
||||
});
|
||||
|
||||
it("creates system when absent", () => {
|
||||
const body = {};
|
||||
injectSystemPrompt(body, FORMATS.CLAUDE, P1);
|
||||
expect(body.system).toBe(P1);
|
||||
});
|
||||
|
||||
it("real body with messages[] injects into system, never a system role turn", () => {
|
||||
const body = { system: "base", messages: [{ role: ROLE.USER, content: "hi" }] };
|
||||
injectSystemPrompt(body, FORMATS.CLAUDE, P1);
|
||||
expect(body.system).toBe(`base${SEP}${P1}`);
|
||||
expect(body.messages).toEqual([{ role: ROLE.USER, content: "hi" }]);
|
||||
});
|
||||
|
||||
it("absent system with messages[] creates system field, not a system message", () => {
|
||||
const body = { messages: [{ role: ROLE.USER, content: "hi" }] };
|
||||
injectSystemPrompt(body, FORMATS.CLAUDE, P1);
|
||||
expect(body.system).toBe(P1);
|
||||
expect(body.messages.some(m => m.role === ROLE.SYSTEM)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("system-inject gemini", () => {
|
||||
it("preserves snake_case key", () => {
|
||||
const body = { system_instruction: { parts: [{ text: "base" }] } };
|
||||
injectSystemPrompt(body, FORMATS.GEMINI, P1);
|
||||
expect(body.system_instruction.parts.length).toBe(2);
|
||||
expect(body.system_instruction.parts[1].text).toBe(P1);
|
||||
expect(body.systemInstruction).toBeUndefined();
|
||||
});
|
||||
it("preserves camelCase key", () => {
|
||||
const body = { systemInstruction: { parts: [{ text: "base" }] } };
|
||||
injectSystemPrompt(body, FORMATS.GEMINI, P1);
|
||||
expect(body.systemInstruction.parts[1].text).toBe(P1);
|
||||
expect(body.system_instruction).toBeUndefined();
|
||||
});
|
||||
it("handles Antigravity wrapper request.systemInstruction", () => {
|
||||
const body = { request: { systemInstruction: { parts: [{ text: "base" }] } } };
|
||||
injectSystemPrompt(body, FORMATS.ANTIGRAVITY, P1);
|
||||
expect(body.request.systemInstruction.parts[1].text).toBe(P1);
|
||||
});
|
||||
it("exact idempotency for gemini", () => {
|
||||
const body = { systemInstruction: { parts: [{ text: "base" }] } };
|
||||
injectSystemPrompt(body, FORMATS.GEMINI, P1);
|
||||
injectSystemPrompt(body, FORMATS.GEMINI, P1);
|
||||
expect(body.systemInstruction.parts.filter(p => p.text === P1).length).toBe(1);
|
||||
injectSystemPrompt(body, FORMATS.GEMINI, P2);
|
||||
expect(body.systemInstruction.parts.filter(p => p.text === P2).length).toBe(1);
|
||||
});
|
||||
it("creates when absent", () => {
|
||||
const body = {};
|
||||
injectSystemPrompt(body, FORMATS.GEMINI, P1);
|
||||
expect(body.systemInstruction.parts[0].text).toBe(P1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("system-inject kiro", () => {
|
||||
it("updates systemPrompt and mirrored prefix of first history user preserving tail", () => {
|
||||
const oldPrompt = "OLD_SYS";
|
||||
const timeCtx = "[Context: Current time is 2026-01-01T00:00:00.000Z]";
|
||||
const tail = "user tail content";
|
||||
const historyUserContent = `${oldPrompt}${SEP}${timeCtx}${SEP}${tail}`;
|
||||
const body = {
|
||||
systemPrompt: oldPrompt,
|
||||
conversationState: {
|
||||
history: [{ userInputMessage: { content: historyUserContent, modelId: "m" } }, { assistantResponseMessage: { content: "..." } }],
|
||||
currentMessage: { userInputMessage: { content: "current " + tail, modelId: "m" } },
|
||||
},
|
||||
};
|
||||
injectSystemPrompt(body, FORMATS.KIRO, P1);
|
||||
const next = `${oldPrompt}${SEP}${P1}`;
|
||||
expect(body.systemPrompt).toBe(next);
|
||||
expect(body.conversationState.history[0].userInputMessage.content).toBe(`${next}${SEP}${timeCtx}${SEP}${tail}`);
|
||||
// currentMessage must stay untouched
|
||||
expect(body.conversationState.currentMessage.userInputMessage.content).toBe("current " + tail);
|
||||
});
|
||||
|
||||
it("when no history user, updates currentMessage instead", () => {
|
||||
const oldPrompt = "OLD";
|
||||
const body = {
|
||||
systemPrompt: oldPrompt,
|
||||
conversationState: {
|
||||
history: [],
|
||||
currentMessage: { userInputMessage: { content: `${oldPrompt}${SEP}tail`, modelId: "m" } },
|
||||
},
|
||||
};
|
||||
injectSystemPrompt(body, FORMATS.KIRO, P1);
|
||||
expect(body.systemPrompt).toBe(`${oldPrompt}${SEP}${P1}`);
|
||||
expect(body.conversationState.currentMessage.userInputMessage.content).toBe(`${oldPrompt}${SEP}${P1}${SEP}tail`);
|
||||
});
|
||||
|
||||
it("empty old prompt prepends to chosen user content", () => {
|
||||
const body = {
|
||||
systemPrompt: "",
|
||||
conversationState: {
|
||||
history: [{ userInputMessage: { content: "tail hello", modelId: "m" } }],
|
||||
currentMessage: { userInputMessage: { content: "cur", modelId: "m" } },
|
||||
},
|
||||
};
|
||||
injectSystemPrompt(body, FORMATS.KIRO, P1);
|
||||
expect(body.systemPrompt).toBe(P1);
|
||||
expect(body.conversationState.history[0].userInputMessage.content).toBe(`${P1}${SEP}tail hello`);
|
||||
});
|
||||
|
||||
it("if old prompt not mirrored at head, do not alter user content", () => {
|
||||
const body = {
|
||||
systemPrompt: "OLD",
|
||||
conversationState: {
|
||||
history: [{ userInputMessage: { content: "different head content", modelId: "m" } }],
|
||||
currentMessage: { userInputMessage: { content: "cur", modelId: "m" } },
|
||||
},
|
||||
};
|
||||
injectSystemPrompt(body, FORMATS.KIRO, P1);
|
||||
expect(body.systemPrompt).toBe(`OLD${SEP}${P1}`);
|
||||
expect(body.conversationState.history[0].userInputMessage.content).toBe("different head content");
|
||||
});
|
||||
|
||||
it("exact retry idempotency for kiro", () => {
|
||||
const oldPrompt = "OLD";
|
||||
const body = {
|
||||
systemPrompt: oldPrompt,
|
||||
conversationState: {
|
||||
history: [{ userInputMessage: { content: `${oldPrompt}${SEP}tail`, modelId: "m" } }],
|
||||
currentMessage: { userInputMessage: { content: "cur", modelId: "m" } },
|
||||
},
|
||||
};
|
||||
injectSystemPrompt(body, FORMATS.KIRO, P1);
|
||||
const after1 = JSON.parse(JSON.stringify(body));
|
||||
injectSystemPrompt(body, FORMATS.KIRO, P1);
|
||||
expect(body.systemPrompt).toBe(after1.systemPrompt);
|
||||
expect(body.conversationState.history[0].userInputMessage.content).toBe(after1.conversationState.history[0].userInputMessage.content);
|
||||
// different prompt both apply
|
||||
injectSystemPrompt(body, FORMATS.KIRO, P2);
|
||||
expect(body.systemPrompt).toBe(`${oldPrompt}${SEP}${P1}${SEP}${P2}`);
|
||||
});
|
||||
|
||||
it("preserves non-enumerable _kiroUpstreamModel", () => {
|
||||
const body = {
|
||||
systemPrompt: "OLD",
|
||||
conversationState: { history: [{ userInputMessage: { content: "OLD" + SEP + "tail", modelId: "m" } }], currentMessage: { userInputMessage: { content: "OLD" + SEP + "tail2", modelId: "m" } } },
|
||||
};
|
||||
Object.defineProperty(body, "_kiroUpstreamModel", { value: "m", enumerable: false });
|
||||
injectSystemPrompt(body, FORMATS.KIRO, P1);
|
||||
expect(body._kiroUpstreamModel).toBe("m");
|
||||
expect(Object.getOwnPropertyDescriptor(body, "_kiroUpstreamModel").enumerable).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("system-inject regression fixes", () => {
|
||||
it("kiro partial mutation converges on retry after transient content write failure", () => {
|
||||
const oldPrompt = "OLD";
|
||||
let failNextWrite = true;
|
||||
const um = { content: `${oldPrompt}${SEP}tail`, modelId: "m" };
|
||||
const proxiedUm = new Proxy(um, {
|
||||
set(t, p, v) {
|
||||
if (p === "content" && failNextWrite) { failNextWrite = false; throw new Error("transient"); }
|
||||
t[p] = v; return true;
|
||||
},
|
||||
});
|
||||
const body = {
|
||||
systemPrompt: oldPrompt,
|
||||
conversationState: {
|
||||
history: [{ userInputMessage: proxiedUm }],
|
||||
},
|
||||
};
|
||||
injectSystemPrompt(body, FORMATS.KIRO, P1);
|
||||
// first pass rolled back atomically — nothing half-applied
|
||||
expect(body.systemPrompt).toBe(oldPrompt);
|
||||
expect(um.content).toBe(`${oldPrompt}${SEP}tail`);
|
||||
// retry converges
|
||||
injectSystemPrompt(body, FORMATS.KIRO, P1);
|
||||
expect(body.systemPrompt).toBe(`${oldPrompt}${SEP}${P1}`);
|
||||
expect(um.content).toBe(`${oldPrompt}${SEP}${P1}${SEP}tail`);
|
||||
});
|
||||
|
||||
it("kiro rolls back systemPrompt when user content write fails (atomicity)", () => {
|
||||
const oldPrompt = "OLD";
|
||||
const body = {
|
||||
systemPrompt: oldPrompt,
|
||||
conversationState: {
|
||||
history: [{ userInputMessage: Object.freeze({ content: `${oldPrompt}${SEP}tail`, modelId: "m" }) }],
|
||||
},
|
||||
};
|
||||
injectSystemPrompt(body, FORMATS.KIRO, P1);
|
||||
expect(body.systemPrompt).toBe(oldPrompt);
|
||||
});
|
||||
|
||||
it("kiro shape gate: stray conversationState without history/currentMessage does not hijack chat body", () => {
|
||||
const body = { messages: [{ role: ROLE.SYSTEM, content: "hello" }], systemPrompt: "", conversationState: {} };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, P1);
|
||||
expect(body.messages[0].content).toBe(`hello${SEP}${P1}`);
|
||||
});
|
||||
|
||||
it("substring occurrence does not suppress injection (exact SEP-delimited idempotency)", () => {
|
||||
const body = { messages: [{ role: ROLE.SYSTEM, content: "You are RULE follower" }] };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, "RULE");
|
||||
expect(body.messages[0].content).toBe(`You are RULE follower${SEP}RULE`);
|
||||
});
|
||||
|
||||
it("instructions substring occurrence does not suppress injection", () => {
|
||||
const body = { instructions: "You are RULE follower" };
|
||||
injectSystemPrompt(body, FORMATS.OPENAI, "RULE");
|
||||
expect(body.instructions).toBe(`You are RULE follower${SEP}RULE`);
|
||||
});
|
||||
|
||||
it("kiro empty-old prepend fires when prompt appears mid-tail only", () => {
|
||||
const body = {
|
||||
systemPrompt: "",
|
||||
conversationState: {
|
||||
history: [{ userInputMessage: { content: `some ${P1} here`, modelId: "m" } }],
|
||||
},
|
||||
};
|
||||
injectSystemPrompt(body, FORMATS.KIRO, P1);
|
||||
expect(body.conversationState.history[0].userInputMessage.content).toBe(`${P1}${SEP}some ${P1} here`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("system-inject fail-open", () => {
|
||||
it("null/undefined bodies never throw", () => {
|
||||
expect(() => injectSystemPrompt(null, FORMATS.OPENAI, P1)).not.toThrow();
|
||||
expect(() => injectSystemPrompt(undefined, FORMATS.OPENAI, P1)).not.toThrow();
|
||||
expect(() => injectSystemPrompt({}, FORMATS.OPENAI, null)).not.toThrow();
|
||||
});
|
||||
|
||||
it("malformed messages array never throws", () => {
|
||||
expect(() => injectSystemPrompt({ messages: null }, FORMATS.OPENAI, P1)).not.toThrow();
|
||||
expect(() => injectSystemPrompt({ messages: "bad" }, FORMATS.OPENAI, P1)).not.toThrow();
|
||||
expect(() => injectSystemPrompt({ messages: [{ role: null, content: null }] }, FORMATS.OPENAI, P1)).not.toThrow();
|
||||
});
|
||||
|
||||
it("frozen body never throws and does not partially mutate", () => {
|
||||
const body = { messages: [{ role: ROLE.SYSTEM, content: "hello" }] };
|
||||
Object.freeze(body);
|
||||
Object.freeze(body.messages);
|
||||
Object.freeze(body.messages[0]);
|
||||
expect(() => injectSystemPrompt(body, FORMATS.OPENAI, P1)).not.toThrow();
|
||||
expect(body.messages[0].content).toBe("hello");
|
||||
});
|
||||
|
||||
it("Proxy throwing setter never throws", () => {
|
||||
const throwingMsg = new Proxy({ role: ROLE.SYSTEM, content: "hello" }, {
|
||||
set() { throw new Error("msg setter fail"); },
|
||||
});
|
||||
const arrProxy = new Proxy([throwingMsg], {
|
||||
get(t, p, r) { return Reflect.get(t, p, r); },
|
||||
set() { throw new Error("arr setter fail"); },
|
||||
});
|
||||
const proxy = new Proxy({}, {
|
||||
set(t, p, v) { if (p === "messages") throw new Error("setter fail"); return Reflect.set(t, p, v); },
|
||||
get(t, p) { if (p === "messages") return arrProxy; return t[p]; },
|
||||
});
|
||||
expect(() => injectSystemPrompt(proxy, FORMATS.OPENAI, P1)).not.toThrow();
|
||||
expect(() => injectSystemPrompt(proxy, FORMATS.OPENAI_RESPONSES, P1)).not.toThrow();
|
||||
});
|
||||
|
||||
it("frozen claude never throws", () => {
|
||||
const body = { system: [{ type: CLAUDE_BLOCK.TEXT, text: "a" }] };
|
||||
Object.freeze(body.system);
|
||||
expect(() => injectSystemPrompt(body, FORMATS.CLAUDE, P1)).not.toThrow();
|
||||
});
|
||||
|
||||
it("frozen gemini never throws", () => {
|
||||
const body = { systemInstruction: { parts: [{ text: "a" }] } };
|
||||
Object.freeze(body.systemInstruction.parts);
|
||||
expect(() => injectSystemPrompt(body, FORMATS.GEMINI, P1)).not.toThrow();
|
||||
});
|
||||
|
||||
it("injectCaveman and injectPonytail fail open on frozen", () => {
|
||||
const body = { messages: [{ role: ROLE.SYSTEM, content: "hi" }] };
|
||||
Object.freeze(body);
|
||||
Object.freeze(body.messages);
|
||||
expect(() => injectCaveman(body, FORMATS.OPENAI, "full")).not.toThrow();
|
||||
expect(() => injectPonytail(body, FORMATS.OPENAI, "full")).not.toThrow();
|
||||
});
|
||||
|
||||
it("different caveman and ponytail prompts both apply", () => {
|
||||
const body = { messages: [{ role: ROLE.SYSTEM, content: "base" }] };
|
||||
injectCaveman(body, FORMATS.OPENAI, "full");
|
||||
const afterCaveman = body.messages[0].content;
|
||||
expect(afterCaveman).toContain(CAVEMAN_PROMPTS.full.slice(0, 30));
|
||||
injectPonytail(body, FORMATS.OPENAI, "full");
|
||||
expect(body.messages[0].content).toContain(PONYTAIL_PROMPTS.full.slice(0, 30));
|
||||
expect(body.messages[0].content).toContain(afterCaveman);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user