Files
fusion/packages/cli/src/commands/__tests__/agent-import.test.ts
gsxdsm bce7dbd96f chore: consolidate test files into __tests__/ dirs and clean stray engine artifacts
- Move all co-located *.test.* files into sibling __tests__/ directories so the
  layout is consistent across packages (159 renames + content-rewrite moves).
  Updates relative imports, vi.mock specifiers, and __dirname/import.meta.url
  path resolutions where tests read fixtures from disk.
- Drop tracked tsc-emit alongside engine .ts sources (auth-storage/logger/
  skill-resolver/context-limit-detector/pi.{js,d.ts,*.map}). These were
  accidentally committed in a merge and the stale pi.js was masking a real
  test-mock vs source mismatch (tests imported "../pi.js" and vite preferred
  the stale build over pi.ts).
- Add packages/engine/.gitignore to block future src/*.{js,d.ts,map}.
- Refactor plugin pi-module seams (openclaw/paperclip/hermes) to ESM-import
  createFnAgent / promptWithFallback / describeModel from @fusion/engine
  instead of require()-ing packages/engine/src/pi.js. Adds @fusion/engine to
  the two plugin package.jsons that were missing it; exports describeModel
  from the engine public API.
- Fix engine test mocks now that they run against current pi.ts: add
  ModelRegistry.create static to mocks in pi.test.ts and pi-create-fn-agent
  .test.ts; switch three boundary-result toEqual assertions to toMatchObject
  so the new content/isError fields don't trip exact-match comparison.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 19:39:20 -07:00

672 lines
23 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { writeFileSync, mkdirSync, rmSync, existsSync, readFileSync } from "node:fs";
import { execSync } from "node:child_process";
import { join, resolve } from "node:path";
import { tmpdir } from "node:os";
import { AgentStore } from "@fusion/core";
import { runAgentImport } from "../agent-import.js";
function makeAgentManifest(options: {
name: string;
title?: string;
slug?: string;
reportsTo?: string;
skills?: string[];
body?: string;
}): string {
const lines = ["---", `name: ${options.name}`];
if (options.title) {
lines.push(`title: ${options.title}`);
}
if (options.slug) {
lines.push(`slug: ${options.slug}`);
}
if (options.reportsTo) {
lines.push(`reportsTo: ${options.reportsTo}`);
}
if (options.skills && options.skills.length > 0) {
lines.push("skills:");
for (const skill of options.skills) {
lines.push(` - ${skill}`);
}
}
lines.push("---", options.body ?? `${options.name} instructions`);
return lines.join("\n");
}
function createCompanyDirectory(basePath: string, agentName = "CEO"): string {
mkdirSync(basePath, { recursive: true });
writeFileSync(
join(basePath, "COMPANY.md"),
"---\nname: Example Company\nslug: example-company\n---\nCompany description",
);
const teamDir = join(basePath, "teams", "engineering");
mkdirSync(teamDir, { recursive: true });
writeFileSync(
join(teamDir, "TEAM.md"),
"---\nname: Engineering\nmanager: ../ceo/AGENTS.md\n---",
);
const agentDir = join(basePath, "agents", "ceo");
mkdirSync(agentDir, { recursive: true });
writeFileSync(
join(agentDir, "AGENTS.md"),
makeAgentManifest({
name: agentName,
title: "Chief Executive",
skills: ["review"],
body: "Lead the company",
}),
);
return basePath;
}
function createHierarchyCompanyDirectory(basePath: string): string {
mkdirSync(basePath, { recursive: true });
writeFileSync(
join(basePath, "COMPANY.md"),
"---\nname: Example Company\nslug: example-company\n---\nCompany description",
);
mkdirSync(join(basePath, "agents", "ceo"), { recursive: true });
writeFileSync(
join(basePath, "agents", "ceo", "AGENTS.md"),
makeAgentManifest({
name: "CEO",
slug: "ceo",
title: "Chief Executive",
body: "Lead the company",
}),
);
mkdirSync(join(basePath, "agents", "vp-eng"), { recursive: true });
writeFileSync(
join(basePath, "agents", "vp-eng", "AGENTS.md"),
makeAgentManifest({
name: "VP Engineering",
slug: "vp-eng",
reportsTo: "ceo",
body: "Lead engineering",
}),
);
mkdirSync(join(basePath, "agents", "staff-eng"), { recursive: true });
writeFileSync(
join(basePath, "agents", "staff-eng", "AGENTS.md"),
makeAgentManifest({
name: "Staff Engineer",
reportsTo: "../vp-eng/AGENTS.md",
body: "Build systems",
}),
);
return basePath;
}
function makeSkillManifest(options: {
name: string;
description?: string;
slug?: string;
version?: string;
license?: string;
authors?: string[];
tags?: string[];
instructionBody?: string;
}): string {
const lines = ["---"];
lines.push(`name: ${options.name}`);
if (options.description) lines.push(`description: ${options.description}`);
if (options.slug) lines.push(`slug: ${options.slug}`);
if (options.version) lines.push(`version: ${options.version}`);
if (options.license) lines.push(`license: ${options.license}`);
if (options.authors && options.authors.length > 0) {
lines.push("authors:");
for (const author of options.authors) {
lines.push(` - ${author}`);
}
}
if (options.tags && options.tags.length > 0) {
lines.push("tags:");
for (const tag of options.tags) {
lines.push(` - ${tag}`);
}
}
lines.push("---");
if (options.instructionBody) {
lines.push(options.instructionBody);
}
return lines.join("\n");
}
function createCompanyDirectoryWithSkills(basePath: string, skills: Array<{
name: string;
description?: string;
instructionBody?: string;
}>): string {
// Create base company structure
mkdirSync(basePath, { recursive: true });
writeFileSync(
join(basePath, "COMPANY.md"),
"---\nname: Example Company\nslug: example-company\n---\nCompany description",
);
const agentDir = join(basePath, "agents", "ceo");
mkdirSync(agentDir, { recursive: true });
writeFileSync(
join(agentDir, "AGENTS.md"),
makeAgentManifest({
name: "CEO",
title: "Chief Executive",
body: "Lead the company",
}),
);
// Create skills
const skillsDir = join(basePath, "skills");
for (const skill of skills) {
const skillDir = join(skillsDir, skill.name.toLowerCase().replace(/\s+/g, "-"));
mkdirSync(skillDir, { recursive: true });
writeFileSync(
join(skillDir, "SKILL.md"),
makeSkillManifest({
name: skill.name,
description: skill.description,
instructionBody: skill.instructionBody,
}),
);
}
return basePath;
}
describe("agent-import", () => {
const tmpDir = join(tmpdir(), `fn-agent-import-test-${process.pid}`);
let createAgentMock: ReturnType<typeof vi.fn>;
let listAgentsMock: ReturnType<typeof vi.fn>;
let initMock: ReturnType<typeof vi.fn>;
beforeEach(() => {
mkdirSync(tmpDir, { recursive: true });
createAgentMock = vi.fn().mockImplementation(async (input: any) => ({
id: `agent-${String(input.name).toLowerCase().replace(/[^a-z0-9]+/g, "-")}`,
...input,
}));
listAgentsMock = vi.fn().mockResolvedValue([]);
initMock = vi.fn().mockResolvedValue(undefined);
vi.spyOn(AgentStore.prototype, "init").mockImplementation(initMock);
vi.spyOn(AgentStore.prototype, "listAgents").mockImplementation(listAgentsMock);
vi.spyOn(AgentStore.prototype, "createAgent").mockImplementation(createAgentMock);
});
afterEach(() => {
vi.restoreAllMocks();
try {
rmSync(tmpDir, { recursive: true, force: true });
} catch {
// Ignore cleanup errors
}
});
it("reports error on invalid source path", async () => {
const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => {
throw new Error("process.exit");
});
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
await expect(runAgentImport(join(tmpDir, "missing"))).rejects.toThrow("process.exit");
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining("Path not found"));
exitSpy.mockRestore();
errorSpy.mockRestore();
});
it("reports parse error on malformed AGENTS.md", async () => {
const manifestPath = join(tmpDir, "AGENTS.md");
writeFileSync(manifestPath, "name: missing frontmatter delimiters");
const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => {
throw new Error("process.exit");
});
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
await expect(runAgentImport(manifestPath)).rejects.toThrow("process.exit");
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining("Parse error"));
exitSpy.mockRestore();
errorSpy.mockRestore();
});
it("handles empty directory gracefully", async () => {
const emptyDir = join(tmpDir, "empty-company");
mkdirSync(emptyDir, { recursive: true });
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
await runAgentImport(emptyDir);
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("No agents found"));
logSpy.mockRestore();
});
it("imports agents from an Agent Companies directory", async () => {
const companyDir = createCompanyDirectory(join(tmpDir, "company-dir"));
await runAgentImport(companyDir);
expect(createAgentMock).toHaveBeenCalledTimes(1);
expect(createAgentMock).toHaveBeenCalledWith(
expect.objectContaining({ name: "CEO", role: "custom", title: "Chief Executive" }),
);
});
it("resolves imported manager hierarchy to created Fusion agent ids", async () => {
const companyDir = createHierarchyCompanyDirectory(join(tmpDir, "company-hierarchy"));
createAgentMock
.mockResolvedValueOnce({ id: "agent-ceo", name: "CEO" })
.mockResolvedValueOnce({ id: "agent-vp-eng", name: "VP Engineering" })
.mockResolvedValueOnce({ id: "agent-staff-eng", name: "Staff Engineer" });
await runAgentImport(companyDir);
expect(createAgentMock).toHaveBeenNthCalledWith(1, expect.objectContaining({
name: "CEO",
role: "custom",
}));
expect(createAgentMock).toHaveBeenNthCalledWith(2, expect.objectContaining({
name: "VP Engineering",
role: "custom",
reportsTo: "agent-ceo",
}));
expect(createAgentMock).toHaveBeenNthCalledWith(3, expect.objectContaining({
name: "Staff Engineer",
role: "custom",
reportsTo: "agent-vp-eng",
}));
});
it("resolves skipped existing managers before importing their reports", async () => {
const companyDir = createHierarchyCompanyDirectory(join(tmpDir, "company-existing-manager"));
listAgentsMock.mockResolvedValue([
{
id: "agent-ceo-existing",
name: "CEO",
role: "custom",
metadata: { agentCompaniesSlug: "ceo" },
},
]);
await runAgentImport(companyDir, { skipExisting: true });
expect(createAgentMock).toHaveBeenCalledTimes(2);
expect(createAgentMock).toHaveBeenNthCalledWith(1, expect.objectContaining({
name: "VP Engineering",
reportsTo: "agent-ceo-existing",
}));
expect(createAgentMock).toHaveBeenNthCalledWith(2, expect.objectContaining({
name: "Staff Engineer",
reportsTo: "agent-vp-engineering",
}));
});
it("imports agents from a single AGENTS.md file", async () => {
const manifestPath = join(tmpDir, "AGENTS.md");
writeFileSync(
manifestPath,
makeAgentManifest({
name: "Solo Agent",
title: "Single File Agent",
skills: ["review"],
}),
);
await runAgentImport(manifestPath);
expect(createAgentMock).toHaveBeenCalledTimes(1);
expect(createAgentMock).toHaveBeenCalledWith(
expect.objectContaining({ name: "Solo Agent", role: "custom" }),
);
});
it("imports agents from a .tar.gz archive", async () => {
const companyDir = createCompanyDirectory(join(tmpDir, "company-archive-src"), "Archive CEO");
const archivePath = join(tmpDir, "company.tar.gz");
execSync(`tar czf ${JSON.stringify(archivePath)} -C ${JSON.stringify(companyDir)} .`);
await runAgentImport(archivePath);
expect(createAgentMock).toHaveBeenCalledTimes(1);
expect(createAgentMock).toHaveBeenCalledWith(
expect.objectContaining({ name: "Archive CEO", role: "custom" }),
);
});
it("supports dry-run mode", async () => {
const companyDir = createCompanyDirectory(join(tmpDir, "company-dry-run"));
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
await runAgentImport(companyDir, { dryRun: true });
expect(createAgentMock).not.toHaveBeenCalled();
const output = logSpy.mock.calls.flat().join(" ");
expect(output).toContain("[DRY RUN]");
expect(output).toContain("Agents: 1");
expect(output).toContain("Teams: 1");
logSpy.mockRestore();
});
it("supports skip-existing", async () => {
const companyDir = createCompanyDirectory(join(tmpDir, "company-skip"));
listAgentsMock.mockResolvedValue([{ id: "agent-1", name: "CEO", role: "custom" }]);
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
await runAgentImport(companyDir, { skipExisting: true });
expect(createAgentMock).not.toHaveBeenCalled();
const output = logSpy.mock.calls.flat().join(" ");
expect(output).toContain("Skipped: 1");
logSpy.mockRestore();
});
it("reports unsupported file formats", async () => {
const unsupportedPath = join(tmpDir, "manifest.json");
writeFileSync(unsupportedPath, JSON.stringify({ name: "Not a manifest" }));
const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => {
throw new Error("process.exit");
});
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
await expect(runAgentImport(unsupportedPath)).rejects.toThrow("process.exit");
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining("Unsupported format"));
exitSpy.mockRestore();
errorSpy.mockRestore();
});
describe("skill import", () => {
const projectDir = join(tmpDir, "test-project");
const originalCwd = process.cwd();
beforeEach(() => {
mkdirSync(projectDir, { recursive: true });
// Create .fusion directory with fusion.db to make it detectable as a project
mkdirSync(join(projectDir, ".fusion"), { recursive: true });
writeFileSync(join(projectDir, ".fusion", "fusion.db"), "");
// Change to project directory so project auto-detection works
process.chdir(projectDir);
});
afterEach(() => {
process.chdir(originalCwd);
});
it("imports skills from directory package to skills/imported directory", async () => {
const companyDir = createCompanyDirectoryWithSkills(
join(tmpDir, "company-with-skills"),
[
{ name: "Code Review", description: "Review code changes", instructionBody: "Review all PRs carefully" },
{ name: "Strategy", instructionBody: "Plan the roadmap" },
],
);
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
await runAgentImport(companyDir);
// Verify skill files were created
const skillDir = join(projectDir, "skills", "imported", "example-company");
expect(existsSync(join(skillDir, "code-review", "SKILL.md"))).toBe(true);
expect(existsSync(join(skillDir, "strategy", "SKILL.md"))).toBe(true);
// Verify output includes skill results
const output = logSpy.mock.calls.flat().join(" ");
expect(output).toContain("Skills:");
expect(output).toContain("2 imported");
logSpy.mockRestore();
});
it("generates skill markdown with required frontmatter keys", async () => {
const companyDir = createCompanyDirectoryWithSkills(
join(tmpDir, "company-frontmatter-test"),
[{ name: "Test Skill", instructionBody: "Test instructions" }],
);
await runAgentImport(companyDir);
const skillContent = readFileSync(
join(projectDir, "skills", "imported", "example-company", "test-skill", "SKILL.md"),
"utf-8",
);
// Check required frontmatter keys
expect(skillContent).toContain("name: Test Skill");
expect(skillContent).toContain("schema: agentcompanies/v1");
expect(skillContent).toContain("kind: skill");
// Check body
expect(skillContent).toContain("Test instructions");
});
it("includes optional frontmatter fields when present", async () => {
const companyDir = join(tmpDir, "company-optional-frontmatter");
mkdirSync(companyDir, { recursive: true });
writeFileSync(
join(companyDir, "COMPANY.md"),
"---\nname: Test Co\nslug: test-co\n---",
);
mkdirSync(join(companyDir, "agents", "test"), { recursive: true });
writeFileSync(
join(companyDir, "agents", "test", "AGENTS.md"),
makeAgentManifest({ name: "Test Agent" }),
);
const skillDir = join(companyDir, "skills", "my-skill");
mkdirSync(skillDir, { recursive: true });
writeFileSync(
join(skillDir, "SKILL.md"),
makeSkillManifest({
name: "My Skill",
slug: "custom-slug",
description: "A test skill",
version: "1.0.0",
license: "MIT",
authors: ["Author One", "Author Two"],
tags: ["testing", "example"],
instructionBody: "Do the thing",
}),
);
await runAgentImport(companyDir);
const skillContent = readFileSync(
join(projectDir, "skills", "imported", "test-co", "my-skill", "SKILL.md"),
"utf-8",
);
expect(skillContent).toContain("description: A test skill");
expect(skillContent).toContain("version: 1.0.0");
expect(skillContent).toContain("license: MIT");
expect(skillContent).toContain("authors:");
expect(skillContent).toContain("- Author One");
expect(skillContent).toContain("- Author Two");
expect(skillContent).toContain("tags:");
expect(skillContent).toContain("- testing");
expect(skillContent).toContain("- example");
});
it("uses fallback template for skill without instruction body", async () => {
const companyDir = join(tmpDir, "company-no-body");
mkdirSync(companyDir, { recursive: true });
writeFileSync(
join(companyDir, "COMPANY.md"),
"---\nname: Test Co\nslug: test-co\n---",
);
mkdirSync(join(companyDir, "agents", "test"), { recursive: true });
writeFileSync(
join(companyDir, "agents", "test", "AGENTS.md"),
makeAgentManifest({ name: "Test Agent" }),
);
const skillDir = join(companyDir, "skills", "bare-skill");
mkdirSync(skillDir, { recursive: true });
writeFileSync(
join(skillDir, "SKILL.md"),
"---\nname: Bare Skill\n---\n",
);
await runAgentImport(companyDir);
const skillContent = readFileSync(
join(projectDir, "skills", "imported", "test-co", "bare-skill", "SKILL.md"),
"utf-8",
);
expect(skillContent).toContain("# Bare Skill");
});
it("skips existing skill files and reports them", async () => {
const companyDir = createCompanyDirectoryWithSkills(
join(tmpDir, "company-existing-skill"),
[{ name: "Existing Skill", instructionBody: "Original content" }],
);
// Pre-create the skill file
const existingSkillDir = join(projectDir, "skills", "imported", "example-company", "existing-skill");
mkdirSync(existingSkillDir, { recursive: true });
writeFileSync(join(existingSkillDir, "SKILL.md"), "---\nname: Existing Skill\n---\nAlready exists");
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
await runAgentImport(companyDir);
const output = logSpy.mock.calls.flat().join(" ");
expect(output).toContain("1 skipped");
expect(output).toContain("Existing Skill");
// Verify file was not overwritten
const skillContent = readFileSync(join(existingSkillDir, "SKILL.md"), "utf-8");
expect(skillContent).toContain("Already exists");
logSpy.mockRestore();
});
it("does not write skill files in dry-run mode", async () => {
const companyDir = createCompanyDirectoryWithSkills(
join(tmpDir, "company-dry-run-skills"),
[{ name: "Dry Run Skill", instructionBody: "Should not be written" }],
);
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
await runAgentImport(companyDir, { dryRun: true });
// Verify skill file was NOT created
const skillPath = join(projectDir, "skills", "imported", "example-company", "dry-run-skill", "SKILL.md");
expect(existsSync(skillPath)).toBe(false);
// Verify output shows what would be imported
const output = logSpy.mock.calls.flat().join(" ");
expect(output).toContain("[DRY RUN]");
expect(output).toContain("1 imported");
logSpy.mockRestore();
});
it("does not import skills for single AGENTS.md file", async () => {
const manifestPath = join(tmpDir, "solo-agent-with-skill.md");
writeFileSync(
manifestPath,
makeAgentManifest({
name: "Solo Agent",
skills: ["some-skill"],
}),
);
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
await runAgentImport(manifestPath);
const output = logSpy.mock.calls.flat().join(" ");
// Should not have a Skills section for single file imports
expect(output).not.toContain("Skills:");
logSpy.mockRestore();
});
it("imports skills from tar.gz archive", async () => {
const companyDir = createCompanyDirectoryWithSkills(
join(tmpDir, "company-archive-skills"),
[{ name: "Archived Skill", instructionBody: "From archive" }],
);
const archivePath = join(tmpDir, "company-with-skills.tar.gz");
execSync(`tar czf ${JSON.stringify(archivePath)} -C ${JSON.stringify(companyDir)} .`);
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
await runAgentImport(archivePath);
// Verify skill file was created
const skillDir = join(projectDir, "skills", "imported", "example-company");
expect(existsSync(join(skillDir, "archived-skill", "SKILL.md"))).toBe(true);
const output = logSpy.mock.calls.flat().join(" ");
expect(output).toContain("Skills:");
expect(output).toContain("1 imported");
logSpy.mockRestore();
});
it("handles company without slug using fallback directory name", async () => {
const companyDir = join(tmpDir, "company-no-slug");
mkdirSync(companyDir, { recursive: true });
writeFileSync(
join(companyDir, "COMPANY.md"),
"---\nname: Company Without Slug\n---\nNo slug provided",
);
mkdirSync(join(companyDir, "agents", "test"), { recursive: true });
writeFileSync(
join(companyDir, "agents", "test", "AGENTS.md"),
makeAgentManifest({ name: "Test Agent" }),
);
const skillDir = join(companyDir, "skills", "no-slug-skill");
mkdirSync(skillDir, { recursive: true });
writeFileSync(
join(skillDir, "SKILL.md"),
makeSkillManifest({ name: "No Slug Skill" }),
);
await runAgentImport(companyDir);
// Should use "unknown-company" fallback
const skillDir2 = join(projectDir, "skills", "imported", "unknown-company", "no-slug-skill");
expect(existsSync(join(skillDir2, "SKILL.md"))).toBe(true);
});
it("uses company slug for directory naming", async () => {
const companyDir = join(tmpDir, "company-custom-slug");
mkdirSync(companyDir, { recursive: true });
writeFileSync(
join(companyDir, "COMPANY.md"),
"---\nname: Custom Name\nslug: my-custom-slug\n---",
);
mkdirSync(join(companyDir, "agents", "test"), { recursive: true });
writeFileSync(
join(companyDir, "agents", "test", "AGENTS.md"),
makeAgentManifest({ name: "Test Agent" }),
);
const skillDir = join(companyDir, "skills", "slugged-skill");
mkdirSync(skillDir, { recursive: true });
writeFileSync(
join(skillDir, "SKILL.md"),
makeSkillManifest({ name: "Slugged Skill" }),
);
await runAgentImport(companyDir);
// Should use the custom slug
const skillDir2 = join(projectDir, "skills", "imported", "my-custom-slug", "slugged-skill");
expect(existsSync(join(skillDir2, "SKILL.md"))).toBe(true);
});
});
});