test(engine): improve coverage for prompt layers wiring, backward compat, and skill scoping

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthew Greenberg
2026-05-08 20:21:01 -04:00
parent 8570f955e8
commit 441191332b
3 changed files with 611 additions and 0 deletions

View File

@@ -0,0 +1,318 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { PathLike } from "node:fs";
/**
* Tests that createFnAgent correctly wires prompt layers to
* DefaultResourceLoader and that tools are sorted deterministically.
*
* These tests verify the actual subsystem wiring rather than testing
* the layer logic in isolation.
*/
const createAgentSessionMock = vi.fn();
const createCodingToolsMock = vi.fn(() => []);
const createReadOnlyToolsMock = vi.fn(() => []);
const createExtensionRuntimeMock = vi.fn();
const discoverAndLoadExtensionsMock = vi.fn().mockResolvedValue({
runtime: { pendingProviderRegistrations: [] },
errors: [],
});
const packageManagerResolveMock = vi.fn().mockResolvedValue({ extensions: [] });
const findMock = vi.fn();
const getAllMock = vi.fn(() => [] as any[]);
const registerProviderMock = vi.fn();
const refreshMock = vi.fn();
const settingsManagerInMemoryMock = vi.fn(() => ({ kind: "settings-manager" }));
const setFallbackResolverMock = vi.fn();
const reloadMock = vi.fn(async () => {});
const execSyncMock = vi.fn((_cmd?: any, _opts?: any) => "");
const existsSyncMock = vi.fn((_path: PathLike) => false);
const readFileSyncMock = vi.fn((_path?: any) => "{}");
const readCustomProvidersMock = vi.fn(() => []);
// Capture DefaultResourceLoader constructor args
let capturedResourceLoaderOptions: any = null;
vi.mock("node:child_process", () => {
const execSyncFn = execSyncMock;
const kPromisifyCustom = Symbol.for("nodejs.util.promisify.custom");
const execFn: any = vi.fn((cmd: string, opts: any, cb: any) => {
const callback = typeof opts === "function" ? opts : cb;
const options = typeof opts === "function" ? {} : (opts ?? {});
try {
const out = execSyncFn(cmd, { ...options, stdio: ["pipe", "pipe", "pipe"] });
const stdout = out === undefined ? "" : out.toString();
if (typeof callback === "function") callback(null, stdout, "");
} catch (err) {
if (typeof callback === "function") {
const error = err as { stdout?: string; stderr?: string };
callback(err, error?.stdout?.toString?.() ?? "", error?.stderr?.toString?.() ?? "");
}
}
});
execFn[kPromisifyCustom] = (cmd: string, opts?: any) =>
new Promise((resolve, reject) => {
execFn(cmd, opts, (err: any, stdout: string, stderr: string) => {
if (err) {
(err as Record<string, unknown>).stdout = stdout;
(err as Record<string, unknown>).stderr = stderr;
reject(err);
} else {
resolve({ stdout, stderr });
}
});
});
return { execSync: execSyncFn, exec: execFn };
});
vi.mock("node:fs", async () => {
const actual = await vi.importActual<typeof import("node:fs")>("node:fs");
return {
...actual,
existsSync: existsSyncMock,
readFileSync: readFileSyncMock,
};
});
vi.mock("../custom-providers.js", () => ({
readCustomProviders: readCustomProvidersMock,
}));
vi.mock("@mariozechner/pi-coding-agent", () => ({
AuthStorage: {
create: () => ({
setFallbackResolver: setFallbackResolverMock,
}),
},
createAgentSession: createAgentSessionMock,
createBashTool: () => ({ name: "bash" }),
createCodingTools: createCodingToolsMock,
createEditTool: () => ({ name: "edit" }),
createExtensionRuntime: createExtensionRuntimeMock,
createFindTool: () => ({ name: "find" }),
createGrepTool: () => ({ name: "grep" }),
createLsTool: () => ({ name: "ls" }),
createReadOnlyTools: createReadOnlyToolsMock,
createReadTool: () => ({ name: "read" }),
createWriteTool: () => ({ name: "write" }),
DefaultResourceLoader: class {
constructor(options: any) {
capturedResourceLoaderOptions = options;
}
async reload() {
await reloadMock();
}
},
DefaultPackageManager: class {
async resolve() {
return packageManagerResolveMock();
}
},
discoverAndLoadExtensions: discoverAndLoadExtensionsMock,
getAgentDir: () => "/mock-agent-dir",
ModelRegistry: class {
static create(..._args: unknown[]) {
return new (this as unknown as new () => unknown)();
}
find(provider: string, modelId: string) {
return findMock(provider, modelId);
}
getAll() {
return getAllMock();
}
registerProvider(name: string, config: unknown) {
return registerProviderMock(name, config);
}
refresh() {
return refreshMock();
}
},
SessionManager: {
inMemory: () => ({ kind: "session-manager" }),
},
SettingsManager: {
create: vi.fn(),
inMemory: settingsManagerInMemoryMock,
},
}));
describe("createFnAgent prompt layer wiring", () => {
beforeEach(() => {
vi.clearAllMocks();
capturedResourceLoaderOptions = null;
execSyncMock.mockReturnValue("");
existsSyncMock.mockReturnValue(false);
readFileSyncMock.mockReturnValue("{}");
readCustomProvidersMock.mockReturnValue([]);
findMock.mockImplementation((provider: string, modelId: string) => ({ provider, id: modelId }));
createAgentSessionMock.mockResolvedValue({
session: {
prompt: vi.fn(),
subscribe: vi.fn(),
dispose: vi.fn(),
setThinkingLevel: vi.fn(),
},
});
});
it("passes stable layer as systemPromptOverride when layers provided", async () => {
const { createFnAgent } = await import("../pi.js");
await createFnAgent({
cwd: "/tmp/test-project",
systemPrompt: "Stable prefix.\n\nDynamic suffix.",
systemPromptLayers: {
stable: "Stable prefix.",
dynamic: "Dynamic suffix.",
},
});
expect(capturedResourceLoaderOptions).toBeDefined();
const override = capturedResourceLoaderOptions.systemPromptOverride();
expect(override).toBe("Stable prefix.");
});
it("passes dynamic layer via appendSystemPromptOverride when layers provided", async () => {
const { createFnAgent } = await import("../pi.js");
await createFnAgent({
cwd: "/tmp/test-project",
systemPrompt: "Stable.\n\nDynamic content.",
systemPromptLayers: {
stable: "Stable.",
dynamic: "Dynamic content.",
},
});
expect(capturedResourceLoaderOptions).toBeDefined();
const appended = capturedResourceLoaderOptions.appendSystemPromptOverride();
expect(appended).toEqual(["Dynamic content."]);
});
it("falls back to full systemPrompt when no layers provided", async () => {
const { createFnAgent } = await import("../pi.js");
await createFnAgent({
cwd: "/tmp/test-project",
systemPrompt: "Full system prompt.",
});
expect(capturedResourceLoaderOptions).toBeDefined();
const override = capturedResourceLoaderOptions.systemPromptOverride();
expect(override).toBe("Full system prompt.");
});
it("returns empty array from appendSystemPromptOverride when no layers", async () => {
const { createFnAgent } = await import("../pi.js");
await createFnAgent({
cwd: "/tmp/test-project",
systemPrompt: "Full prompt.",
});
const appended = capturedResourceLoaderOptions.appendSystemPromptOverride();
expect(appended).toEqual([]);
});
it("returns empty array from appendSystemPromptOverride when dynamic is empty", async () => {
const { createFnAgent } = await import("../pi.js");
await createFnAgent({
cwd: "/tmp/test-project",
systemPrompt: "Stable only.",
systemPromptLayers: {
stable: "Stable only.",
dynamic: "",
},
});
const appended = capturedResourceLoaderOptions.appendSystemPromptOverride();
expect(appended).toEqual([]);
});
});
describe("createFnAgent deterministic tool ordering", () => {
beforeEach(() => {
vi.clearAllMocks();
capturedResourceLoaderOptions = null;
execSyncMock.mockReturnValue("");
existsSyncMock.mockReturnValue(false);
readFileSyncMock.mockReturnValue("{}");
readCustomProvidersMock.mockReturnValue([]);
findMock.mockImplementation((provider: string, modelId: string) => ({ provider, id: modelId }));
createAgentSessionMock.mockResolvedValue({
session: {
prompt: vi.fn(),
subscribe: vi.fn(),
dispose: vi.fn(),
setThinkingLevel: vi.fn(),
},
});
});
it("passes tools to createAgentSession in alphabetical order", async () => {
const { createFnAgent } = await import("../pi.js");
await createFnAgent({
cwd: "/tmp/test-project",
systemPrompt: "Test.",
tools: "coding",
});
expect(createAgentSessionMock).toHaveBeenCalled();
const callArgs = createAgentSessionMock.mock.calls[0][0];
const toolNames = (callArgs.customTools ?? []).map((t: any) => t.name);
// Tools should be in alphabetical order
const sorted = [...toolNames].sort();
expect(toolNames).toEqual(sorted);
});
it("sorts custom tools mixed with built-in tools", async () => {
const { createFnAgent } = await import("../pi.js");
await createFnAgent({
cwd: "/tmp/test-project",
systemPrompt: "Test.",
tools: "coding",
customTools: [
{ name: "zzz_custom", execute: vi.fn() } as any,
{ name: "aaa_custom", execute: vi.fn() } as any,
],
});
const callArgs = createAgentSessionMock.mock.calls[0][0];
const toolNames = (callArgs.customTools ?? []).map((t: any) => t.name);
const sorted = [...toolNames].sort();
expect(toolNames).toEqual(sorted);
});
it("sorts readonly tools with custom tools", async () => {
createReadOnlyToolsMock.mockReturnValueOnce([
{ name: "read" },
{ name: "grep" },
{ name: "find" },
] as any);
const { createFnAgent } = await import("../pi.js");
await createFnAgent({
cwd: "/tmp/test-project",
systemPrompt: "Test.",
tools: "readonly",
customTools: [
{ name: "fn_task_list", execute: vi.fn() } as any,
],
});
const callArgs = createAgentSessionMock.mock.calls[0][0];
const toolNames = (callArgs.customTools ?? []).map((t: any) => t.name);
const sorted = [...toolNames].sort();
expect(toolNames).toEqual(sorted);
});
});

