1196 lines
34 KiB
TypeScript
1196 lines
34 KiB
TypeScript
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,
|
|
hasProtocolPath,
|
|
buildAdvisoryReminder,
|
|
discoverContextWindow,
|
|
estimateTokens,
|
|
findAllArtifactPairs,
|
|
buildInjectionPayload,
|
|
outgoingMessagesHaveMarker,
|
|
providerPayloadHasMarker,
|
|
shouldReinjectForEvent,
|
|
isRelevantTurnForReinjection,
|
|
detectEditIntent,
|
|
detectArchitectureSensitiveReasoning,
|
|
isSensitiveAction,
|
|
messagesHaveBypass,
|
|
extractBypassReason,
|
|
evaluateStrictBypass,
|
|
buildStrictBypassGuard,
|
|
ROOT_PAIR_START_MARKER,
|
|
ROOT_PAIR_END_MARKER,
|
|
TRUST_BOUNDARY_TEXT,
|
|
BYPASS_MARKER_PREFIX,
|
|
} 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("protocol path detection", () => {
|
|
it("detects protocol path when messages contain marker and trust boundary", () => {
|
|
const messages = [
|
|
{
|
|
content: `Start ${ROOT_PAIR_START_MARKER} ${TRUST_BOUNDARY_TEXT} end`,
|
|
},
|
|
];
|
|
expect(hasProtocolPath(messages)).toBe(true);
|
|
});
|
|
|
|
it("detects protocol path when payload contains marker and trust boundary", () => {
|
|
const payload = {
|
|
body: `${ROOT_PAIR_START_MARKER}\n${TRUST_BOUNDARY_TEXT}`,
|
|
};
|
|
expect(hasProtocolPath(undefined, payload)).toBe(true);
|
|
});
|
|
|
|
it("returns false when only marker is present", () => {
|
|
const messages = [{ content: `hello ${ROOT_PAIR_START_MARKER} world` }];
|
|
expect(hasProtocolPath(messages)).toBe(false);
|
|
});
|
|
|
|
it("returns false when only trust boundary is present", () => {
|
|
const messages = [{ content: `hello ${TRUST_BOUNDARY_TEXT} world` }];
|
|
expect(hasProtocolPath(messages)).toBe(false);
|
|
});
|
|
|
|
it("returns false when neither marker nor trust boundary is present", () => {
|
|
const messages = [{ content: "plain text" }];
|
|
expect(hasProtocolPath(messages)).toBe(false);
|
|
});
|
|
|
|
it("returns false for empty messages and undefined payload", () => {
|
|
expect(hasProtocolPath([])).toBe(false);
|
|
expect(hasProtocolPath(undefined, undefined)).toBe(false);
|
|
});
|
|
|
|
it("handles array content blocks", () => {
|
|
const messages = [
|
|
{
|
|
content: [
|
|
{ type: "text", text: ROOT_PAIR_START_MARKER },
|
|
{ type: "text", text: TRUST_BOUNDARY_TEXT },
|
|
],
|
|
},
|
|
];
|
|
expect(hasProtocolPath(messages)).toBe(true);
|
|
});
|
|
|
|
it("requires both elements in the same combined source", () => {
|
|
const messages = [{ content: ROOT_PAIR_START_MARKER }];
|
|
const payload = { body: TRUST_BOUNDARY_TEXT };
|
|
expect(hasProtocolPath(messages, payload)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("advisory reminder", () => {
|
|
it("returns a visible lightweight reminder with no synthetic artifacts", () => {
|
|
const reminder = buildAdvisoryReminder();
|
|
expect(reminder).toContain("advisory mode active");
|
|
expect(reminder).toContain("📋");
|
|
expect(reminder).not.toContain(ROOT_PAIR_START_MARKER);
|
|
expect(reminder).not.toContain("### Root index");
|
|
expect(reminder).not.toContain("### Root map");
|
|
});
|
|
});
|
|
|
|
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");
|
|
});
|
|
});
|
|
|
|
describe("sensitive action detection", () => {
|
|
it("detects explicit edit_intent event type", () => {
|
|
expect(isSensitiveAction("edit_intent", [])).toBe(true);
|
|
});
|
|
|
|
it("detects explicit architecture_sensitive event type", () => {
|
|
expect(isSensitiveAction("architecture_sensitive", [])).toBe(true);
|
|
});
|
|
|
|
it("falls back to edit-intent heuristic for generic events", () => {
|
|
expect(
|
|
isSensitiveAction("user_chat", [{ content: "edit the file" }]),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("falls back to architecture heuristic for generic events", () => {
|
|
expect(
|
|
isSensitiveAction("user_chat", [
|
|
{ content: "this is an architecture decision" },
|
|
]),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("treats compaction as non-sensitive", () => {
|
|
expect(isSensitiveAction("compaction", [{ content: "compacting" }])).toBe(
|
|
false,
|
|
);
|
|
});
|
|
|
|
it("treats agent_start as non-sensitive", () => {
|
|
expect(isSensitiveAction("agent_start", [{ content: "hi" }])).toBe(false);
|
|
});
|
|
|
|
it("returns false for irrelevant messages without event type", () => {
|
|
expect(
|
|
isSensitiveAction(undefined, [{ content: "what is the weather?" }]),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("returns false when no event type or messages are provided", () => {
|
|
expect(isSensitiveAction(undefined, undefined)).toBe(false);
|
|
});
|
|
|
|
it("handles array content blocks", () => {
|
|
expect(
|
|
isSensitiveAction(undefined, [
|
|
{
|
|
content: [
|
|
{ type: "text", text: "please " },
|
|
{ type: "text", text: "refactor the module" },
|
|
],
|
|
},
|
|
]),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("detects sensitive action from payload when messages are empty", () => {
|
|
expect(isSensitiveAction("user_chat", [], "edit the file")).toBe(true);
|
|
});
|
|
|
|
it("detects architecture-sensitive action from payload", () => {
|
|
expect(isSensitiveAction("user_chat", [], "architecture decision")).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it("returns false for irrelevant payload without messages or event type", () => {
|
|
expect(
|
|
isSensitiveAction(undefined, undefined, "what is the weather?"),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("combines messages and payload for heuristic detection", () => {
|
|
expect(
|
|
isSensitiveAction(
|
|
"user_chat",
|
|
[{ content: "please consider" }],
|
|
"the architecture decision",
|
|
),
|
|
).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("bypass marker parsing", () => {
|
|
it("detects a valid bypass marker in messages", () => {
|
|
expect(
|
|
messagesHaveBypass([{ content: "[PI_MAP_BYPASS: temporary fix]" }]),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("detects a valid bypass marker prefix", () => {
|
|
expect(BYPASS_MARKER_PREFIX).toBe("[PI_MAP_BYPASS:");
|
|
});
|
|
|
|
it("returns false when no marker is present", () => {
|
|
expect(messagesHaveBypass([{ content: "plain text" }])).toBe(false);
|
|
});
|
|
|
|
it("extracts the bypass reason", () => {
|
|
expect(
|
|
extractBypassReason([{ content: "[PI_MAP_BYPASS: temporary fix]" }]),
|
|
).toBe("temporary fix");
|
|
});
|
|
|
|
it("returns undefined when marker is absent", () => {
|
|
expect(extractBypassReason([{ content: "plain text" }])).toBeUndefined();
|
|
});
|
|
|
|
it("rejects an empty bypass reason", () => {
|
|
expect(
|
|
extractBypassReason([{ content: "[PI_MAP_BYPASS:]" }]),
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it("rejects a whitespace-only bypass reason", () => {
|
|
expect(
|
|
extractBypassReason([{ content: "[PI_MAP_BYPASS: ]" }]),
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it("extracts the first reason when multiple markers are present", () => {
|
|
expect(
|
|
extractBypassReason([
|
|
{
|
|
content: "[PI_MAP_BYPASS: first] [PI_MAP_BYPASS: second]",
|
|
},
|
|
]),
|
|
).toBe("first");
|
|
});
|
|
|
|
it("handles array content blocks", () => {
|
|
expect(
|
|
extractBypassReason([
|
|
{
|
|
content: [
|
|
{ type: "text", text: "[PI_MAP_BYPASS: " },
|
|
{ type: "text", text: "array reason]" },
|
|
],
|
|
},
|
|
]),
|
|
).toBe("array reason");
|
|
});
|
|
|
|
it("detects bypass marker in payload string", () => {
|
|
expect(
|
|
messagesHaveBypass(undefined, "[PI_MAP_BYPASS: payload reason]"),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("detects bypass marker in payload object", () => {
|
|
expect(
|
|
messagesHaveBypass(undefined, {
|
|
body: "[PI_MAP_BYPASS: object reason]",
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("extracts reason from payload when messages have none", () => {
|
|
expect(
|
|
extractBypassReason(undefined, "[PI_MAP_BYPASS: payload reason]"),
|
|
).toBe("payload reason");
|
|
});
|
|
|
|
it("prefers first marker across messages and payload", () => {
|
|
expect(
|
|
extractBypassReason(
|
|
[{ content: "[PI_MAP_BYPASS: msg reason]" }],
|
|
"[PI_MAP_BYPASS: payload reason]",
|
|
),
|
|
).toBe("msg reason");
|
|
});
|
|
|
|
it("extracts reason with colon inside", () => {
|
|
expect(
|
|
extractBypassReason([{ content: "[PI_MAP_BYPASS: fix: edge case]" }]),
|
|
).toBe("fix: edge case");
|
|
});
|
|
|
|
it("rejects bypass marker with only newlines/spaces", () => {
|
|
expect(
|
|
extractBypassReason([{ content: "[PI_MAP_BYPASS:\n\t ]" }]),
|
|
).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("strict bypass evaluation", () => {
|
|
it("never guards in non-strict modes", () => {
|
|
for (const mode of ["off", "advisory", "strong"] as const) {
|
|
const decision = evaluateStrictBypass(
|
|
{ messages: [{ content: "edit the file" }], type: "edit_intent" },
|
|
mode,
|
|
);
|
|
expect(decision.guard).toBe(false);
|
|
}
|
|
});
|
|
|
|
it("does not guard non-sensitive turns in strict mode", () => {
|
|
const decision = evaluateStrictBypass(
|
|
{ messages: [{ content: "hi" }], type: "user_chat" },
|
|
"strict",
|
|
);
|
|
expect(decision.guard).toBe(false);
|
|
});
|
|
|
|
it("guards sensitive turns when protocol path is missing", () => {
|
|
const decision = evaluateStrictBypass(
|
|
{ messages: [{ content: "edit the file" }], type: "edit_intent" },
|
|
"strict",
|
|
);
|
|
expect(decision.guard).toBe(true);
|
|
expect(decision.reason).toContain("protocol path missing");
|
|
});
|
|
|
|
it("does not guard sensitive turns when protocol path is present", () => {
|
|
const decision = evaluateStrictBypass(
|
|
{
|
|
messages: [
|
|
{
|
|
content: `${ROOT_PAIR_START_MARKER} ${TRUST_BOUNDARY_TEXT}`,
|
|
},
|
|
],
|
|
type: "edit_intent",
|
|
},
|
|
"strict",
|
|
);
|
|
expect(decision.guard).toBe(false);
|
|
});
|
|
|
|
it("uses heuristic fallback for generic sensitive events", () => {
|
|
const decision = evaluateStrictBypass(
|
|
{
|
|
messages: [{ content: "refactor the module" }],
|
|
type: "user_chat",
|
|
},
|
|
"strict",
|
|
);
|
|
expect(decision.guard).toBe(true);
|
|
});
|
|
|
|
it("clears the guard when a valid inline bypass marker is present", () => {
|
|
const decision = evaluateStrictBypass(
|
|
{
|
|
messages: [
|
|
{
|
|
content: "[PI_MAP_BYPASS: emergency patch] edit the file",
|
|
},
|
|
],
|
|
type: "edit_intent",
|
|
},
|
|
"strict",
|
|
);
|
|
expect(decision.guard).toBe(false);
|
|
expect(decision.bypassMarker).toBe("emergency patch");
|
|
});
|
|
|
|
it("does not clear the guard for an empty bypass marker", () => {
|
|
const decision = evaluateStrictBypass(
|
|
{
|
|
messages: [{ content: "[PI_MAP_BYPASS:] edit the file" }],
|
|
type: "edit_intent",
|
|
},
|
|
"strict",
|
|
);
|
|
expect(decision.guard).toBe(true);
|
|
});
|
|
|
|
it("does not guard when event is null or undefined", () => {
|
|
expect(evaluateStrictBypass(undefined, "strict").guard).toBe(false);
|
|
expect(evaluateStrictBypass(null, "strict").guard).toBe(false);
|
|
});
|
|
|
|
it("does not guard when payload-only protocol path is present", () => {
|
|
const decision = evaluateStrictBypass(
|
|
{
|
|
messages: [{ content: "edit the file" }],
|
|
payload: `${ROOT_PAIR_START_MARKER}\n${TRUST_BOUNDARY_TEXT}`,
|
|
type: "edit_intent",
|
|
},
|
|
"strict",
|
|
);
|
|
expect(decision.guard).toBe(false);
|
|
});
|
|
|
|
it("clears guard when bypass marker is only in payload", () => {
|
|
const decision = evaluateStrictBypass(
|
|
{
|
|
messages: [{ content: "edit the file" }],
|
|
payload: "[PI_MAP_BYPASS: emergency patch]",
|
|
type: "edit_intent",
|
|
},
|
|
"strict",
|
|
);
|
|
expect(decision.guard).toBe(false);
|
|
expect(decision.bypassMarker).toBe("emergency patch");
|
|
});
|
|
|
|
it("guards when payload-only bypass marker has empty reason", () => {
|
|
const decision = evaluateStrictBypass(
|
|
{
|
|
messages: [{ content: "edit the file" }],
|
|
payload: "[PI_MAP_BYPASS:]",
|
|
type: "edit_intent",
|
|
},
|
|
"strict",
|
|
);
|
|
expect(decision.guard).toBe(true);
|
|
});
|
|
|
|
it("uses heuristic fallback for payload-only sensitive events", () => {
|
|
const decision = evaluateStrictBypass(
|
|
{
|
|
messages: [],
|
|
payload: "refactor the module",
|
|
type: "user_chat",
|
|
},
|
|
"strict",
|
|
);
|
|
expect(decision.guard).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("strict bypass guard message", () => {
|
|
it("includes the provided reason", () => {
|
|
const guard = buildStrictBypassGuard("custom reason");
|
|
expect(guard).toContain("custom reason");
|
|
expect(guard).toContain("🛑");
|
|
});
|
|
|
|
it("includes a default reason when none is provided", () => {
|
|
const guard = buildStrictBypassGuard();
|
|
expect(guard).toContain("sensitive action");
|
|
expect(guard).toContain("protocol path");
|
|
});
|
|
|
|
it("tells the user how to proceed", () => {
|
|
const guard = buildStrictBypassGuard();
|
|
expect(guard).toContain("PI_MAP_BYPASS");
|
|
expect(guard).toContain("restore the project-map context");
|
|
});
|
|
|
|
it("does not contain synthetic root-pair content", () => {
|
|
const guard = buildStrictBypassGuard();
|
|
expect(guard).not.toContain(ROOT_PAIR_START_MARKER);
|
|
expect(guard).not.toContain("### Root index");
|
|
expect(guard).not.toContain("### Root map");
|
|
});
|
|
});
|
|
});
|