Files
fusion/packages/engine/src/__tests__/session-skill-context.test.ts
gsxdsm 49a459a869 FN-7954: fix plugin skill toggle keys for custom skillFiles paths
Align plugin skill enable/disable reads with the resolved skillFiles path so Skills-view toggles persist and sessions honor them.

- Accept optional skillRelativePath in resolvePluginSkillEnabled for custom skillFiles keys
- Pass resolved relativePath from dashboard skills adapter when merging plugin skills
- Reuse resolved body path in session-skill-context for enable checks and additionalSkillPaths
- Add unit coverage for custom-path round-trip, enable, and disable behavior
- Ship patch changeset for @runfusion/fusion

Files changed:
 .changeset/fn-7954-plugin-skill-toggle-key-fix.md  |  7 +++
 packages/core/src/__tests__/skill-settings.test.ts | 15 ++++++
 packages/core/src/skill-settings.ts                |  8 +++-
 .../dashboard/src/__tests__/skills-adapter.test.ts | 55 ++++++++++++++++++++++
 packages/dashboard/src/skills-adapter.ts           |  1 +
 .../src/__tests__/session-skill-context.test.ts    | 45 ++++++++++++++++++
 packages/engine/src/session-skill-context.ts       | 29 +++++++-----
 7 files changed, 147 insertions(+), 13 deletions(-)

Fusion-Task-Id: FN-7954

Fusion-Task-Lineage: 3399186b-7325-4ed7-8900-85eb2ef98c7e

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-15 10:38:04 -07:00

825 lines
30 KiB
TypeScript