View File

@@ -0,0 +1,189 @@
import { describe, it, expect } from "vitest";
import { buildSystemPromptWithInstructions } from "../agent-instructions.js";
import { buildPromptLayers, collapsePromptLayers } from "../prompt-layers.js";
/**
* Backward compatibility tests: verify that the new layered prompt approach
* produces byte-identical output to the legacy buildSystemPromptWithInstructions
* + manual concatenation pattern used by each subsystem.
*/
describe("prompt layers backward compatibility", () => {
describe("collapsed layers match buildSystemPromptWithInstructions", () => {
it("matches when only base prompt is provided", () => {
const basePrompt = "You are a reviewer.";
const oldResult = buildSystemPromptWithInstructions(basePrompt, "");
const layers = buildPromptLayers({ basePrompt });
const newResult = collapsePromptLayers(layers);
expect(newResult).toBe(oldResult);
});
it("matches with base prompt and instructions", () => {
const basePrompt = "You are a reviewer.";
const instructions = "Check for SQL injection.";
const oldResult = buildSystemPromptWithInstructions(basePrompt, instructions);
const layers = buildPromptLayers({
basePrompt,
agentInstructions: instructions,
});
const newResult = collapsePromptLayers(layers);
expect(newResult).toBe(oldResult);
});
it("matches with base prompt, instructions, and plugin contributions", () => {
const basePrompt = "You are an executor.";
const instructions = "Follow TDD.";
const plugins = "## Plugin: security\n\nScan for vulnerabilities.";
// Old approach: buildSystemPromptWithInstructions + manual concatenation
const oldSystemPrompt = buildSystemPromptWithInstructions(basePrompt, instructions);
const oldResult = `${oldSystemPrompt}\n\n${plugins}`;
// New approach: buildPromptLayers + collapsePromptLayers
const layers = buildPromptLayers({
basePrompt,
agentInstructions: instructions,
pluginContributions: plugins,
});
const newResult = collapsePromptLayers(layers);
expect(newResult).toBe(oldResult);
});
it("matches with empty instructions and plugin contributions", () => {
const basePrompt = "You are a triage agent.";
const plugins = "## Plugin: research\n\nUse web search.";
// Old approach: buildSystemPromptWithInstructions returns base (empty instructions)
// then plugins appended
const oldSystemPrompt = buildSystemPromptWithInstructions(basePrompt, "");
const oldResult = `${oldSystemPrompt}\n\n${plugins}`;
const layers = buildPromptLayers({
basePrompt,
pluginContributions: plugins,
});
const newResult = collapsePromptLayers(layers);
expect(newResult).toBe(oldResult);
});
it("matches with empty instructions and no plugins", () => {
const basePrompt = "You are a heartbeat agent.";
const oldResult = buildSystemPromptWithInstructions(basePrompt, "");
const layers = buildPromptLayers({ basePrompt });
const newResult = collapsePromptLayers(layers);
expect(newResult).toBe(oldResult);
});
});
describe("reviewer assembly pattern", () => {
it("reproduces the reviewer prompt assembly", () => {
const basePrompt = "You are an independent code and plan reviewer.";
const memoryInstructions = "\n## Memory\n\nUse fn_memory_search.";
const agentInstructions = "Focus on security.";
const plugins = "## Plugin: lint\n\nCheck eslint.";
// Old reviewer pattern:
// 1. buildSystemPromptWithInstructions(base + memory, instructions)
// 2. if plugins: concatenate
const oldSystemPrompt = buildSystemPromptWithInstructions(
basePrompt + memoryInstructions,
agentInstructions,
);
const oldResult = `${oldSystemPrompt}\n\n${plugins}`;
// New reviewer pattern:
const layers = buildPromptLayers({
basePrompt: basePrompt + memoryInstructions,
agentInstructions,
pluginContributions: plugins,
});
const newResult = collapsePromptLayers(layers);
expect(newResult).toBe(oldResult);
});
});
describe("executor assembly pattern", () => {
it("reproduces the executor prompt assembly", () => {
const basePrompt = "You are a task execution agent.";
const agentInstructions = "Follow the spec precisely.";
const plugins = "## Plugin: deploy\n\nCheck CI status.";
// Old executor pattern:
// 1. buildSystemPromptWithInstructions(base, instructions)
// 2. if plugins: concatenate
const oldSystemPrompt = buildSystemPromptWithInstructions(
basePrompt,
agentInstructions,
);
const oldResult = `${oldSystemPrompt}\n\n${plugins}`;
const layers = buildPromptLayers({
basePrompt,
agentInstructions,
pluginContributions: plugins,
});
const newResult = collapsePromptLayers(layers);
expect(newResult).toBe(oldResult);
});
});
describe("heartbeat assembly pattern", () => {
it("reproduces the heartbeat prompt assembly with multi-part instructions", () => {
const basePrompt = "You are a heartbeat agent.";
const identitySection = "## Identity\n\nYou are Agent-1.";
const memoryInstructions = "## Memory\n\nUse memory tools.";
const selfImprovePrompt = "## Self-Improvement\n\nReview your performance.";
// Old heartbeat pattern:
// 1. Join identity + memory + selfImprove with \n\n
// 2. buildSystemPromptWithInstructions(base, joined)
// 3. if plugins: concatenate
const joinedInstructions = [identitySection, memoryInstructions, selfImprovePrompt]
.filter((part) => part.trim())
.join("\n\n");
const oldResult = buildSystemPromptWithInstructions(basePrompt, joinedInstructions);
const layers = buildPromptLayers({
basePrompt,
agentInstructions: joinedInstructions,
});
const newResult = collapsePromptLayers(layers);
expect(newResult).toBe(oldResult);
});
});
describe("triage assembly pattern", () => {
it("reproduces the triage prompt assembly with multi-part instructions", () => {
const basePrompt = "You are a task specification agent.";
const identitySection = "## Identity\n\nYou are TriageBot.";
const triageInstructions = "Be thorough.";
const researchGuidance = "## Research\n\nUse web search when needed.";
// Old triage pattern:
// 1. Join identity + instructions + research with \n\n
// 2. buildSystemPromptWithInstructions(base, joined)
const joinedInstructions = [identitySection, triageInstructions, researchGuidance]
.filter((section) => section.trim())
.join("\n\n");
const oldResult = buildSystemPromptWithInstructions(basePrompt, joinedInstructions);
const layers = buildPromptLayers({
basePrompt,
agentInstructions: joinedInstructions,
});
const newResult = collapsePromptLayers(layers);
expect(newResult).toBe(oldResult);
});
});
});

