237 lines
7.6 KiB
TypeScript
237 lines
7.6 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,
|
|
discoverContextWindow,
|
|
estimateTokens,
|
|
findAllArtifactPairs,
|
|
buildInjectionPayload,
|
|
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("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");
|
|
});
|
|
});
|
|
});
|