import { describe, it, expect } from "vitest"; import { mkdtempSync, writeFileSync, mkdirSync } from "fs"; import { join } from "path"; import { tmpdir } from "os"; import { buildRootPairBlock, hasRootPairMarker, buildPreInitHint, computeInjectionBudget, modeAllowsPreInitHint, modeAllowsInjection, modeRequiresProtocolPath, discoverContextWindow, estimateTokens, findAllArtifactPairs, buildInjectionPayload, outgoingMessagesHaveMarker, providerPayloadHasMarker, shouldReinjectForEvent, isRelevantTurnForReinjection, detectEditIntent, detectArchitectureSensitiveReasoning, ROOT_PAIR_START_MARKER, ROOT_PAIR_END_MARKER, TRUST_BOUNDARY_TEXT, } from "../src/prompt-injection.js"; describe("prompt-injection helpers", () => { describe("canonical markers", () => { it("builds a root-pair block with markers", () => { const block = buildRootPairBlock("index content", "map content"); expect(block).toContain(ROOT_PAIR_START_MARKER); expect(block).toContain(ROOT_PAIR_END_MARKER); expect(block).toContain(TRUST_BOUNDARY_TEXT); expect(block).toContain("index content"); expect(block).toContain("map content"); }); it("detects root-pair marker in content", () => { const block = buildRootPairBlock("i", "m"); expect(hasRootPairMarker(block)).toBe(true); expect(hasRootPairMarker("plain text")).toBe(false); }); }); describe("pre-init hint", () => { it("returns a visible hint with no synthetic artifacts", () => { const hint = buildPreInitHint(); expect(hint).toContain("project_map_init"); expect(hint).toContain("📋"); expect(hint).not.toContain(ROOT_PAIR_START_MARKER); expect(hint).not.toContain("### Root index"); expect(hint).not.toContain("### Root map"); }); }); describe("budget calculation", () => { it("uses relative percent when smaller than absolute cap", () => { const budget = computeInjectionBudget( { contextBudgetPercent: 15, contextBudgetMaxTokens: 100_000 }, 200_000, ); expect(budget).toBe(30_000); }); it("uses absolute cap when smaller than relative percent", () => { const budget = computeInjectionBudget( { contextBudgetPercent: 15, contextBudgetMaxTokens: 100_000 }, 10_000_000, ); expect(budget).toBe(100_000); }); it("falls back to absolute cap when context window is unknown", () => { const budget = computeInjectionBudget( { contextBudgetPercent: 15, contextBudgetMaxTokens: 100_000 }, undefined, ); expect(budget).toBe(100_000); }); it("falls back to absolute cap when context window is zero", () => { const budget = computeInjectionBudget( { contextBudgetPercent: 15, contextBudgetMaxTokens: 100_000 }, 0, ); expect(budget).toBe(100_000); }); }); describe("mode helpers", () => { it("allows pre-init hint for all modes except off", () => { expect(modeAllowsPreInitHint("off")).toBe(false); expect(modeAllowsPreInitHint("advisory")).toBe(true); expect(modeAllowsPreInitHint("strong")).toBe(true); expect(modeAllowsPreInitHint("strict")).toBe(true); }); it("allows injection only for strong and strict", () => { expect(modeAllowsInjection("off")).toBe(false); expect(modeAllowsInjection("advisory")).toBe(false); expect(modeAllowsInjection("strong")).toBe(true); expect(modeAllowsInjection("strict")).toBe(true); }); it("requires protocol path only for strict", () => { expect(modeRequiresProtocolPath("off")).toBe(false); expect(modeRequiresProtocolPath("advisory")).toBe(false); expect(modeRequiresProtocolPath("strong")).toBe(false); expect(modeRequiresProtocolPath("strict")).toBe(true); }); }); describe("context-window discovery", () => { it("reads contextWindow from model metadata", () => { const ctx = { model: { contextWindow: 256_000 } }; expect(discoverContextWindow(ctx)).toBe(256_000); }); it("reads maxContextTokens from model metadata", () => { const ctx = { model: { maxContextTokens: 128_000 } }; expect(discoverContextWindow(ctx)).toBe(128_000); }); it("falls back to known model lookup by id", () => { expect(discoverContextWindow({ model: { id: "gpt-4o-mini" } })).toBe( 128_000, ); expect(discoverContextWindow({ model: { id: "kimi-for-coding" } })).toBe( 200_000, ); }); it("returns undefined for unknown models", () => { expect( discoverContextWindow({ model: { id: "unknown-model" } }), ).toBeUndefined(); }); it("returns undefined when model is missing", () => { expect(discoverContextWindow({})).toBeUndefined(); }); }); describe("token estimation", () => { it("estimates tokens from character count", () => { expect(estimateTokens("")).toBe(0); expect(estimateTokens("abcd")).toBe(1); expect(estimateTokens("abcde")).toBe(2); }); }); describe("artifact-pair discovery", () => { it("finds paired artifacts shallow-first", () => { const dir = mkdtempSync(join(tmpdir(), "pi-pair-test-")); mkdirSync(join(dir, "src", "nested"), { recursive: true }); writeFileSync(join(dir, ".pi-map.md"), "# root"); writeFileSync(join(dir, ".pi-map.index.md"), "# root index"); writeFileSync(join(dir, "src", ".pi-map.md"), "# src"); writeFileSync(join(dir, "src", ".pi-map.index.md"), "# src index"); writeFileSync(join(dir, "src", "nested", ".pi-map.md"), "# nested"); writeFileSync( join(dir, "src", "nested", ".pi-map.index.md"), "# nested index", ); const pairs = findAllArtifactPairs(dir); expect(pairs.map((p) => p.dir)).toEqual([".", "src", "src/nested"]); }); it("ignores directories missing one of the paired artifacts", () => { const dir = mkdtempSync(join(tmpdir(), "pi-pair-test-")); writeFileSync(join(dir, ".pi-map.md"), "# root"); // missing .pi-map.index.md intentionally expect(findAllArtifactPairs(dir)).toHaveLength(0); }); }); describe("outgoing message marker scan", () => { it("detects marker in string content", () => { const messages = [{ content: `hello ${ROOT_PAIR_START_MARKER} world` }]; expect(outgoingMessagesHaveMarker(messages)).toBe(true); }); it("detects marker in text field", () => { const messages = [{ text: `before ${ROOT_PAIR_START_MARKER} after` }]; expect(outgoingMessagesHaveMarker(messages)).toBe(true); }); it("detects marker in array content blocks", () => { const messages = [ { content: [ { type: "text", text: "prefix" }, { type: "text", text: ROOT_PAIR_START_MARKER }, ], }, ]; expect(outgoingMessagesHaveMarker(messages)).toBe(true); }); it("returns false when no marker is present", () => { const messages = [{ content: "plain text" }, { text: "more text" }]; expect(outgoingMessagesHaveMarker(messages)).toBe(false); }); it("returns false for empty messages", () => { expect(outgoingMessagesHaveMarker([])).toBe(false); }); }); describe("provider payload marker fallback", () => { it("detects marker in string payload", () => { expect( providerPayloadHasMarker(`foo ${ROOT_PAIR_START_MARKER} bar`), ).toBe(true); }); it("detects marker in JSON-stringified object", () => { expect( providerPayloadHasMarker({ body: { text: ROOT_PAIR_START_MARKER } }), ).toBe(true); }); it("returns false for plain string without marker", () => { expect(providerPayloadHasMarker("plain payload")).toBe(false); }); it("returns false for null payload", () => { expect(providerPayloadHasMarker(null)).toBe(false); }); it("returns false for undefined payload", () => { expect(providerPayloadHasMarker(undefined)).toBe(false); }); }); describe("reinjection decision", () => { it("rejects reinjection for off mode", () => { const decision = shouldReinjectForEvent( { messages: [{ content: "hi" }], type: "agent_start" }, "off", ); expect(decision.needed).toBe(false); expect(decision.reason).toBe("mode_does_not_require_reinjection"); }); it("rejects reinjection for advisory mode", () => { const decision = shouldReinjectForEvent( { messages: [{ content: "hi" }], type: "agent_start" }, "advisory", ); expect(decision.needed).toBe(false); expect(decision.reason).toBe("mode_does_not_require_reinjection"); }); it("rejects reinjection when marker is present in messages", () => { const decision = shouldReinjectForEvent( { messages: [{ content: ROOT_PAIR_START_MARKER }], type: "agent_start", }, "strong", ); expect(decision.needed).toBe(false); expect(decision.reason).toBe("marker_present_in_messages"); }); it("rejects reinjection when marker is present in payload", () => { const decision = shouldReinjectForEvent( { messages: [{ content: "hi" }], payload: ROOT_PAIR_START_MARKER, type: "agent_start", }, "strong", ); expect(decision.needed).toBe(false); expect(decision.reason).toBe("marker_present_in_payload"); }); it("allows reinjection for agent_start in strong mode when marker absent", () => { const decision = shouldReinjectForEvent( { messages: [{ content: "hi" }], type: "agent_start" }, "strong", ); expect(decision.needed).toBe(true); expect(decision.reason).toBe("relevant_turn_and_marker_absent"); }); it("allows reinjection for edit_intent in strict mode when marker absent", () => { const decision = shouldReinjectForEvent( { messages: [{ content: "edit this file" }], type: "edit_intent" }, "strict", ); expect(decision.needed).toBe(true); expect(decision.reason).toBe("relevant_turn_and_marker_absent"); }); it("allows reinjection for architecture_sensitive in strong mode", () => { const decision = shouldReinjectForEvent( { messages: [{ content: "discuss architecture" }], type: "architecture_sensitive", }, "strong", ); expect(decision.needed).toBe(true); expect(decision.reason).toBe("relevant_turn_and_marker_absent"); }); it("rejects reinjection for irrelevant event types", () => { const decision = shouldReinjectForEvent( { messages: [{ content: "hi" }], type: "user_chat" }, "strong", ); expect(decision.needed).toBe(false); expect(decision.reason).toBe("not_a_relevant_turn"); }); it("treats compaction as a relevant turn", () => { expect(isRelevantTurnForReinjection("compaction")).toBe(true); }); it("treats artifact_change as a relevant turn", () => { expect(isRelevantTurnForReinjection("artifact_change")).toBe(true); }); it("allows reinjection for compaction in strong mode when marker absent", () => { const decision = shouldReinjectForEvent( { messages: [{ content: "hi" }], type: "compaction" }, "strong", ); expect(decision.needed).toBe(true); expect(decision.reason).toBe("relevant_turn_and_marker_absent"); }); it("rejects reinjection for compaction when marker is present", () => { const decision = shouldReinjectForEvent( { messages: [{ content: ROOT_PAIR_START_MARKER }], type: "compaction", }, "strong", ); expect(decision.needed).toBe(false); expect(decision.reason).toBe("marker_present_in_messages"); }); it("forces reinjection for artifact_change even when marker is present", () => { const decision = shouldReinjectForEvent( { messages: [{ content: ROOT_PAIR_START_MARKER }], type: "artifact_change", }, "strong", ); expect(decision.needed).toBe(true); expect(decision.reason).toBe("root_pair_artifact_changed"); }); it("rejects reinjection for all relevant turn types in off mode", () => { const types = [ "agent_start", "edit_intent", "architecture_sensitive", "compaction", "artifact_change", ]; for (const type of types) { const decision = shouldReinjectForEvent( { messages: [{ content: "hi" }], type }, "off", ); expect(decision.needed).toBe(false); expect(decision.reason).toBe("mode_does_not_require_reinjection"); } }); it("rejects reinjection for all relevant turn types in advisory mode", () => { const types = [ "agent_start", "edit_intent", "architecture_sensitive", "compaction", "artifact_change", ]; for (const type of types) { const decision = shouldReinjectForEvent( { messages: [{ content: "hi" }], type }, "advisory", ); expect(decision.needed).toBe(false); expect(decision.reason).toBe("mode_does_not_require_reinjection"); } }); it("allows reinjection for strict mode on relevant turns", () => { const decision = shouldReinjectForEvent( { messages: [{ content: "hi" }], type: "edit_intent" }, "strict", ); expect(decision.needed).toBe(true); expect(decision.reason).toBe("relevant_turn_and_marker_absent"); }); it("rejects reinjection in strict mode when marker is present", () => { const decision = shouldReinjectForEvent( { messages: [{ content: ROOT_PAIR_START_MARKER }], type: "architecture_sensitive", }, "strict", ); expect(decision.needed).toBe(false); expect(decision.reason).toBe("marker_present_in_messages"); }); it("forces reinjection for artifact_change in strict mode even with marker", () => { const decision = shouldReinjectForEvent( { messages: [{ content: ROOT_PAIR_START_MARKER }], type: "artifact_change", }, "strict", ); expect(decision.needed).toBe(true); expect(decision.reason).toBe("root_pair_artifact_changed"); }); it("rejects reinjection for unknown event types even in strong mode", () => { const decision = shouldReinjectForEvent( { messages: [{ content: "hi" }], type: "custom_event" }, "strong", ); expect(decision.needed).toBe(false); expect(decision.reason).toBe("not_a_relevant_turn"); }); it("prefers message marker over payload marker when both present", () => { const decision = shouldReinjectForEvent( { messages: [{ content: ROOT_PAIR_START_MARKER }], payload: "no marker here", type: "edit_intent", }, "strong", ); expect(decision.needed).toBe(false); expect(decision.reason).toBe("marker_present_in_messages"); }); it("falls back to payload when messages are empty", () => { const decision = shouldReinjectForEvent( { messages: [], payload: ROOT_PAIR_START_MARKER, type: "edit_intent", }, "strong", ); expect(decision.needed).toBe(false); expect(decision.reason).toBe("marker_present_in_payload"); }); }); describe("edit intent detection", () => { it("detects edit-the-file patterns", () => { expect(detectEditIntent([{ content: "edit the file" }])).toBe(true); expect(detectEditIntent([{ content: "modify this code" }])).toBe(true); expect(detectEditIntent([{ content: "update the function" }])).toBe(true); }); it("detects change-the-class patterns", () => { expect(detectEditIntent([{ content: "change the class" }])).toBe(true); expect(detectEditIntent([{ content: "refactor the module" }])).toBe(true); expect(detectEditIntent([{ content: "fix the bug" }])).toBe(true); }); it("detects write-new patterns", () => { expect(detectEditIntent([{ content: "write a file" }])).toBe(true); expect(detectEditIntent([{ content: "create a component" }])).toBe(true); expect(detectEditIntent([{ content: "generate a function" }])).toBe(true); expect(detectEditIntent([{ content: "write new file" }])).toBe(true); }); it("detects code-block edit intent", () => { expect( detectEditIntent([ { content: "```ts\nedit the function\n```", }, ]), ).toBe(true); }); it("detects apply-change patterns", () => { expect( detectEditIntent([{ content: "apply the change to fix this" }]), ).toBe(true); expect(detectEditIntent([{ content: "apply this patch" }])).toBe(true); }); it("returns false for non-edit content", () => { expect(detectEditIntent([{ content: "what is the weather?" }])).toBe( false, ); expect(detectEditIntent([{ content: "explain how this works" }])).toBe( false, ); expect(detectEditIntent([{ content: "hello world" }])).toBe(false); }); it("handles array content blocks", () => { expect( detectEditIntent([ { content: [ { type: "text", text: "please " }, { type: "text", text: "edit the file" }, ], }, ]), ).toBe(true); }); it("returns false for empty messages", () => { expect(detectEditIntent([])).toBe(false); }); it("detects delete/remove patterns", () => { expect(detectEditIntent([{ content: "delete the old method" }])).toBe( true, ); expect(detectEditIntent([{ content: "remove the unused code" }])).toBe( true, ); }); it("detects rewrite/patch patterns", () => { expect(detectEditIntent([{ content: "rewrite the component" }])).toBe( true, ); expect(detectEditIntent([{ content: "patch the file" }])).toBe(true); }); }); describe("architecture-sensitive reasoning detection", () => { it("detects architecture decision patterns", () => { expect( detectArchitectureSensitiveReasoning([ { content: "this is an architecture decision" }, ]), ).toBe(true); expect( detectArchitectureSensitiveReasoning([ { content: "the architectural pattern we chose" }, ]), ).toBe(true); }); it("detects design pattern patterns", () => { expect( detectArchitectureSensitiveReasoning([ { content: "design pattern review" }, ]), ).toBe(true); expect( detectArchitectureSensitiveReasoning([ { content: "design choice analysis" }, ]), ).toBe(true); }); it("detects restructure patterns", () => { expect( detectArchitectureSensitiveReasoning([ { content: "restructure the codebase" }, ]), ).toBe(true); expect( detectArchitectureSensitiveReasoning([ { content: "reorganize the modules" }, ]), ).toBe(true); expect( detectArchitectureSensitiveReasoning([ { content: "redesign the API layer" }, ]), ).toBe(true); expect( detectArchitectureSensitiveReasoning([ { content: "rearchitect the system" }, ]), ).toBe(true); }); it("detects system design patterns", () => { expect( detectArchitectureSensitiveReasoning([ { content: "system design overview" }, ]), ).toBe(true); expect( detectArchitectureSensitiveReasoning([ { content: "high-level architecture" }, ]), ).toBe(true); }); it("detects dependency patterns", () => { expect( detectArchitectureSensitiveReasoning([ { content: "dependency injection strategy" }, ]), ).toBe(true); expect( detectArchitectureSensitiveReasoning([ { content: "dependency graph analysis" }, ]), ).toBe(true); }); it("detects API/interface design patterns", () => { expect( detectArchitectureSensitiveReasoning([ { content: "API design migration" }, ]), ).toBe(true); expect( detectArchitectureSensitiveReasoning([ { content: "interface contract change" }, ]), ).toBe(true); }); it("detects architecture style patterns", () => { expect( detectArchitectureSensitiveReasoning([ { content: "microservice architecture" }, ]), ).toBe(true); expect( detectArchitectureSensitiveReasoning([ { content: "clean architecture principles" }, ]), ).toBe(true); }); it("detects data and quality attribute patterns", () => { expect( detectArchitectureSensitiveReasoning([ { content: "data model design" }, ]), ).toBe(true); expect( detectArchitectureSensitiveReasoning([ { content: "scalability tradeoff decision" }, ]), ).toBe(true); expect( detectArchitectureSensitiveReasoning([ { content: "security concern analysis" }, ]), ).toBe(true); expect( detectArchitectureSensitiveReasoning([ { content: "performance concern review" }, ]), ).toBe(true); }); it("returns false for non-architecture content", () => { expect( detectArchitectureSensitiveReasoning([ { content: "what is the weather?" }, ]), ).toBe(false); expect( detectArchitectureSensitiveReasoning([ { content: "fix the typo in readme" }, ]), ).toBe(false); expect( detectArchitectureSensitiveReasoning([ { content: "add a console log" }, ]), ).toBe(false); }); it("handles array content blocks", () => { expect( detectArchitectureSensitiveReasoning([ { content: [ { type: "text", text: "let's discuss " }, { type: "text", text: "the architecture" }, ], }, ]), ).toBe(true); }); it("returns false for empty messages", () => { expect(detectArchitectureSensitiveReasoning([])).toBe(false); }); }); describe("injection payload", () => { it("always includes the root pair", () => { const dir = mkdtempSync(join(tmpdir(), "pi-payload-test-")); writeFileSync(join(dir, ".pi-map.md"), "# root map"); writeFileSync(join(dir, ".pi-map.index.md"), "# root index"); const payload = buildInjectionPayload( dir, { contextBudgetPercent: 15, contextBudgetMaxTokens: 100_000 }, undefined, ); expect(payload.content).toContain(ROOT_PAIR_START_MARKER); expect(payload.content).toContain(ROOT_PAIR_END_MARKER); expect(payload.content).toContain("# root map"); expect(payload.content).toContain("# root index"); expect(payload.display).toBe(false); }); it("expands additional pairs when budget allows", () => { const dir = mkdtempSync(join(tmpdir(), "pi-payload-test-")); mkdirSync(join(dir, "src"), { recursive: true }); writeFileSync(join(dir, ".pi-map.md"), "# root map"); writeFileSync(join(dir, ".pi-map.index.md"), "# root index"); writeFileSync(join(dir, "src", ".pi-map.md"), "# src map"); writeFileSync(join(dir, "src", ".pi-map.index.md"), "# src index"); const payload = buildInjectionPayload( dir, { contextBudgetPercent: 100, contextBudgetMaxTokens: 10_000 }, 10_000, ); expect(payload.content).toContain("# src map"); expect(payload.content).toContain("# src index"); }); it("stops expanding when budget is exhausted", () => { const dir = mkdtempSync(join(tmpdir(), "pi-payload-test-")); mkdirSync(join(dir, "src"), { recursive: true }); writeFileSync(join(dir, ".pi-map.md"), "# root map"); writeFileSync(join(dir, ".pi-map.index.md"), "# root index"); writeFileSync( join(dir, "src", ".pi-map.md"), `# src map ${"x".repeat(400)}`, ); writeFileSync( join(dir, "src", ".pi-map.index.md"), `# src index ${"x".repeat(400)}`, ); const payload = buildInjectionPayload( dir, { contextBudgetPercent: 100, contextBudgetMaxTokens: 10 }, 10_000, ); expect(payload.content).toContain("# root map"); expect(payload.content).not.toContain("# src map x"); }); }); });