View File

@@ -0,0 +1,104 @@
import { describe, it, expect } from "vitest";
import { getSkillPurposeFilter } from "../skill-resolver.js";
/**
* Integration-style tests for getSkillPurposeFilter that verify filtering
* against a realistic set of skill names typical of a Fusion project.
*
* The existing skill-resolver-scoping.test.ts tests individual skill names
* in isolation. These tests verify the aggregate filtering behavior: how
* many skills each purpose loads from a full project skill set, and that
* the filter produces sensible subsets.
*/
describe("getSkillPurposeFilter integration", () => {
// Simulate a realistic set of skill names from a Fusion project
const ALL_SKILLS = [
"fusion",
"code-review",
"security-review",
"review",
"deployment",
"task-management",
"monitoring",
"heartbeat",
"research",
"web-search",
];
it("executor loads all skills", () => {
const filter = getSkillPurposeFilter("executor");
const loaded = ALL_SKILLS.filter(filter);
expect(loaded).toEqual(ALL_SKILLS);
});
it("reviewer loads only review-related skills", () => {
const filter = getSkillPurposeFilter("reviewer");
const loaded = ALL_SKILLS.filter(filter);
expect(loaded).toEqual(["code-review", "security-review", "review"]);
expect(loaded).not.toContain("fusion");
expect(loaded).not.toContain("deployment");
expect(loaded).not.toContain("monitoring");
});
it("heartbeat loads only monitoring-related skills", () => {
const filter = getSkillPurposeFilter("heartbeat");
const loaded = ALL_SKILLS.filter(filter);
expect(loaded).toEqual(["monitoring", "heartbeat"]);
expect(loaded).not.toContain("fusion");
expect(loaded).not.toContain("code-review");
});
it("triage loads all skills (same as executor)", () => {
const filter = getSkillPurposeFilter("triage");
const loaded = ALL_SKILLS.filter(filter);
expect(loaded).toEqual(ALL_SKILLS);
});
it("reviewer filters out the majority of skills", () => {
const filter = getSkillPurposeFilter("reviewer");
const loaded = ALL_SKILLS.filter(filter);
// Reviewer should load significantly fewer skills than total
expect(loaded.length).toBeLessThan(ALL_SKILLS.length / 2);
});
it("heartbeat filters to a minimal subset", () => {
const filter = getSkillPurposeFilter("heartbeat");
const loaded = ALL_SKILLS.filter(filter);
// Heartbeat should have even fewer than reviewer
expect(loaded.length).toBeLessThanOrEqual(2);
});
it("unknown purpose passes all skills through (safe fallback)", () => {
const filter = getSkillPurposeFilter("some-future-purpose");
const loaded = ALL_SKILLS.filter(filter);
expect(loaded).toEqual(ALL_SKILLS);
});
it("reviewer and heartbeat produce disjoint sets from the same input", () => {
const reviewerFilter = getSkillPurposeFilter("reviewer");
const heartbeatFilter = getSkillPurposeFilter("heartbeat");
const reviewerSkills = ALL_SKILLS.filter(reviewerFilter);
const heartbeatSkills = ALL_SKILLS.filter(heartbeatFilter);
// No skill should appear in both reviewer and heartbeat sets
const overlap = reviewerSkills.filter((s) => heartbeatSkills.includes(s));
expect(overlap).toEqual([]);
});
it("executor is a superset of all other purpose filters", () => {
const executorFilter = getSkillPurposeFilter("executor");
const reviewerFilter = getSkillPurposeFilter("reviewer");
const heartbeatFilter = getSkillPurposeFilter("heartbeat");
const executorSkills = new Set(ALL_SKILLS.filter(executorFilter));
const reviewerSkills = ALL_SKILLS.filter(reviewerFilter);
const heartbeatSkills = ALL_SKILLS.filter(heartbeatFilter);
for (const skill of reviewerSkills) {
expect(executorSkills.has(skill)).toBe(true);
}
for (const skill of heartbeatSkills) {
expect(executorSkills.has(skill)).toBe(true);
}
});
});