feat(prompt): implement prompt injection slice 3
This commit is contained in:
@@ -93,6 +93,7 @@ describe("pi-extension", () => {
|
||||
it("registers session_start and before_agent_start events", () => {
|
||||
expect(registeredEvents).toHaveProperty("session_start");
|
||||
expect(registeredEvents).toHaveProperty("before_agent_start");
|
||||
expect(registeredEvents).toHaveProperty("context");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -411,5 +412,559 @@ describe("pi-extension", () => {
|
||||
expect(result.message.display).toBe(false);
|
||||
expect(result.message.content).toContain("root `.pi-map.index.md`");
|
||||
});
|
||||
|
||||
it("skips reinjection when outgoing context already contains marker", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
|
||||
writeFileSync(join(dir, ".pi-map.md"), "# .\n## role\nTest\n");
|
||||
writeFileSync(
|
||||
join(dir, ".pi-project-map.json"),
|
||||
JSON.stringify({ promptInjectionMode: "strong" }),
|
||||
);
|
||||
mockCtx.cwd = dir;
|
||||
|
||||
const handler = registeredEvents.before_agent_start;
|
||||
const result = await handler(
|
||||
{
|
||||
messages: [
|
||||
{ content: "<!-- PI_MAP_ROOT_PAIR_START --> already injected" },
|
||||
],
|
||||
type: "agent_start",
|
||||
},
|
||||
mockCtx,
|
||||
);
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it("reinjects when root-pair artifact changes since last check", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
|
||||
writeFileSync(join(dir, ".pi-map.md"), "# .\n## role\nOriginal\n");
|
||||
writeFileSync(
|
||||
join(dir, ".pi-map.index.md"),
|
||||
"# . (index)\n## role\nTestIndex\n",
|
||||
);
|
||||
writeFileSync(
|
||||
join(dir, ".pi-project-map.json"),
|
||||
JSON.stringify({ promptInjectionMode: "strong" }),
|
||||
);
|
||||
mockCtx.cwd = dir;
|
||||
|
||||
const handler = registeredEvents.before_agent_start;
|
||||
|
||||
// First call establishes baseline mtimes and injects
|
||||
await handler(null, mockCtx);
|
||||
|
||||
// Modify root map on disk
|
||||
writeFileSync(join(dir, ".pi-map.md"), "# .\n## role\nUpdated\n");
|
||||
|
||||
// Second call should detect the change and reinject
|
||||
const result = await handler(null, mockCtx);
|
||||
|
||||
expect(result).toHaveProperty("message");
|
||||
expect(result.message.content).toContain("Updated");
|
||||
expect(result.message.content).toContain("TestIndex");
|
||||
});
|
||||
});
|
||||
|
||||
describe("context event", () => {
|
||||
it("registers a context handler", () => {
|
||||
expect(registeredEvents).toHaveProperty("context");
|
||||
expect(typeof registeredEvents.context).toBe("function");
|
||||
});
|
||||
|
||||
it("injects on relevant turn when marker is absent", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
|
||||
writeFileSync(join(dir, ".pi-map.md"), "# .\n## role\nTestMap\n");
|
||||
writeFileSync(
|
||||
join(dir, ".pi-map.index.md"),
|
||||
"# . (index)\n## role\nTestIndex\n",
|
||||
);
|
||||
writeFileSync(
|
||||
join(dir, ".pi-project-map.json"),
|
||||
JSON.stringify({ promptInjectionMode: "strong" }),
|
||||
);
|
||||
mockCtx.cwd = dir;
|
||||
|
||||
const handler = registeredEvents.context;
|
||||
const result = await handler(
|
||||
{ messages: [{ content: "edit this file" }], type: "edit_intent" },
|
||||
mockCtx,
|
||||
);
|
||||
|
||||
expect(result).toHaveProperty("message");
|
||||
expect(result.message.content).toContain(
|
||||
"<!-- PI_MAP_ROOT_PAIR_START -->",
|
||||
);
|
||||
expect(result.message.content).toContain("TestMap");
|
||||
expect(result.message.content).toContain("TestIndex");
|
||||
});
|
||||
|
||||
it("skips injection when marker is already present in messages", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
|
||||
writeFileSync(join(dir, ".pi-map.md"), "# .\n## role\nTest\n");
|
||||
writeFileSync(
|
||||
join(dir, ".pi-project-map.json"),
|
||||
JSON.stringify({ promptInjectionMode: "strong" }),
|
||||
);
|
||||
mockCtx.cwd = dir;
|
||||
|
||||
const handler = registeredEvents.context;
|
||||
const result = await handler(
|
||||
{
|
||||
messages: [{ content: "<!-- PI_MAP_ROOT_PAIR_START -->" }],
|
||||
type: "edit_intent",
|
||||
},
|
||||
mockCtx,
|
||||
);
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it("skips injection when marker is present in provider payload", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
|
||||
writeFileSync(join(dir, ".pi-map.md"), "# .\n## role\nTest\n");
|
||||
writeFileSync(
|
||||
join(dir, ".pi-project-map.json"),
|
||||
JSON.stringify({ promptInjectionMode: "strong" }),
|
||||
);
|
||||
mockCtx.cwd = dir;
|
||||
|
||||
const handler = registeredEvents.context;
|
||||
const result = await handler(
|
||||
{
|
||||
messages: [{ content: "hi" }],
|
||||
payload: { body: "<!-- PI_MAP_ROOT_PAIR_START -->" },
|
||||
type: "edit_intent",
|
||||
},
|
||||
mockCtx,
|
||||
);
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it("skips injection for irrelevant event types", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
|
||||
writeFileSync(join(dir, ".pi-map.md"), "# .\n## role\nTest\n");
|
||||
writeFileSync(
|
||||
join(dir, ".pi-project-map.json"),
|
||||
JSON.stringify({ promptInjectionMode: "strong" }),
|
||||
);
|
||||
mockCtx.cwd = dir;
|
||||
|
||||
const handler = registeredEvents.context;
|
||||
const result = await handler(
|
||||
{ messages: [{ content: "hi" }], type: "user_chat" },
|
||||
mockCtx,
|
||||
);
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it("skips injection when mode is advisory", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
|
||||
writeFileSync(join(dir, ".pi-map.md"), "# .\n## role\nTest\n");
|
||||
writeFileSync(
|
||||
join(dir, ".pi-project-map.json"),
|
||||
JSON.stringify({ promptInjectionMode: "advisory" }),
|
||||
);
|
||||
mockCtx.cwd = dir;
|
||||
|
||||
const handler = registeredEvents.context;
|
||||
const result = await handler(
|
||||
{ messages: [{ content: "edit this" }], type: "edit_intent" },
|
||||
mockCtx,
|
||||
);
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it("reinjects on compaction event when marker is absent", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
|
||||
writeFileSync(join(dir, ".pi-map.md"), "# .\n## role\nTestMap\n");
|
||||
writeFileSync(
|
||||
join(dir, ".pi-map.index.md"),
|
||||
"# . (index)\n## role\nTestIndex\n",
|
||||
);
|
||||
writeFileSync(
|
||||
join(dir, ".pi-project-map.json"),
|
||||
JSON.stringify({ promptInjectionMode: "strong" }),
|
||||
);
|
||||
mockCtx.cwd = dir;
|
||||
|
||||
const handler = registeredEvents.context;
|
||||
const result = await handler(
|
||||
{ messages: [{ content: "compacting" }], type: "compaction" },
|
||||
mockCtx,
|
||||
);
|
||||
|
||||
expect(result).toHaveProperty("message");
|
||||
expect(result.message.content).toContain(
|
||||
"<!-- PI_MAP_ROOT_PAIR_START -->",
|
||||
);
|
||||
expect(result.message.content).toContain("TestMap");
|
||||
expect(result.message.content).toContain("TestIndex");
|
||||
});
|
||||
|
||||
it("skips reinjection on compaction when marker is present", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
|
||||
writeFileSync(join(dir, ".pi-map.md"), "# .\n## role\nTest\n");
|
||||
writeFileSync(
|
||||
join(dir, ".pi-project-map.json"),
|
||||
JSON.stringify({ promptInjectionMode: "strong" }),
|
||||
);
|
||||
mockCtx.cwd = dir;
|
||||
|
||||
const handler = registeredEvents.context;
|
||||
const result = await handler(
|
||||
{
|
||||
messages: [{ content: "<!-- PI_MAP_ROOT_PAIR_START -->" }],
|
||||
type: "compaction",
|
||||
},
|
||||
mockCtx,
|
||||
);
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it("reinjects on artifact_change event even when marker is present", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
|
||||
writeFileSync(join(dir, ".pi-map.md"), "# .\n## role\nTestMap\n");
|
||||
writeFileSync(
|
||||
join(dir, ".pi-map.index.md"),
|
||||
"# . (index)\n## role\nTestIndex\n",
|
||||
);
|
||||
writeFileSync(
|
||||
join(dir, ".pi-project-map.json"),
|
||||
JSON.stringify({ promptInjectionMode: "strong" }),
|
||||
);
|
||||
mockCtx.cwd = dir;
|
||||
|
||||
const handler = registeredEvents.context;
|
||||
const result = await handler(
|
||||
{
|
||||
messages: [{ content: "<!-- PI_MAP_ROOT_PAIR_START -->" }],
|
||||
type: "artifact_change",
|
||||
},
|
||||
mockCtx,
|
||||
);
|
||||
|
||||
expect(result).toHaveProperty("message");
|
||||
expect(result.message.content).toContain("TestMap");
|
||||
expect(result.message.content).toContain("TestIndex");
|
||||
});
|
||||
|
||||
it("reinjects when root-pair artifact changes since last check", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
|
||||
writeFileSync(join(dir, ".pi-map.md"), "# .\n## role\nOriginal\n");
|
||||
writeFileSync(
|
||||
join(dir, ".pi-map.index.md"),
|
||||
"# . (index)\n## role\nTestIndex\n",
|
||||
);
|
||||
writeFileSync(
|
||||
join(dir, ".pi-project-map.json"),
|
||||
JSON.stringify({ promptInjectionMode: "strong" }),
|
||||
);
|
||||
mockCtx.cwd = dir;
|
||||
|
||||
const handler = registeredEvents.context;
|
||||
|
||||
// Initialize mtimes with an irrelevant turn (no injection)
|
||||
await handler(
|
||||
{ messages: [{ content: "hi" }], type: "user_chat" },
|
||||
mockCtx,
|
||||
);
|
||||
|
||||
// Modify root map on disk
|
||||
writeFileSync(join(dir, ".pi-map.md"), "# .\n## role\nUpdated\n");
|
||||
|
||||
// Next call should detect the change and reinject even on an irrelevant turn
|
||||
const result = await handler(
|
||||
{ messages: [{ content: "hi" }], type: "user_chat" },
|
||||
mockCtx,
|
||||
);
|
||||
|
||||
expect(result).toHaveProperty("message");
|
||||
expect(result.message.content).toContain("Updated");
|
||||
expect(result.message.content).toContain("TestIndex");
|
||||
});
|
||||
|
||||
it("sequence: irrelevant turn skips, first relevant turn reinjects", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
|
||||
writeFileSync(join(dir, ".pi-map.md"), "# .\n## role\nTestMap\n");
|
||||
writeFileSync(
|
||||
join(dir, ".pi-map.index.md"),
|
||||
"# . (index)\n## role\nTestIndex\n",
|
||||
);
|
||||
writeFileSync(
|
||||
join(dir, ".pi-project-map.json"),
|
||||
JSON.stringify({ promptInjectionMode: "strong" }),
|
||||
);
|
||||
mockCtx.cwd = dir;
|
||||
|
||||
const handler = registeredEvents.context;
|
||||
|
||||
const skip = await handler(
|
||||
{ messages: [{ content: "hi" }], type: "user_chat" },
|
||||
mockCtx,
|
||||
);
|
||||
expect(skip).toEqual({});
|
||||
|
||||
const inject = await handler(
|
||||
{ messages: [{ content: "edit this file" }], type: "edit_intent" },
|
||||
mockCtx,
|
||||
);
|
||||
expect(inject).toHaveProperty("message");
|
||||
expect(inject.message.content).toContain("PI_MAP_ROOT_PAIR_START");
|
||||
});
|
||||
|
||||
it("sequence: relevant turn reinjects, subsequent relevant turn skips when marker present", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
|
||||
writeFileSync(join(dir, ".pi-map.md"), "# .\n## role\nTestMap\n");
|
||||
writeFileSync(
|
||||
join(dir, ".pi-map.index.md"),
|
||||
"# . (index)\n## role\nTestIndex\n",
|
||||
);
|
||||
writeFileSync(
|
||||
join(dir, ".pi-project-map.json"),
|
||||
JSON.stringify({ promptInjectionMode: "strong" }),
|
||||
);
|
||||
mockCtx.cwd = dir;
|
||||
|
||||
const handler = registeredEvents.context;
|
||||
|
||||
const first = await handler(
|
||||
{ messages: [{ content: "edit this file" }], type: "edit_intent" },
|
||||
mockCtx,
|
||||
);
|
||||
expect(first).toHaveProperty("message");
|
||||
expect(first.message.content).toContain("PI_MAP_ROOT_PAIR_START");
|
||||
|
||||
const second = await handler(
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
content: `<!-- PI_MAP_ROOT_PAIR_START --> already injected`,
|
||||
},
|
||||
],
|
||||
type: "edit_intent",
|
||||
},
|
||||
mockCtx,
|
||||
);
|
||||
expect(second).toEqual({});
|
||||
});
|
||||
|
||||
it("sequence: edit_intent then compaction reinjects only when marker absent", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
|
||||
writeFileSync(join(dir, ".pi-map.md"), "# .\n## role\nTestMap\n");
|
||||
writeFileSync(
|
||||
join(dir, ".pi-map.index.md"),
|
||||
"# . (index)\n## role\nTestIndex\n",
|
||||
);
|
||||
writeFileSync(
|
||||
join(dir, ".pi-project-map.json"),
|
||||
JSON.stringify({ promptInjectionMode: "strong" }),
|
||||
);
|
||||
mockCtx.cwd = dir;
|
||||
|
||||
const handler = registeredEvents.context;
|
||||
|
||||
const edit = await handler(
|
||||
{ messages: [{ content: "edit this file" }], type: "edit_intent" },
|
||||
mockCtx,
|
||||
);
|
||||
expect(edit).toHaveProperty("message");
|
||||
expect(edit.message.content).toContain("PI_MAP_ROOT_PAIR_START");
|
||||
|
||||
const compaction = await handler(
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
content: `<!-- PI_MAP_ROOT_PAIR_START --> already injected`,
|
||||
},
|
||||
],
|
||||
type: "compaction",
|
||||
},
|
||||
mockCtx,
|
||||
);
|
||||
expect(compaction).toEqual({});
|
||||
});
|
||||
|
||||
it("sequence: architecture_sensitive after irrelevant turn reinjects once", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
|
||||
writeFileSync(join(dir, ".pi-map.md"), "# .\n## role\nTestMap\n");
|
||||
writeFileSync(
|
||||
join(dir, ".pi-map.index.md"),
|
||||
"# . (index)\n## role\nTestIndex\n",
|
||||
);
|
||||
writeFileSync(
|
||||
join(dir, ".pi-project-map.json"),
|
||||
JSON.stringify({ promptInjectionMode: "strong" }),
|
||||
);
|
||||
mockCtx.cwd = dir;
|
||||
|
||||
const handler = registeredEvents.context;
|
||||
|
||||
const skip = await handler(
|
||||
{ messages: [{ content: "hi" }], type: "user_chat" },
|
||||
mockCtx,
|
||||
);
|
||||
expect(skip).toEqual({});
|
||||
|
||||
const first = await handler(
|
||||
{
|
||||
messages: [{ content: "discuss the architecture" }],
|
||||
type: "architecture_sensitive",
|
||||
},
|
||||
mockCtx,
|
||||
);
|
||||
expect(first).toHaveProperty("message");
|
||||
expect(first.message.content).toContain("PI_MAP_ROOT_PAIR_START");
|
||||
|
||||
const second = await handler(
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
content: `<!-- PI_MAP_ROOT_PAIR_START --> already injected`,
|
||||
},
|
||||
],
|
||||
type: "architecture_sensitive",
|
||||
},
|
||||
mockCtx,
|
||||
);
|
||||
expect(second).toEqual({});
|
||||
});
|
||||
|
||||
it("sequence: artifact_change invalidation triggers reinjection even after skipped turns", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
|
||||
writeFileSync(join(dir, ".pi-map.md"), "# .\n## role\nOriginal\n");
|
||||
writeFileSync(
|
||||
join(dir, ".pi-map.index.md"),
|
||||
"# . (index)\n## role\nTestIndex\n",
|
||||
);
|
||||
writeFileSync(
|
||||
join(dir, ".pi-project-map.json"),
|
||||
JSON.stringify({ promptInjectionMode: "strong" }),
|
||||
);
|
||||
mockCtx.cwd = dir;
|
||||
|
||||
const handler = registeredEvents.context;
|
||||
|
||||
// Establish baseline mtimes with an irrelevant turn
|
||||
await handler(
|
||||
{ messages: [{ content: "hi" }], type: "user_chat" },
|
||||
mockCtx,
|
||||
);
|
||||
|
||||
// Relevant turn that finds marker present skips reinjection
|
||||
const skip = await handler(
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
content: `<!-- PI_MAP_ROOT_PAIR_START --> injected`,
|
||||
},
|
||||
],
|
||||
type: "edit_intent",
|
||||
},
|
||||
mockCtx,
|
||||
);
|
||||
expect(skip).toEqual({});
|
||||
|
||||
// Modify root artifacts on disk
|
||||
writeFileSync(join(dir, ".pi-map.md"), "# .\n## role\nUpdated\n");
|
||||
|
||||
// Even an irrelevant turn must reinject because artifacts changed
|
||||
const result = await handler(
|
||||
{ messages: [{ content: "hi" }], type: "user_chat" },
|
||||
mockCtx,
|
||||
);
|
||||
expect(result).toHaveProperty("message");
|
||||
expect(result.message.content).toContain("Updated");
|
||||
expect(result.message.content).toContain("TestIndex");
|
||||
});
|
||||
|
||||
it("sequence: strict mode reinjects on relevant turns same as strong mode", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
|
||||
writeFileSync(join(dir, ".pi-map.md"), "# .\n## role\nTestMap\n");
|
||||
writeFileSync(
|
||||
join(dir, ".pi-map.index.md"),
|
||||
"# . (index)\n## role\nTestIndex\n",
|
||||
);
|
||||
writeFileSync(
|
||||
join(dir, ".pi-project-map.json"),
|
||||
JSON.stringify({ promptInjectionMode: "strict" }),
|
||||
);
|
||||
mockCtx.cwd = dir;
|
||||
|
||||
const handler = registeredEvents.context;
|
||||
|
||||
const first = await handler(
|
||||
{ messages: [{ content: "edit this file" }], type: "edit_intent" },
|
||||
mockCtx,
|
||||
);
|
||||
expect(first).toHaveProperty("message");
|
||||
expect(first.message.content).toContain("PI_MAP_ROOT_PAIR_START");
|
||||
|
||||
const second = await handler(
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
content: `<!-- PI_MAP_ROOT_PAIR_START --> already injected`,
|
||||
},
|
||||
],
|
||||
type: "architecture_sensitive",
|
||||
},
|
||||
mockCtx,
|
||||
);
|
||||
expect(second).toEqual({});
|
||||
|
||||
const third = await handler(
|
||||
{ messages: [{ content: "compacting" }], type: "compaction" },
|
||||
mockCtx,
|
||||
);
|
||||
expect(third).toHaveProperty("message");
|
||||
expect(third.message.content).toContain("PI_MAP_ROOT_PAIR_START");
|
||||
});
|
||||
|
||||
it("sequence: mixed relevant and irrelevant turns preserves deterministic behavior", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
|
||||
writeFileSync(join(dir, ".pi-map.md"), "# .\n## role\nTestMap\n");
|
||||
writeFileSync(
|
||||
join(dir, ".pi-map.index.md"),
|
||||
"# . (index)\n## role\nTestIndex\n",
|
||||
);
|
||||
writeFileSync(
|
||||
join(dir, ".pi-project-map.json"),
|
||||
JSON.stringify({ promptInjectionMode: "strong" }),
|
||||
);
|
||||
mockCtx.cwd = dir;
|
||||
|
||||
const handler = registeredEvents.context;
|
||||
|
||||
const turns = [
|
||||
{ type: "user_chat", expectInject: false },
|
||||
{ type: "edit_intent", expectInject: true },
|
||||
{ type: "user_chat", expectInject: false },
|
||||
{ type: "architecture_sensitive", expectInject: false }, // marker present from prior injection
|
||||
{ type: "compaction", expectInject: false }, // marker still present
|
||||
];
|
||||
|
||||
let markerPrefix = "";
|
||||
for (const turn of turns) {
|
||||
const result = await handler(
|
||||
{
|
||||
messages: [{ content: `${markerPrefix}${turn.type}` }],
|
||||
type: turn.type,
|
||||
},
|
||||
mockCtx,
|
||||
);
|
||||
if (turn.expectInject) {
|
||||
expect(result).toHaveProperty("message");
|
||||
expect(result.message.content).toContain("PI_MAP_ROOT_PAIR_START");
|
||||
markerPrefix = "<!-- PI_MAP_ROOT_PAIR_START --> ";
|
||||
} else {
|
||||
expect(result).toEqual({});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user