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:
318
packages/engine/src/__tests__/pi-layers-wiring.test.ts
Normal file
318
packages/engine/src/__tests__/pi-layers-wiring.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
104
packages/engine/src/__tests__/skill-scoping-integration.test.ts
Normal file
104
packages/engine/src/__tests__/skill-scoping-integration.test.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user