feat(prompt): implement prompt injection slice 3
This commit is contained in:
@@ -14,6 +14,12 @@ import {
|
||||
estimateTokens,
|
||||
findAllArtifactPairs,
|
||||
buildInjectionPayload,
|
||||
outgoingMessagesHaveMarker,
|
||||
providerPayloadHasMarker,
|
||||
shouldReinjectForEvent,
|
||||
isRelevantTurnForReinjection,
|
||||
detectEditIntent,
|
||||
detectArchitectureSensitiveReasoning,
|
||||
ROOT_PAIR_START_MARKER,
|
||||
ROOT_PAIR_END_MARKER,
|
||||
TRUST_BOUNDARY_TEXT,
|
||||
@@ -172,6 +178,536 @@ describe("prompt-injection helpers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
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-"));
|
||||
|
||||
Reference in New Issue
Block a user