Files
pi-config/extensions/rpiv-pi/extensions/rpiv-core/guidance.test.ts

141 lines
5.2 KiB
TypeScript

import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { createMockPi, writeGuidanceTree } from "@juicesharp/rpiv-test-utils";
import { afterEach, beforeEach, describe, expect, it, type vi } from "vitest";
import { clearInjectionState, handleToolCallGuidance, injectRootGuidance, resolveGuidance } from "./guidance.js";
let projectDir: string;
beforeEach(() => {
projectDir = mkdtempSync(join(tmpdir(), "rpiv-guidance-"));
clearInjectionState();
});
afterEach(() => {
rmSync(projectDir, { recursive: true, force: true });
});
describe("resolveGuidance — ladder", () => {
it("AGENTS.md > CLAUDE.md > architecture.md at depth > 0", () => {
writeGuidanceTree(projectDir, {
"src/AGENTS.md": "agents-body",
"src/CLAUDE.md": "claude-body",
".rpiv/guidance/src/architecture.md": "arch-body",
});
const resolved = resolveGuidance(join(projectDir, "src", "foo.ts"), projectDir);
const srcEntry = resolved.find((r) => r.relativePath.startsWith("src/"));
expect(srcEntry?.kind).toBe("agents");
});
it("depth 0 skips AGENTS/CLAUDE but keeps root architecture.md", () => {
writeGuidanceTree(projectDir, {
"AGENTS.md": "root-agents",
".rpiv/guidance/architecture.md": "root-arch",
});
const resolved = resolveGuidance(join(projectDir, "any", "file.ts"), projectDir);
const rootEntry = resolved.find((r) => r.relativePath === ".rpiv/guidance/architecture.md");
expect(rootEntry?.kind).toBe("architecture");
expect(resolved.some((r) => r.relativePath === "AGENTS.md")).toBe(false);
});
it("returns root-first, specific-last order", () => {
writeGuidanceTree(projectDir, {
".rpiv/guidance/architecture.md": "root",
"a/AGENTS.md": "a",
"a/b/AGENTS.md": "ab",
});
const resolved = resolveGuidance(join(projectDir, "a", "b", "c.ts"), projectDir);
expect(resolved.map((r) => r.content)).toEqual(["root", "a", "ab"]);
});
it("returns empty when file is outside projectDir", () => {
expect(resolveGuidance("/totally/elsewhere/foo.ts", projectDir)).toEqual([]);
});
it("returns empty when nothing exists along the ladder", () => {
expect(resolveGuidance(join(projectDir, "x.ts"), projectDir)).toEqual([]);
});
});
describe("injectRootGuidance", () => {
it("sends root architecture.md when present", () => {
writeGuidanceTree(projectDir, { ".rpiv/guidance/architecture.md": "body" });
const { pi } = createMockPi();
injectRootGuidance(projectDir, pi);
expect(pi.sendMessage).toHaveBeenCalledTimes(1);
const content = (pi.sendMessage as ReturnType<typeof vi.fn>).mock.calls[0][0].content;
expect(content).toContain("body");
expect(content).toContain("reference material, NOT a task");
expect(content).toContain("auto-loaded at session start");
});
it("is idempotent across calls within a session", () => {
writeGuidanceTree(projectDir, { ".rpiv/guidance/architecture.md": "body" });
const { pi } = createMockPi();
injectRootGuidance(projectDir, pi);
injectRootGuidance(projectDir, pi);
expect(pi.sendMessage).toHaveBeenCalledTimes(1);
});
it("re-injects after clearInjectionState", () => {
writeGuidanceTree(projectDir, { ".rpiv/guidance/architecture.md": "body" });
const { pi } = createMockPi();
injectRootGuidance(projectDir, pi);
clearInjectionState();
injectRootGuidance(projectDir, pi);
expect(pi.sendMessage).toHaveBeenCalledTimes(2);
});
it("no-ops when root architecture.md is missing", () => {
const { pi } = createMockPi();
injectRootGuidance(projectDir, pi);
expect(pi.sendMessage).not.toHaveBeenCalled();
});
});
describe("handleToolCallGuidance", () => {
it("skips non-read/edit/write tools", () => {
const { pi } = createMockPi();
handleToolCallGuidance({ toolName: "bash", input: {} }, { cwd: projectDir }, pi);
expect(pi.sendMessage).not.toHaveBeenCalled();
});
it("dedupes per-file across multiple tool_calls", () => {
writeGuidanceTree(projectDir, { "src/AGENTS.md": "a" });
const { pi } = createMockPi();
const ev = { toolName: "read", input: { file_path: join(projectDir, "src", "x.ts") } };
handleToolCallGuidance(ev, { cwd: projectDir }, pi);
handleToolCallGuidance(ev, { cwd: projectDir }, pi);
expect(pi.sendMessage).toHaveBeenCalledTimes(1);
});
it("supports both 'path' and 'file_path' input keys", () => {
writeGuidanceTree(projectDir, { "src/AGENTS.md": "a" });
const { pi } = createMockPi();
handleToolCallGuidance(
{ toolName: "edit", input: { path: join(projectDir, "src", "x.ts") } },
{ cwd: projectDir },
pi,
);
expect(pi.sendMessage).toHaveBeenCalledTimes(1);
});
it("emits one sendMessage combining multiple newly-resolved files", () => {
writeGuidanceTree(projectDir, {
".rpiv/guidance/architecture.md": "root",
"src/AGENTS.md": "src",
});
const { pi } = createMockPi();
handleToolCallGuidance(
{ toolName: "write", input: { file_path: join(projectDir, "src", "x.ts") } },
{ cwd: projectDir },
pi,
);
expect(pi.sendMessage).toHaveBeenCalledTimes(1);
const content = (pi.sendMessage as ReturnType<typeof vi.fn>).mock.calls[0][0].content;
expect(content).toContain("root");
expect(content).toContain("src");
expect(content).toContain("auto-loaded because write touched src/x.ts");
});
});