import { describe, it, expect, vi, afterEach } from "vitest";
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import {
normalizeAgentSkills,
collectPluginSkillNames,
buildSessionSkillContext,
buildSessionSkillContextSync,
SKILL_DIAGNOSTIC_MESSAGES,
type SessionPurpose,
} from "../session-skill-context.js";
import type { Agent, AgentStore } from "@fusion/core";
import type { PluginRunner } from "../plugin-runner.js";
const tempDirs: string[] = [];
async function createProjectWithSettings(settings: Record<string, unknown>): Promise<string> {
const projectRoot = await mkdtemp(join(tmpdir(), "session-skill-context-"));
tempDirs.push(projectRoot);
await mkdir(join(projectRoot, ".fusion"), { recursive: true });
await writeFile(join(projectRoot, ".fusion", "settings.json"), JSON.stringify(settings), "utf-8");
return projectRoot;
}
function pluginRunnerWithSkills(
skills: Array<{ pluginId: string; pluginRoot?: string; skill: { name: string; enabled?: boolean; skillFiles?: string[] } }>,
): PluginRunner {
return { getPluginSkills: vi.fn().mockReturnValue(skills) } as unknown as PluginRunner;
}
async function createPluginSkillRoot(skillName: string, body = "# Plugin skill\n"): Promise<{ pluginRoot: string; skillDir: string }> {
const pluginRoot = await mkdtemp(join(tmpdir(), "session-plugin-skill-"));
tempDirs.push(pluginRoot);
const skillDir = join(pluginRoot, "skills", skillName);
await mkdir(skillDir, { recursive: true });
await writeFile(join(skillDir, "SKILL.md"), body, "utf-8");
return { pluginRoot, skillDir };
}
async function createPluginSkillRootAt(relativePath: string, body = "# Plugin skill\n"): Promise<{ pluginRoot: string; skillDir: string }> {
const pluginRoot = await mkdtemp(join(tmpdir(), "session-plugin-skill-"));
tempDirs.push(pluginRoot);
const skillFile = join(pluginRoot, relativePath);
const skillDir = dirname(skillFile);
await mkdir(skillDir, { recursive: true });
await writeFile(skillFile, body, "utf-8");
return { pluginRoot, skillDir };
}
afterEach(async () => {
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
});
describe("normalizeAgentSkills", () => {
it("returns empty array for non-array input", () => {
expect(normalizeAgentSkills(undefined)).toEqual([]);
expect(normalizeAgentSkills(null)).toEqual([]);
expect(normalizeAgentSkills("string")).toEqual([]);
expect(normalizeAgentSkills({})).toEqual([]);
});
it("handles string entries", () => {
const skills = ["triage", "executor", "reviewer"];
expect(normalizeAgentSkills(skills)).toEqual(["triage", "executor", "reviewer"]);
});
it("handles object entries with name property", () => {
const skills = [
{ name: "triage" },
{ name: "executor" },
{ name: "reviewer" },
];
expect(normalizeAgentSkills(skills)).toEqual(["triage", "executor", "reviewer"]);
});
it("handles mixed string and object entries", () => {
const skills = [
"triage",
{ name: "executor" },
{ name: "reviewer" },
"merger",
];
expect(normalizeAgentSkills(skills)).toEqual(["triage", "executor", "reviewer", "merger"]);
});
it("trims whitespace from entries", () => {
const skills = [" triage ", { name: " executor " }];
expect(normalizeAgentSkills(skills)).toEqual(["triage", "executor"]);
});
it("drops empty entries", () => {
const skills = ["", "triage", " ", "executor", { name: "" }, { name: "reviewer" }];
expect(normalizeAgentSkills(skills)).toEqual(["triage", "executor", "reviewer"]);
});
it("drops invalid entries", () => {
const skills = [
123,
null,
{ foo: "bar" },
"triage",
undefined,
{ name: "executor" },
];
expect(normalizeAgentSkills(skills)).toEqual(["triage", "executor"]);
});
it("deduplicates while preserving first occurrence order", () => {
const skills = ["triage", "executor", "triage", "reviewer", "executor"];
expect(normalizeAgentSkills(skills)).toEqual(["triage", "executor", "reviewer"]);
});
it("handles duplicate object entries", () => {
const skills = [
{ name: "triage" },
{ name: "executor" },
{ name: "triage" },
];
expect(normalizeAgentSkills(skills)).toEqual(["triage", "executor"]);
});
it("handles case-sensitive deduplication", () => {
const skills = ["Triage", "triage", "EXECUTOR", "executor"];
expect(normalizeAgentSkills(skills)).toEqual(["Triage", "triage", "EXECUTOR", "executor"]);
});
it("returns empty array for array of only invalid entries", () => {
expect(normalizeAgentSkills([null, undefined, "", 123, {}])).toEqual([]);
});
it("handles object entries with name, deduplicates, trims, and drops invalid entries", () => {
const skills = [
{ name: " review " },
" custom-skill ",
{ name: "review" },
123,
null,
{ foo: "bar" },
"",
{ name: "" },
];
expect(normalizeAgentSkills(skills)).toEqual(["review", "custom-skill"]);
});
});
describe("buildSessionSkillContextSync", () => {
const projectRootDir = "/test/project";
describe("assigned agent skills", () => {
it("uses assigned agent skills when available", () => {
const agent = {
id: "agent-001",
name: "Test Agent",
role: "executor" as const,
state: "idle" as const,
metadata: { skills: ["triage", "executor"] },
} as unknown as Agent;
const result = buildSessionSkillContextSync(agent, "executor", projectRootDir);
expect(result.skillSource).toBe("assigned-agent");
expect(result.resolvedSkillNames).toEqual(["triage", "executor"]);
expect(result.skillSelectionContext).toEqual({
projectRootDir,
requestedSkillNames: ["triage", "executor"],
sessionPurpose: "executor",
});
});
it("uses object-style agent skills", () => {
const agent: Agent = {
id: "agent-001",
name: "Test Agent",
role: "executor",
state: "idle",
metadata: { skills: [{ name: "triage" }, { name: "executor" }] },
} as unknown as Agent;
const result = buildSessionSkillContextSync(agent, "executor", projectRootDir);
expect(result.skillSource).toBe("assigned-agent");
expect(result.resolvedSkillNames).toEqual(["triage", "executor"]);
});
it("correctly extracts skills from a cached agent object", () => {
const agent = {
id: "agent-001",
name: "Test Agent",
role: "executor" as const,
state: "idle" as const,
metadata: { skills: ["triage", "executor"] },
} as unknown as Agent;
const result = buildSessionSkillContextSync(agent, "executor", projectRootDir);
expect(result.skillSource).toBe("assigned-agent");
expect(result.resolvedSkillNames).toEqual(["triage", "executor"]);
expect(result.skillSelectionContext).toEqual({
projectRootDir,
requestedSkillNames: ["triage", "executor"],
sessionPurpose: "executor",
});
});
it("falls back to role when agent has empty skills", () => {
const agent: Agent = {
id: "agent-001",
name: "Test Agent",
role: "executor",
state: "idle",
metadata: { skills: [] },
} as unknown as Agent;
const result = buildSessionSkillContextSync(agent, "executor", projectRootDir);
expect(result.skillSource).toBe("role-fallback");
expect(result.resolvedSkillNames).toEqual(["fusion"]);
});
it("falls back to role when agent has no metadata", () => {
const agent: Agent = {
id: "agent-001",
name: "Test Agent",
role: "executor",
state: "idle",
metadata: {},
} as unknown as Agent;
const result = buildSessionSkillContextSync(agent, "executor", projectRootDir);
expect(result.skillSource).toBe("role-fallback");
expect(result.resolvedSkillNames).toEqual(["fusion"]);
});
it("falls back to role when agent has no metadata.skills", () => {
const agent: Agent = {
id: "agent-001",
name: "Test Agent",
role: "executor",
state: "idle",
} as unknown as Agent;
const result = buildSessionSkillContextSync(agent, "executor", projectRootDir);
expect(result.skillSource).toBe("role-fallback");
expect(result.resolvedSkillNames).toEqual(["fusion"]);
});
});
describe("role fallback skills", () => {
it("returns fusion role fallback for triage purpose", () => {
const result = buildSessionSkillContextSync(null, "triage", projectRootDir);
expect(result.skillSource).toBe("role-fallback");
expect(result.resolvedSkillNames).toEqual(["fusion"]);
});
it("returns fusion role fallback for executor purpose", () => {
const result = buildSessionSkillContextSync(null, "executor", projectRootDir);
expect(result.skillSource).toBe("role-fallback");
expect(result.resolvedSkillNames).toEqual(["fusion"]);
});
it("returns fusion role fallback for reviewer purpose", () => {
const result = buildSessionSkillContextSync(null, "reviewer", projectRootDir);
expect(result.skillSource).toBe("role-fallback");
expect(result.resolvedSkillNames).toEqual(["fusion"]);
});
it("returns fusion role fallback for merger purpose", () => {
const result = buildSessionSkillContextSync(null, "merger", projectRootDir);
expect(result.skillSource).toBe("role-fallback");
expect(result.resolvedSkillNames).toEqual(["fusion"]);
});
it("returns no skills for heartbeat purpose (no role fallback)", () => {
const result = buildSessionSkillContextSync(null, "heartbeat", projectRootDir);
expect(result.skillSource).toBe("none");
expect(result.resolvedSkillNames).toEqual([]);
expect(result.skillSelectionContext).toBeUndefined();
});
it("uses agent skills over role fallback", () => {
const agent: Agent = {
id: "agent-001",
name: "Test Agent",
role: "executor",
state: "idle",
metadata: { skills: ["custom-skill-1", "custom-skill-2"] },
} as unknown as Agent;
const result = buildSessionSkillContextSync(agent, "executor", projectRootDir);
expect(result.skillSource).toBe("assigned-agent");
expect(result.resolvedSkillNames).toEqual(["custom-skill-1", "custom-skill-2"]);
});
});
describe("no skills available", () => {
it("returns undefined context when no skills and no fallback", () => {
const result = buildSessionSkillContextSync(null, "heartbeat", projectRootDir);
expect(result.skillSelectionContext).toBeUndefined();
expect(result.resolvedSkillNames).toEqual([]);
});
});
it("merges plugin skills in sync path", () => {
const pluginRunner = {
getPluginSkills: vi.fn().mockReturnValue([
{ pluginId: "plugin-a", skill: { name: "plugin-skill" } },
]),
} as unknown as PluginRunner;
const result = buildSessionSkillContextSync(null, "executor", projectRootDir, pluginRunner);
expect(result.resolvedSkillNames).toEqual(["fusion", "plugin-skill"]);
});
it("honors per-project plugin skill toggles in sync path", async () => {
const projectRoot = await createProjectWithSettings({
packages: [
{ source: "plugin:plugin-a", skills: ["+skills/opt-in/SKILL.md"] },
{ source: "plugin:plugin-b", skills: ["-skills/opt-out/SKILL.md"] },
],
});
const pluginRunner = pluginRunnerWithSkills([
{ pluginId: "plugin-a", skill: { name: "opt-in", enabled: false } },
{ pluginId: "plugin-b", skill: { name: "opt-out", enabled: true } },
{ pluginId: "plugin-c", skill: { name: "default-on" } },
]);
const result = buildSessionSkillContextSync(null, "executor", projectRoot, pluginRunner);
expect(result.resolvedSkillNames).toEqual(["fusion", "opt-in", "default-on"]);
});
it("threads enabled plugin skill body directories into sync session context", async () => {
const { pluginRoot, skillDir } = await createPluginSkillRoot("plugin-skill", "# Plugin body\n");
const pluginRunner = pluginRunnerWithSkills([
{ pluginId: "plugin-a", pluginRoot, skill: { name: "plugin-skill" } },
]);
const result = buildSessionSkillContextSync(null, "executor", projectRootDir, pluginRunner);
expect(result.resolvedSkillNames).toEqual(["fusion", "plugin-skill"]);
expect(result.additionalSkillPaths).toEqual([skillDir, dirname(skillDir)]);
});
it("keeps legacy behavior in sync path when pluginRunner is omitted", () => {
const result = buildSessionSkillContextSync(null, "executor", projectRootDir);
expect(result.resolvedSkillNames).toEqual(["fusion"]);
expect(result.additionalSkillPaths).toEqual([]);
});
});
describe("collectPluginSkillNames", () => {
it("returns empty arrays when pluginRunner is undefined", () => {
expect(collectPluginSkillNames(undefined)).toEqual({ names: [], pluginIds: [], additionalSkillPaths: [] });
});
it("returns empty arrays when no plugin skills are contributed", () => {
const pluginRunner = { getPluginSkills: vi.fn().mockReturnValue([]) } as unknown as PluginRunner;
expect(collectPluginSkillNames(pluginRunner)).toEqual({ names: [], pluginIds: [], additionalSkillPaths: [] });
});
it("returns enabled plugin skill names and dedupes by first occurrence", () => {
const pluginRunner = {
getPluginSkills: vi.fn().mockReturnValue([
{ pluginId: "plugin-a", skill: { name: "alpha" } },
{ pluginId: "plugin-b", skill: { name: "beta" } },
{ pluginId: "plugin-c", skill: { name: "alpha" } },
]),
} as unknown as PluginRunner;
expect(collectPluginSkillNames(pluginRunner)).toEqual({
names: ["alpha", "beta"],
pluginIds: ["plugin-a", "plugin-b"],
additionalSkillPaths: [],
});
});
it("filters out disabled skills", () => {
const pluginRunner = {
getPluginSkills: vi.fn().mockReturnValue([
{ pluginId: "plugin-a", skill: { name: "alpha", enabled: false } },
{ pluginId: "plugin-b", skill: { name: "beta", enabled: true } },
]),
} as unknown as PluginRunner;
expect(collectPluginSkillNames(pluginRunner)).toEqual({
names: ["beta"],
pluginIds: ["plugin-b"],
additionalSkillPaths: [],
});
});
it("uses project settings to enable a statically disabled plugin skill", async () => {
const projectRoot = await createProjectWithSettings({
packages: [{ source: "plugin:plugin-a", skills: ["+skills/alpha/SKILL.md"] }],
});
const pluginRunner = pluginRunnerWithSkills([
{ pluginId: "plugin-a", skill: { name: "alpha", enabled: false } },
]);
expect(collectPluginSkillNames(pluginRunner, projectRoot)).toEqual({
names: ["alpha"],
pluginIds: ["plugin-a"],
additionalSkillPaths: [],
});
});
it("uses project settings to disable a statically enabled plugin skill", async () => {
const projectRoot = await createProjectWithSettings({
packages: [{ source: "plugin:plugin-a", skills: ["-skills/alpha/SKILL.md"] }],
});
const pluginRunner = pluginRunnerWithSkills([
{ pluginId: "plugin-a", skill: { name: "alpha", enabled: true } },
{ pluginId: "plugin-b", skill: { name: "beta" } },
]);
expect(collectPluginSkillNames(pluginRunner, projectRoot)).toEqual({
names: ["beta"],
pluginIds: ["plugin-b"],
additionalSkillPaths: [],
});
});
it("uses package-scoped custom skillFiles paths to enable statically disabled plugin skills", async () => {
const relativePath = "skills/data/ef-core/SKILL.md";
const { pluginRoot, skillDir } = await createPluginSkillRootAt(relativePath);
const projectRoot = await createProjectWithSettings({
packages: [{ source: "plugin:plugin-a", skills: [`+${relativePath}`] }],
});
const pluginRunner = pluginRunnerWithSkills([
{ pluginId: "plugin-a", pluginRoot, skill: { name: "ef-core", enabled: false, skillFiles: [relativePath] } },
]);
expect(collectPluginSkillNames(pluginRunner, projectRoot)).toEqual({
names: ["ef-core"],
pluginIds: ["plugin-a"],
additionalSkillPaths: [skillDir, dirname(skillDir)],
});
});
it("uses package-scoped custom skillFiles paths to disable statically enabled plugin skills", async () => {
const relativePath = "skills/data/ef-core/SKILL.md";
const { pluginRoot } = await createPluginSkillRootAt(relativePath);
const projectRoot = await createProjectWithSettings({
packages: [{ source: "plugin:plugin-a", skills: [`-${relativePath}`] }],
});
const pluginRunner = pluginRunnerWithSkills([
{ pluginId: "plugin-a", pluginRoot, skill: { name: "ef-core", enabled: true, skillFiles: [relativePath] } },
{ pluginId: "plugin-b", skill: { name: "beta" } },
]);
expect(collectPluginSkillNames(pluginRunner, projectRoot)).toEqual({
names: ["beta"],
pluginIds: ["plugin-b"],
additionalSkillPaths: [],
});
});
it("falls back to static defaults when project settings omit a plugin skill", async () => {
const projectRoot = await createProjectWithSettings({ skills: [] });
const pluginRunner = pluginRunnerWithSkills([
{ pluginId: "plugin-a", skill: { name: "alpha", enabled: false } },
{ pluginId: "plugin-b", skill: { name: "beta" } },
]);
expect(collectPluginSkillNames(pluginRunner, projectRoot)).toEqual({
names: ["beta"],
pluginIds: ["plugin-b"],
additionalSkillPaths: [],
});
});
it("honors top-level skill toggle entries for plugin skills", async () => {
const projectRoot = await createProjectWithSettings({
skills: ["+skills/alpha/SKILL.md"],
});
const pluginRunner = pluginRunnerWithSkills([
{ pluginId: "plugin-a", skill: { name: "alpha", enabled: false } },
]);
expect(collectPluginSkillNames(pluginRunner, projectRoot)).toEqual({
names: ["alpha"],
pluginIds: ["plugin-a"],
additionalSkillPaths: [],
});
});
it("resolves settings from the real project root when called with a worktree path", async () => {
const projectRoot = await createProjectWithSettings({
packages: [{ source: "plugin:plugin-a", skills: ["+skills/alpha/SKILL.md"] }],
});
const worktreeRoot = join(projectRoot, ".worktrees", "branch-a");
await mkdir(worktreeRoot, { recursive: true });
const pluginRunner = pluginRunnerWithSkills([
{ pluginId: "plugin-a", skill: { name: "alpha", enabled: false } },
]);
expect(collectPluginSkillNames(pluginRunner, worktreeRoot)).toEqual({
names: ["alpha"],
pluginIds: ["plugin-a"],
additionalSkillPaths: [],
});
});
it("adds enabled plugin skill body directories and dedupes duplicate directories", async () => {
const { pluginRoot, skillDir } = await createPluginSkillRoot("alpha");
const pluginRunner = pluginRunnerWithSkills([
{ pluginId: "plugin-a", pluginRoot, skill: { name: "alpha" } },
{ pluginId: "plugin-b", pluginRoot, skill: { name: "alpha" } },
{ pluginId: "plugin-c", pluginRoot, skill: { name: "beta", skillFiles: ["skills/alpha/SKILL.md"] } },
]);
expect(collectPluginSkillNames(pluginRunner)).toEqual({
names: ["alpha", "beta"],
pluginIds: ["plugin-a", "plugin-c"],
additionalSkillPaths: [skillDir, dirname(skillDir)],
});
});
it("does not add body directories for disabled skills or missing pluginRoot", async () => {
const { pluginRoot } = await createPluginSkillRoot("alpha");
const pluginRunner = pluginRunnerWithSkills([
{ pluginId: "plugin-a", pluginRoot, skill: { name: "alpha", enabled: false } },
{ pluginId: "plugin-b", skill: { name: "beta" } },
]);
expect(collectPluginSkillNames(pluginRunner)).toEqual({
names: ["beta"],
pluginIds: ["plugin-b"],
additionalSkillPaths: [],
});
});
});
describe("buildSessionSkillContext", () => {
const projectRootDir = "/test/project";
it("uses assigned agent skills when available", async () => {
const mockAgent: Agent = {
id: "agent-001",
name: "Test Agent",
role: "executor",
state: "idle",
metadata: { skills: ["triage", "executor"] },
} as unknown as Agent;
const mockAgentStore = {
getAgent: vi.fn().mockResolvedValue(mockAgent),
} as unknown as AgentStore;
const result = await buildSessionSkillContext({
agentStore: mockAgentStore,
task: { assignedAgentId: "agent-001" },
sessionPurpose: "executor",
projectRootDir,
});
expect(result.skillSource).toBe("assigned-agent");
expect(result.resolvedSkillNames).toEqual(["triage", "executor"]);
expect(mockAgentStore.getAgent).toHaveBeenCalledWith("agent-001");
});
it("resolves assigned-agent skills when task.assignedAgentId points to an agent with metadata.skills", async () => {
const mockAgent: Agent = {
id: "agent-001",
name: "Test Agent",
role: "executor",
state: "idle",
metadata: { skills: ["review", "custom-skill"] },
} as unknown as Agent;
const mockAgentStore = {
getAgent: vi.fn().mockResolvedValue(mockAgent),
} as unknown as AgentStore;
const result = await buildSessionSkillContext({
agentStore: mockAgentStore,
task: { assignedAgentId: "agent-001" },
sessionPurpose: "executor",
projectRootDir,
});
expect(result.skillSource).toBe("assigned-agent");
expect(result.resolvedSkillNames).toEqual(["review", "custom-skill"]);
expect(result.skillSelectionContext?.requestedSkillNames).toEqual(["review", "custom-skill"]);
expect(mockAgentStore.getAgent).toHaveBeenCalledWith("agent-001");
});
it("falls back to role fallback skills when assigned agent has no skills", async () => {
const mockAgent: Agent = {
id: "agent-001",
name: "Test Agent",
role: "executor",
state: "idle",
metadata: { skills: [] },
} as unknown as Agent;
const mockAgentStore = {
getAgent: vi.fn().mockResolvedValue(mockAgent),
} as unknown as AgentStore;
const result = await buildSessionSkillContext({
agentStore: mockAgentStore,
task: { assignedAgentId: "agent-001" },
sessionPurpose: "executor",
projectRootDir,
});
expect(result.skillSource).toBe("role-fallback");
expect(result.resolvedSkillNames).toEqual(["fusion"]);
});
it("falls back to role when no assignedAgentId", async () => {
const mockAgentStore = {
getAgent: vi.fn(),
} as unknown as AgentStore;
const result = await buildSessionSkillContext({
agentStore: mockAgentStore,
task: {},
sessionPurpose: "executor",
projectRootDir,
});
expect(result.skillSource).toBe("role-fallback");
expect(result.resolvedSkillNames).toEqual(["fusion"]);
expect(mockAgentStore.getAgent).not.toHaveBeenCalled();
});
it("falls back to role when assigned agent not found", async () => {
const mockAgentStore = {
getAgent: vi.fn().mockResolvedValue(null),
} as unknown as AgentStore;
const result = await buildSessionSkillContext({
agentStore: mockAgentStore,
task: { assignedAgentId: "nonexistent" },
sessionPurpose: "triage",
projectRootDir,
});
expect(result.skillSource).toBe("role-fallback");
expect(result.resolvedSkillNames).toEqual(["fusion"]);
});
it("falls back to role when agent lookup throws", async () => {
const mockAgentStore = {
getAgent: vi.fn().mockRejectedValue(new Error("DB error")),
} as unknown as AgentStore;
const result = await buildSessionSkillContext({
agentStore: mockAgentStore,
task: { assignedAgentId: "agent-001" },
sessionPurpose: "reviewer",
projectRootDir,
});
expect(result.skillSource).toBe("role-fallback");
expect(result.resolvedSkillNames).toEqual(["fusion"]);
});
it("uses heartbeat with no skills when no assigned agent", async () => {
const mockAgentStore = {
getAgent: vi.fn(),
} as unknown as AgentStore;
const result = await buildSessionSkillContext({
agentStore: mockAgentStore,
task: {},
sessionPurpose: "heartbeat",
projectRootDir,
});
expect(result.skillSource).toBe("none");
expect(result.resolvedSkillNames).toEqual([]);
expect(result.skillSelectionContext).toBeUndefined();
});
it("appends plugin skills to requestedSkillNames", async () => {
const mockAgentStore = { getAgent: vi.fn().mockResolvedValue(null) } as unknown as AgentStore;
const pluginRunner = {
getPluginSkills: vi.fn().mockReturnValue([
{ pluginId: "plugin-a", skill: { name: "plugin-skill" } },
]),
} as unknown as PluginRunner;
const result = await buildSessionSkillContext({
agentStore: mockAgentStore,
task: {},
sessionPurpose: "executor",
projectRootDir,
pluginRunner,
});
expect(result.resolvedSkillNames).toEqual(["fusion", "plugin-skill"]);
expect(result.skillSelectionContext?.requestedSkillNames).toEqual(["fusion", "plugin-skill"]);
});
it("threads enabled plugin skill body directories into async session context", async () => {
const { pluginRoot, skillDir } = await createPluginSkillRoot("plugin-skill", "# Plugin body\n");
const mockAgentStore = { getAgent: vi.fn().mockResolvedValue(null) } as unknown as AgentStore;
const pluginRunner = pluginRunnerWithSkills([
{ pluginId: "plugin-a", pluginRoot, skill: { name: "plugin-skill" } },
]);
const result = await buildSessionSkillContext({
agentStore: mockAgentStore,
task: {},
sessionPurpose: "executor",
projectRootDir,
pluginRunner,
});
expect(result.resolvedSkillNames).toEqual(["fusion", "plugin-skill"]);
expect(result.additionalSkillPaths).toEqual([skillDir, dirname(skillDir)]);
});
it("honors per-project plugin skill toggles in async path", async () => {
const projectRoot = await createProjectWithSettings({
packages: [
{ source: "plugin:plugin-a", skills: ["+skills/opt-in/SKILL.md"] },
{ source: "plugin:plugin-b", skills: ["-skills/opt-out/SKILL.md"] },
],
});
const mockAgentStore = { getAgent: vi.fn().mockResolvedValue(null) } as unknown as AgentStore;
const pluginRunner = pluginRunnerWithSkills([
{ pluginId: "plugin-a", skill: { name: "opt-in", enabled: false } },
{ pluginId: "plugin-b", skill: { name: "opt-out", enabled: true } },
{ pluginId: "plugin-c", skill: { name: "default-on" } },
]);
const result = await buildSessionSkillContext({
agentStore: mockAgentStore,
task: {},
sessionPurpose: "executor",
projectRootDir: projectRoot,
pluginRunner,
});
expect(result.resolvedSkillNames).toEqual(["fusion", "opt-in", "default-on"]);
expect(result.skillSelectionContext?.requestedSkillNames).toEqual(["fusion", "opt-in", "default-on"]);
});
it("deduplicates plugin skills against assigned-agent skills case-insensitively", async () => {
const mockAgent: Agent = {
id: "agent-001",
name: "Test Agent",
role: "executor",
state: "idle",
metadata: { skills: ["Fusion"] },
} as unknown as Agent;
const mockAgentStore = { getAgent: vi.fn().mockResolvedValue(mockAgent) } as unknown as AgentStore;
const pluginRunner = {
getPluginSkills: vi.fn().mockReturnValue([
{ pluginId: "plugin-a", skill: { name: "fusion" } },
{ pluginId: "plugin-b", skill: { name: "plugin-skill" } },
]),
} as unknown as PluginRunner;
const result = await buildSessionSkillContext({
agentStore: mockAgentStore,
task: { assignedAgentId: "agent-001" },
sessionPurpose: "executor",
projectRootDir,
pluginRunner,
});
expect(result.resolvedSkillNames).toEqual(["Fusion", "plugin-skill"]);
});
it("creates heartbeat skill context from plugin skills when no agent skills exist", async () => {
const mockAgentStore = { getAgent: vi.fn() } as unknown as AgentStore;
const pluginRunner = {
getPluginSkills: vi.fn().mockReturnValue([
{ pluginId: "plugin-a", skill: { name: "plugin-skill" } },
]),
} as unknown as PluginRunner;
const result = await buildSessionSkillContext({
agentStore: mockAgentStore,
task: {},
sessionPurpose: "heartbeat",
projectRootDir,
pluginRunner,
});
expect(result.skillSource).toBe("role-fallback");
expect(result.skillSelectionContext?.requestedSkillNames).toEqual(["plugin-skill"]);
});
});
describe("SKILL_DIAGNOSTIC_MESSAGES", () => {
it("provides missing skill message template", () => {
const msg = SKILL_DIAGNOSTIC_MESSAGES.missing("custom-skill");
expect(msg).toBe('skill selection: requested skill "custom-skill" not found in discovered skills');
});
it("provides filtered skill message template", () => {
const msg = SKILL_DIAGNOSTIC_MESSAGES.filtered("custom-skill");
expect(msg).toBe('skill selection: requested skill "custom-skill" filtered out by execution-enabled settings');
});
it("provides assigned agent message template", () => {
const msg = SKILL_DIAGNOSTIC_MESSAGES.assignedAgentSkills(3, "agent-001");
expect(msg).toBe("Using skills from assigned agent agent-001 (3 skills)");
});
it("provides role fallback message template", () => {
const msg = SKILL_DIAGNOSTIC_MESSAGES.roleFallbackSkills("triage", ["fusion"]);
expect(msg).toBe("Using role fallback skills for triage: [fusion]");
});
it("provides no skills available message template", () => {
const msg = SKILL_DIAGNOSTIC_MESSAGES.noSkillsAvailable("heartbeat");
expect(msg).toBe("No skills available for heartbeat session (no assigned agent, no role fallback)");
});
});