diff --git a/open-sse/rtk/systemInject.js b/open-sse/rtk/systemInject.js index 0d5af728..b60e15d3 100644 --- a/open-sse/rtk/systemInject.js +++ b/open-sse/rtk/systemInject.js @@ -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 (_) {} } diff --git a/tests/unit/system-inject.test.js b/tests/unit/system-inject.test.js new file mode 100644 index 00000000..bff7c94c --- /dev/null +++ b/tests/unit/system-inject.test.js @@ -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); + }); +});