Files
fusion/packages/engine/src/__tests__/plugin-skill-integration.test.ts
gsxdsm 9f5f981e33 FN-9114: expose enabled skills and enforce agent skill reads
Make project-enabled skills available across agent sessions while preserving explicit per-agent skills as observable read-first requirements.

- resolve all enabled discovered skills without using agent metadata as an availability filter
- carry forced skill intent through PI, plugin, chat, workflow, merger, and heartbeat session paths
- report resolved and unavailable forced skills in session summaries and diagnostics
- document the updated skill model and add release notes and regression coverage

Files changed:
 .changeset/fn-9114-forced-skills.md                |   7 +
 docs/PLUGIN_AUTHORING.md                           |   2 +-
 docs/agents.md                                     |  10 +-
 docs/diagnostics.md                                |   2 +-
 docs/settings-reference.md                         |   2 +-
 packages/core/src/__tests__/skill-settings.test.ts |   9 +
 .../dashboard/src/__tests__/chat-manager.test.ts   |  26 ++-
 packages/dashboard/src/chat.ts                     |  10 +
 .../engine/src/__tests__/agent-skills-flow.test.ts |  14 +-
 .../compound-engineering-skill-resolution.test.ts  |   2 +-
 .../engine/src/__tests__/heartbeat-skills.test.ts  |  41 ++++
 .../__tests__/hermes-runtime-integration.test.ts   |  63 +++++-
 .../engine/src/__tests__/merger-skills.test.ts     |  33 ++-
 packages/engine/src/__tests__/pi.test.ts           |  35 ++-
 .../__tests__/plugin-skill-body-delivery.test.ts   |   8 +-
 .../src/__tests__/plugin-skill-integration.test.ts |   3 +-
 .../src/__tests__/session-skill-context.test.ts    |  40 ++--
 .../engine/src/__tests__/skill-resolver.test.ts    |  38 +++-
 .../__tests__/step-execute-skill-loading.test.ts   |   6 +
 packages/engine/src/agents/agent-runtime.ts        |   2 +
 .../engine/src/agents/agent-session-helpers.ts     |  88 ++++++-
 .../src/cli-runtime/session-skill-context.ts       | 117 ++++------
 packages/engine/src/cli-runtime/skill-resolver.ts  | 252 +++++++++------------
 .../engine/src/executor/execute-workflow-step.ts   |  14 ++
 packages/engine/src/executor/run-implementation.ts |   5 +
 packages/engine/src/pi.ts                          |  28 ++-
 26 files changed, 576 insertions(+), 281 deletions(-)

Fusion-Task-Id: FN-9114

Fusion-Task-Lineage: fe63ebd1-f947-40d7-be7b-c13d7f1c35c9

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-15 21:57:22 -07:00

88 lines
3.2 KiB
TypeScript

import { describe, it, expect, vi } from "vitest";
import type { AgentStore } from "@fusion/core";
import type { PluginRunner } from "../plugins/plugin-runner.js";
import { buildSessionSkillContext } from "../cli-runtime/session-skill-context.js";
import { createSkillsOverrideFromSelection, resolveSessionSkills } from "../cli-runtime/skill-resolver.js";
import type { Skill } from "@earendil-works/pi-coding-agent";
describe("plugin skill integration", () => {
const projectRootDir = "/test/project";
it("merges enabled plugin skills, excludes disabled, and dedupes with agent skills", async () => {
const agentStore = {
getAgent: vi.fn().mockResolvedValue({
id: "agent-1",
metadata: { skills: ["fusion", "agent-only"] },
}),
} as unknown as AgentStore;
const pluginRunner = {
getPluginSkills: vi.fn().mockReturnValue([
{ pluginId: "plugin-a", skill: { name: "fusion", enabled: true } },
{ pluginId: "plugin-a", skill: { name: "plugin-enabled", enabled: true } },
{ pluginId: "plugin-b", skill: { name: "plugin-disabled", enabled: false } },
]),
} as unknown as PluginRunner;
const contextResult = await buildSessionSkillContext({
agentStore,
task: { assignedAgentId: "agent-1" },
sessionPurpose: "executor",
projectRootDir,
pluginRunner,
});
expect(contextResult.skillSelectionContext?.requestedSkillNames).toEqual([
"fusion",
"plugin-enabled",
]);
});
it("coexists with role fallback and flows through resolver pipeline", async () => {
const agentStore = {
getAgent: vi.fn().mockResolvedValue(null),
} as unknown as AgentStore;
const pluginRunner = {
getPluginSkills: vi.fn().mockReturnValue([
{ pluginId: "plugin-a", skill: { name: "plugin-skill", enabled: true } },
]),
} as unknown as PluginRunner;
const contextResult = await buildSessionSkillContext({
agentStore,
task: {},
sessionPurpose: "triage",
projectRootDir,
pluginRunner,
});
expect(contextResult.skillSelectionContext?.requestedSkillNames).toEqual(["fusion", "plugin-skill"]);
const resolved = resolveSessionSkills(contextResult.skillSelectionContext!);
const skillsOverride = createSkillsOverrideFromSelection(resolved, {
requestedSkillNames: contextResult.skillSelectionContext?.requestedSkillNames,
sessionPurpose: contextResult.skillSelectionContext?.sessionPurpose,
});
const makeSkill = (name: string, filePath: string): Skill => ({
name,
description: `${name} description`,
filePath,
baseDir: "/skills",
sourceInfo: {} as Skill["sourceInfo"],
disableModelInvocation: false,
});
const filtered = skillsOverride({
skills: [
makeSkill("fusion", "/skills/fusion/SKILL.md"),
makeSkill("plugin-skill", "/skills/plugin/SKILL.md"),
makeSkill("other", "/skills/other/SKILL.md"),
],
diagnostics: [],
});
expect(contextResult.skillSelectionContext?.requestedSkillNames).toEqual(["fusion", "plugin-skill"]);
expect(filtered.skills.map((skill) => skill.name)).toEqual(["fusion", "plugin-skill", "other"]);
});
});