Files
fusion/packages/dashboard/src/__tests__/routes-agent-import.test.ts
Fusion 62666882f6 feat(FN-3896): support companies.sh repo subpaths in company imports
- Add secure archive subPath handling to company package parsing, including traversal/absolute-path validation
- Parse GitHub website tree URLs from companies.sh metadata to derive preferred branch and nested subPath
- Wire dashboard company import flow to download the correct branch archive and parse nested manifests with clear API errors
- Add parser and route tests for subPath success/failure cases and include changeset for @runfusion/fusion

Fusion-Task-Id: FN-3896
2026-05-09 15:56:11 -07:00

1152 lines
39 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { EventEmitter } from "node:events";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { request } from "../test-request.js";
import { createServer } from "../server.js";
const mockInit = vi.fn().mockResolvedValue(undefined);
const mockListAgents = vi.fn().mockResolvedValue([]);
const mockCreateAgent = vi.fn();
const mockChatStoreInit = vi.fn().mockResolvedValue(undefined);
const mockParseCompanyDirectory = vi.fn();
const mockParseCompanyArchive = vi.fn();
const mockParseSingleAgentManifest = vi.fn();
const mockPrepareAgentCompaniesImport = vi.fn();
// Use vi.hoisted to ensure mocks are available when vi.mock runs
const { mockFsAccess, mockFsMkdir, mockFsWriteFile, mockExecFile, MockAgentCompaniesParseError } = vi.hoisted(() => {
class MockAgentCompaniesParseError extends Error {
constructor(message: string) {
super(message);
this.name = "AgentCompaniesParseError";
}
}
return {
mockFsAccess: vi.fn(),
mockFsMkdir: vi.fn(),
mockFsWriteFile: vi.fn(),
mockExecFile: vi.fn(),
MockAgentCompaniesParseError,
};
});
vi.mock("node:fs/promises", async (importOriginal) => {
const actual = await importOriginal() as Record<string, unknown>;
return {
...actual,
default: actual,
mkdtemp: actual.mkdtemp,
access: mockFsAccess,
stat: actual.stat,
mkdir: mockFsMkdir,
readdir: actual.readdir,
rm: actual.rm,
readFile: actual.readFile,
writeFile: mockFsWriteFile,
};
});
vi.mock("node:child_process", async (importOriginal) => {
const actual = await importOriginal() as Record<string, unknown>;
return {
...actual,
execFile: mockExecFile,
};
});
vi.mock("@fusion/core", () => {
return {
AgentStore: class MockAgentStore {
init = mockInit;
listAgents = mockListAgents;
createAgent = mockCreateAgent;
},
ChatStore: class MockChatStore {
init = mockChatStoreInit;
},
parseCompanyDirectory: (...args: unknown[]) => mockParseCompanyDirectory(...args),
parseCompanyArchive: (...args: unknown[]) => mockParseCompanyArchive(...args),
parseSingleAgentManifest: (...args: unknown[]) => mockParseSingleAgentManifest(...args),
prepareAgentCompaniesImport: (...args: unknown[]) => mockPrepareAgentCompaniesImport(...args),
AgentCompaniesParseError: MockAgentCompaniesParseError,
};
});
class MockStore extends EventEmitter {
getRootDir(): string {
return "/tmp/fn-1174-test";
}
getFusionDir(): string {
return "/tmp/fn-1174-test/.fusion";
}
getDatabase() {
return {
exec: vi.fn(),
prepare: vi.fn().mockReturnValue({
run: vi.fn().mockReturnValue({ changes: 0 }),
get: vi.fn(),
all: vi.fn().mockReturnValue([]),
}),
};
}
}
async function postImport(app: Parameters<typeof request>[0], body: unknown) {
return request(app, "POST", "/api/agents/import", JSON.stringify(body), {
"content-type": "application/json",
});
}
function createGitHubArchiveBuffer(_companySlug: string): Buffer {
return Buffer.from("mock-company-archive");
}
const originalFetch = globalThis.fetch;
describe("POST /api/agents/import", () => {
let store: MockStore;
let app: ReturnType<typeof createServer>;
let testDir: string;
beforeEach(async () => {
vi.clearAllMocks();
rmSync("/tmp/fn-1174-test", { recursive: true, force: true });
testDir = mkdtempSync(join(tmpdir(), "fn-agent-import-route-"));
mockInit.mockResolvedValue(undefined);
mockListAgents.mockResolvedValue([]);
mockCreateAgent.mockReset();
mockCreateAgent.mockImplementation(async (input: any) => ({ id: `agent-${input.name}`, ...input }));
mockExecFile.mockReset();
mockExecFile.mockImplementation((command: string, args: string[], _options: unknown, callback: (error: Error | null, stdout?: string, stderr?: string) => void) => {
if ((command === "tar" || command === "bsdtar") && Array.isArray(args)) {
const extractDirIndex = args.indexOf("-C");
const extractDir = extractDirIndex >= 0 ? args[extractDirIndex + 1] : undefined;
if (extractDir) {
mkdirSync(join(extractDir, "mock-company-main", "acme-ai"), { recursive: true });
}
}
callback(null, "", "");
return {} as any;
});
mockParseCompanyDirectory.mockReturnValue({
company: { name: "Directory Co", slug: "directory-co" },
agents: [{ name: "Dir Agent", skills: ["review"] }],
teams: [{ name: "Engineering" }],
projects: [],
tasks: [],
skills: [{ name: "review" }, { name: "strategy" }],
});
mockParseCompanyArchive.mockResolvedValue({
company: { name: "Archive Co", slug: "archive-co" },
agents: [{ name: "Archive Agent", skills: ["review"] }],
teams: [{ name: "Ops" }],
projects: [],
tasks: [],
skills: [{ name: "review" }, { name: "strategy" }],
});
mockParseSingleAgentManifest.mockReturnValue({
manifest: {
name: "YAML Agent",
title: "Chief Executive",
skills: ["review"],
instructionBody: "Instructions",
},
});
mockPrepareAgentCompaniesImport.mockReturnValue({
items: [{
manifestKey: "yaml-agent",
aliases: ["yaml-agent"],
index: 0,
input: { name: "YAML Agent", role: "custom", title: "Chief Executive", metadata: { skills: ["review"] } },
}],
result: {
created: ["YAML Agent"],
skipped: [],
errors: [],
},
});
store = new MockStore();
app = createServer(store as any);
});
afterEach(() => {
vi.restoreAllMocks();
globalThis.fetch = originalFetch;
rmSync(testDir, { recursive: true, force: true });
});
it("returns 400 when no supported input mode is provided", async () => {
const response = await postImport(app, {});
expect(response.status).toBe(400);
expect((response.body as any).error).toContain("Provide one of");
});
it("imports agents via { agents } mode", async () => {
const response = await postImport(app, {
agents: [{ name: "Test Agent", skills: ["executor"] }],
});
expect(response.status).toBe(200);
expect(mockPrepareAgentCompaniesImport).toHaveBeenCalledTimes(1);
const body = response.body as any;
expect(body.created).toHaveLength(1);
expect(body.created[0].name).toBe("YAML Agent");
expect(body.companyName).toBe("Unknown");
expect(body.companySlug).toBeUndefined();
expect(body.skillsCount).toBe(0);
});
it("imports agents via { source } directory mode", async () => {
const sourceDir = join(testDir, "company");
mkdirSync(join(sourceDir, "agents", "ceo"), { recursive: true });
writeFileSync(join(sourceDir, "agents", "ceo", "AGENTS.md"), "---\nname: CEO\n---\nLead");
const response = await postImport(app, { source: sourceDir });
expect(response.status).toBe(200);
expect(mockParseCompanyDirectory).toHaveBeenCalledWith(sourceDir);
const body = response.body as any;
expect(body.companyName).toBe("Directory Co");
expect(body.companySlug).toBe("directory-co");
expect(body.created).toHaveLength(1);
expect(body.skillsCount).toBe(2);
});
it("imports agents via { source } archive mode", async () => {
const archivePath = join(testDir, "company.tgz");
writeFileSync(archivePath, "archive");
const response = await postImport(app, { source: archivePath });
expect(response.status).toBe(200);
expect(mockParseCompanyArchive).toHaveBeenCalledWith(archivePath);
const body = response.body as any;
expect(body.companyName).toBe("Archive Co");
expect(body.companySlug).toBe("archive-co");
expect(body.skillsCount).toBe(2);
});
it("creates hierarchical agents with resolved parent ids", async () => {
const sourceDir = join(testDir, "hierarchy-company");
mkdirSync(join(sourceDir, "agents", "ceo"), { recursive: true });
writeFileSync(join(sourceDir, "agents", "ceo", "AGENTS.md"), "---\nname: CEO\n---\nLead");
mockPrepareAgentCompaniesImport.mockReturnValue({
items: [
{
manifestKey: "ceo",
aliases: ["ceo"],
index: 0,
input: { name: "CEO", role: "custom" },
},
{
manifestKey: "vp-eng",
aliases: ["vp-eng"],
index: 1,
input: { name: "VP Eng", role: "custom" },
reportsTo: { raw: "ceo", deferredManifestKey: "ceo" },
},
{
manifestKey: "staff-eng",
aliases: ["staff-eng"],
index: 2,
input: { name: "Staff Eng", role: "custom" },
reportsTo: { raw: "../vp-eng/AGENTS.md", deferredManifestKey: "vp-eng" },
},
],
result: {
created: ["CEO", "VP Eng", "Staff Eng"],
skipped: [],
errors: [],
},
});
const response = await postImport(app, { source: sourceDir });
expect(response.status).toBe(200);
expect(mockCreateAgent).toHaveBeenCalledTimes(3);
expect(mockCreateAgent.mock.calls[0]?.[0]).toEqual({ name: "CEO", role: "custom" });
expect(mockCreateAgent.mock.calls[1]?.[0]).toEqual({
name: "VP Eng",
role: "custom",
reportsTo: "agent-CEO",
});
expect(mockCreateAgent.mock.calls[2]?.[0]).toEqual({
name: "Staff Eng",
role: "custom",
reportsTo: "agent-VP Eng",
});
});
it("uses helper-resolved existing manager ids for partial imports", async () => {
mockListAgents.mockResolvedValue([{ id: "agent-ceo", name: "CEO" }]);
mockPrepareAgentCompaniesImport.mockReturnValue({
items: [
{
manifestKey: "vp-eng",
aliases: ["vp-eng"],
index: 0,
input: { name: "VP Eng", role: "custom" },
reportsTo: { raw: "../ceo/AGENTS.md", resolvedAgentId: "agent-ceo" },
},
],
result: {
created: ["VP Eng"],
skipped: ["CEO"],
errors: [],
},
});
const response = await postImport(app, {
manifest: "---\nname: VP Eng\nreportsTo: ../ceo/AGENTS.md\n---\nLead engineering",
skipExisting: true,
});
expect(response.status).toBe(200);
expect(mockCreateAgent).toHaveBeenCalledWith({
name: "VP Eng",
role: "custom",
reportsTo: "agent-ceo",
});
expect(mockPrepareAgentCompaniesImport).toHaveBeenCalledWith(
expect.any(Object),
expect.objectContaining({
skipExisting: ["CEO"],
existingAgents: [{ id: "agent-ceo", name: "CEO" }],
}),
);
});
it("rejects non-directory source paths", async () => {
const filePath = join(testDir, "manifest.md");
writeFileSync(filePath, "---\nname: Agent\n---");
const response = await postImport(app, { source: filePath });
expect(response.status).toBe(400);
expect((response.body as any).error).toContain("directory");
});
it("imports agents via { manifest } AGENTS.md mode", async () => {
const response = await postImport(app, {
manifest: "---\nname: YAML Agent\nskills:\n - review\n---\nInstructions",
});
expect(response.status).toBe(200);
expect(mockParseSingleAgentManifest).toHaveBeenCalled();
});
it("returns dry-run preview and does not create agents", async () => {
const response = await postImport(app, {
manifest: "---\nname: YAML Agent\n---\nInstructions",
dryRun: true,
});
expect(response.status).toBe(200);
const body = response.body as any;
expect(body.dryRun).toBe(true);
expect(body.created).toEqual(["YAML Agent"]);
expect(body.agents).toEqual([
expect.objectContaining({ name: "YAML Agent", role: "custom", title: "Chief Executive" }),
]);
expect(body.skills).toEqual([]);
expect(mockCreateAgent).not.toHaveBeenCalled();
});
it("includes manifest memory in dry-run preview", async () => {
const memory = "Capture operational constraints and open risks before each handoff.";
mockPrepareAgentCompaniesImport.mockReturnValue({
items: [{
manifestKey: "memory-preview-agent",
aliases: ["memory-preview-agent"],
index: 0,
input: { name: "Memory Preview Agent", role: "custom", memory },
}],
result: {
created: ["Memory Preview Agent"],
skipped: [],
errors: [],
},
});
const response = await postImport(app, {
manifest: "---\nname: Memory Preview Agent\nmemory: Capture operational constraints and open risks before each handoff.\n---\nInstructions",
dryRun: true,
});
expect(response.status).toBe(200);
const body = response.body as any;
expect(body.agents).toEqual([
expect.objectContaining({
name: "Memory Preview Agent",
memory,
}),
]);
expect(mockCreateAgent).not.toHaveBeenCalled();
});
it("preserves manifest memory on live import", async () => {
mockPrepareAgentCompaniesImport.mockReturnValue({
items: [{
manifestKey: "memory-agent",
aliases: ["memory-agent"],
index: 0,
input: {
name: "Memory Agent",
role: "custom",
memory: "Capture failed deployment patterns and mitigations.",
},
}],
result: {
created: ["Memory Agent"],
skipped: [],
errors: [],
},
});
const response = await postImport(app, {
manifest: "---\nname: Memory Agent\nmemory: Capture failed deployment patterns and mitigations.\n---\nInstructions",
});
expect(response.status).toBe(200);
expect(mockCreateAgent).toHaveBeenCalledWith(
expect.objectContaining({
name: "Memory Agent",
memory: "Capture failed deployment patterns and mitigations.",
}),
);
});
it("maps store-level duplicate errors to skipped results", async () => {
mockPrepareAgentCompaniesImport.mockReturnValue({
items: [{
manifestKey: "dup-agent",
aliases: ["dup-agent"],
index: 0,
input: { name: "Dup Agent", role: "custom" },
}],
result: {
created: ["Dup Agent"],
skipped: [],
errors: [],
},
});
mockCreateAgent.mockRejectedValueOnce(new Error("Agent with name \"Dup Agent\" already exists (agentId: agent-existing)"));
const response = await postImport(app, {
manifest: "---\nname: Dup Agent\n---\nInstructions",
});
expect(response.status).toBe(200);
const body = response.body as any;
expect(body.skipped).toContain("Dup Agent");
expect(body.errors).toEqual([]);
});
it("honors skipExisting and returns skipped agents", async () => {
mockListAgents.mockResolvedValue([{ id: "agent-existing", name: "YAML Agent" }]);
mockPrepareAgentCompaniesImport.mockReturnValue({
items: [],
result: {
created: [],
skipped: ["YAML Agent"],
errors: [],
},
});
const response = await postImport(app, {
manifest: "---\nname: YAML Agent\n---\nInstructions",
skipExisting: true,
});
expect(response.status).toBe(200);
const body = response.body as any;
expect(body.skipped).toEqual(["YAML Agent"]);
expect(mockCreateAgent).not.toHaveBeenCalled();
});
it("dry-run preview includes skills from company package", async () => {
const sourceDir = join(testDir, "skills-company");
mkdirSync(join(sourceDir, "agents", "ceo"), { recursive: true });
writeFileSync(join(sourceDir, "agents", "ceo", "AGENTS.md"), "---\nname: CEO\n---\nLead");
mockParseCompanyDirectory.mockReturnValue({
company: { name: "Directory Co", slug: "directory-co" },
agents: [{ name: "Dir Agent", skills: ["review"] }],
teams: [{ name: "Engineering" }],
projects: [],
tasks: [],
skills: [
{ name: "review", description: "Review implementation details" },
{ name: "strategy" },
],
});
const response = await postImport(app, { source: sourceDir, dryRun: true });
expect(response.status).toBe(200);
const body = response.body as any;
expect(body.skills).toEqual([
{ name: "review", description: "Review implementation details" },
{ name: "strategy" },
]);
});
it("dry-run preview returns empty skills when package has no skills", async () => {
const sourceDir = join(testDir, "no-skills-company");
mkdirSync(join(sourceDir, "agents", "ceo"), { recursive: true });
writeFileSync(join(sourceDir, "agents", "ceo", "AGENTS.md"), "---\nname: CEO\n---\nLead");
mockParseCompanyDirectory.mockReturnValue({
company: { name: "Directory Co", slug: "directory-co" },
agents: [{ name: "Dir Agent", skills: ["review"] }],
teams: [{ name: "Engineering" }],
projects: [],
tasks: [],
});
const response = await postImport(app, { source: sourceDir, dryRun: true });
expect(response.status).toBe(200);
const body = response.body as any;
expect(body.skills).toEqual([]);
});
it("returns 400 for parser errors", async () => {
mockParseSingleAgentManifest.mockImplementation(() => {
throw new MockAgentCompaniesParseError("Missing YAML frontmatter delimiters (---)");
});
const response = await postImport(app, {
manifest: "invalid",
});
expect(response.status).toBe(400);
expect((response.body as any).error).toContain("Missing YAML frontmatter");
});
it("rejects invalid companies.sh slugs", async () => {
const response = await postImport(app, {
importSource: "companies.sh",
companySlug: "Invalid Slug!",
});
expect(response.status).toBe(400);
expect((response.body as any).error).toContain("Invalid companies.sh slug");
});
it("rejects companies.sh slug with uppercase", async () => {
const response = await postImport(app, {
importSource: "companies.sh",
companySlug: "MyCompany",
});
expect(response.status).toBe(400);
expect((response.body as any).error).toContain("Invalid companies.sh slug");
});
it("rejects companies.sh slug with special characters", async () => {
const response = await postImport(app, {
importSource: "companies.sh",
companySlug: "company@123",
});
expect(response.status).toBe(400);
expect((response.body as any).error).toContain("Invalid companies.sh slug");
});
it("returns companies.sh dry-run preview with package skills", async () => {
const archiveBuffer = createGitHubArchiveBuffer("acme-ai");
globalThis.fetch = vi.fn().mockImplementation((input: unknown) => {
const url = String(input);
if (url === "https://companies.sh/api/companies") {
return Promise.resolve(new Response(
JSON.stringify({ items: [{ slug: "acme-ai", name: "Acme AI", repo: "acme/reviewers", website: "https://github.com/acme/reviewers/tree/main" }] }),
{
status: 200,
headers: { "content-type": "application/json" },
},
));
}
if (url === "https://github.com/acme/reviewers/archive/refs/heads/main.tar.gz") {
return Promise.resolve(new Response(archiveBuffer, {
status: 200,
headers: { "content-type": "application/gzip" },
}));
}
return Promise.reject(new Error(`Unexpected fetch URL: ${url}`));
});
mockParseCompanyArchive.mockResolvedValue({
company: { name: "Acme AI", slug: "acme-ai" },
agents: [{ name: "Reviewer", skills: ["review"] }],
teams: [],
projects: [],
tasks: [],
skills: [{ name: "review", description: "Review implementation details" }],
});
const response = await postImport(app, {
importSource: "companies.sh",
companySlug: "acme-ai",
dryRun: true,
});
expect(response.status, JSON.stringify(response.body)).toBe(200);
const body = response.body as any;
expect(body.dryRun).toBe(true);
expect(body.companyName).toBe("Acme AI");
expect(body.companySlug).toBe("acme-ai");
expect(body.agents).toEqual([expect.objectContaining({ name: "YAML Agent" })]);
expect(body.skills).toEqual([{ name: "review", description: "Review implementation details" }]);
});
it("passes website-derived subPath for monorepo company imports", async () => {
const archiveBuffer = createGitHubArchiveBuffer("gstack");
globalThis.fetch = vi.fn().mockImplementation((input: unknown) => {
const url = String(input);
if (url === "https://companies.sh/api/companies") {
return Promise.resolve(new Response(
JSON.stringify({
items: [
{ slug: "gstack", name: "GStack", repo: "paperclipai/companies", website: "https://github.com/paperclipai/companies/tree/main/gstack" },
{ slug: "aeon", name: "Aeon", repo: "paperclipai/companies", website: "https://github.com/paperclipai/companies/tree/main/aeon" },
],
}),
{ status: 200, headers: { "content-type": "application/json" } },
));
}
if (url === "https://github.com/paperclipai/companies/archive/refs/heads/main.tar.gz") {
return Promise.resolve(new Response(archiveBuffer, {
status: 200,
headers: { "content-type": "application/gzip" },
}));
}
return Promise.reject(new Error(`Unexpected fetch URL: ${url}`));
});
mockParseCompanyArchive.mockImplementation(async (_archivePath: string, options?: { subPath?: string }) => {
expect(options).toEqual({ subPath: "gstack" });
if (options?.subPath === "gstack") {
return {
company: { name: "GStack", slug: "gstack" },
agents: [{ name: "GStack Agent" }],
teams: [],
projects: [],
tasks: [],
skills: [],
};
}
return {
company: { name: "Aeon", slug: "aeon" },
agents: [{ name: "Aeon Agent" }],
teams: [],
projects: [],
tasks: [],
skills: [],
};
});
const response = await postImport(app, {
importSource: "companies.sh",
companySlug: "gstack",
dryRun: true,
});
expect(response.status, JSON.stringify(response.body)).toBe(200);
const body = response.body as any;
expect(body.companyName).toBe("GStack");
expect(body.companySlug).toBe("gstack");
});
it("omits subPath when website is absent or invalid", async () => {
const archiveBuffer = createGitHubArchiveBuffer("acme-ai");
globalThis.fetch = vi.fn().mockImplementation((input: unknown) => {
const url = String(input);
if (url === "https://companies.sh/api/companies") {
return Promise.resolve(new Response(
JSON.stringify({ items: [{ slug: "acme-ai", name: "Acme AI", repo: "acme/reviewers", website: "https://example.com/not-github" }] }),
{ status: 200, headers: { "content-type": "application/json" } },
));
}
if (url === "https://github.com/acme/reviewers/archive/refs/heads/main.tar.gz") {
return Promise.resolve(new Response(archiveBuffer, {
status: 200,
headers: { "content-type": "application/gzip" },
}));
}
return Promise.reject(new Error(`Unexpected fetch URL: ${url}`));
});
mockParseCompanyArchive.mockResolvedValue({
company: { name: "Acme AI", slug: "acme-ai" },
agents: [{ name: "Reviewer", skills: ["review"] }],
teams: [],
projects: [],
tasks: [],
skills: [],
});
const response = await postImport(app, {
importSource: "companies.sh",
companySlug: "acme-ai",
dryRun: true,
});
expect(response.status, JSON.stringify(response.body)).toBe(200);
expect(mockParseCompanyArchive).toHaveBeenCalledWith(expect.any(String), undefined);
});
it("returns companies.sh live import with skill import result", async () => {
const archiveBuffer = createGitHubArchiveBuffer("acme-ai");
globalThis.fetch = vi.fn().mockImplementation((input: unknown) => {
const url = String(input);
if (url === "https://companies.sh/api/companies") {
return Promise.resolve(new Response(
JSON.stringify({ items: [{ slug: "acme-ai", name: "Acme AI", repo: "acme/reviewers", website: "https://github.com/acme/reviewers/tree/main" }] }),
{
status: 200,
headers: { "content-type": "application/json" },
},
));
}
if (url === "https://github.com/acme/reviewers/archive/refs/heads/main.tar.gz") {
return Promise.resolve(new Response(archiveBuffer, {
status: 200,
headers: { "content-type": "application/gzip" },
}));
}
return Promise.reject(new Error(`Unexpected fetch URL: ${url}`));
});
mockParseCompanyArchive.mockResolvedValue({
company: { name: "Acme AI", slug: "acme-ai" },
agents: [{ name: "Reviewer", skills: ["review"] }],
teams: [],
projects: [],
tasks: [],
skills: [{ name: "review", description: "Review implementation details" }],
});
mockFsAccess.mockReset();
mockFsMkdir.mockReset();
mockFsWriteFile.mockReset();
mockFsAccess.mockRejectedValue(new Error("ENOENT"));
mockFsMkdir.mockResolvedValue(undefined);
mockFsWriteFile.mockResolvedValue(undefined);
const response = await postImport(app, {
importSource: "companies.sh",
companySlug: "acme-ai",
});
expect(response.status, JSON.stringify(response.body)).toBe(200);
const body = response.body as any;
expect(body.companyName).toBe("Acme AI");
expect(body.companySlug).toBe("acme-ai");
expect(body.skillsCount).toBe(1);
expect(body.skills).toEqual({
imported: [
expect.objectContaining({
name: "review",
path: expect.stringContaining("skills/imported/acme-ai/review/SKILL.md"),
}),
],
skipped: [],
errors: [],
});
expect(mockFsWriteFile).toHaveBeenCalledTimes(1);
});
it("imports only selected companies.sh agents and skills", async () => {
const archiveBuffer = createGitHubArchiveBuffer("acme-ai");
globalThis.fetch = vi.fn().mockImplementation((input: unknown) => {
const url = String(input);
if (url === "https://companies.sh/api/companies") {
return Promise.resolve(new Response(
JSON.stringify({ items: [{ slug: "acme-ai", name: "Acme AI", repo: "acme/reviewers", website: "https://github.com/acme/reviewers/tree/main" }] }),
{
status: 200,
headers: { "content-type": "application/json" },
},
));
}
if (url === "https://github.com/acme/reviewers/archive/refs/heads/main.tar.gz") {
return Promise.resolve(new Response(archiveBuffer, {
status: 200,
headers: { "content-type": "application/gzip" },
}));
}
return Promise.reject(new Error(`Unexpected fetch URL: ${url}`));
});
mockParseCompanyArchive.mockResolvedValue({
company: { name: "Acme AI", slug: "acme-ai" },
agents: [
{ name: "Reviewer", skills: ["review"] },
{ name: "Planner", skills: ["strategy"] },
],
teams: [],
projects: [],
tasks: [],
skills: [
{ name: "review", description: "Review implementation details" },
{ name: "strategy", description: "Plan implementation details" },
],
});
mockPrepareAgentCompaniesImport.mockImplementation((pkg: any) => ({
items: (pkg.agents ?? []).map((agent: { name: string }, index: number) => ({
manifestKey: agent.name.toLowerCase(),
aliases: [agent.name.toLowerCase()],
index,
input: { name: agent.name, role: "custom" },
})),
result: {
created: (pkg.agents ?? []).map((agent: { name: string }) => agent.name),
skipped: [],
errors: [],
},
}));
mockFsAccess.mockReset();
mockFsMkdir.mockReset();
mockFsWriteFile.mockReset();
mockFsAccess.mockRejectedValue(new Error("ENOENT"));
mockFsMkdir.mockResolvedValue(undefined);
mockFsWriteFile.mockResolvedValue(undefined);
const response = await postImport(app, {
importSource: "companies.sh",
companySlug: "acme-ai",
selectedAgents: ["Reviewer"],
selectedSkills: ["review"],
});
expect(response.status, JSON.stringify(response.body)).toBe(200);
const body = response.body as any;
expect(body.created).toEqual([{ id: "agent-Reviewer", name: "Reviewer" }]);
expect(body.skillsCount).toBe(1);
expect(body.skills.imported).toEqual([
expect.objectContaining({
name: "review",
path: expect.stringContaining("skills/imported/acme-ai/review/SKILL.md"),
}),
]);
expect(mockFsWriteFile).toHaveBeenCalledTimes(1);
const preparedPackage = mockPrepareAgentCompaniesImport.mock.calls[0]?.[0] as any;
expect(preparedPackage.agents).toEqual([{ name: "Reviewer", skills: ["review"] }]);
expect(preparedPackage.skills).toEqual([{ name: "review", description: "Review implementation details" }]);
});
it("live import persists skills and returns skill import result", async () => {
const sourceDir = join(testDir, "skills-live-company");
mkdirSync(join(sourceDir, "agents", "ceo"), { recursive: true });
writeFileSync(join(sourceDir, "agents", "ceo", "AGENTS.md"), "---\nname: CEO\n---\nLead");
mockParseCompanyDirectory.mockReturnValue({
company: { name: "Directory Co", slug: "directory-co" },
agents: [{ name: "Dir Agent", skills: ["review"] }],
teams: [{ name: "Engineering" }],
projects: [],
tasks: [],
skills: [
{ name: "review", description: "Review code changes", instructionBody: "# Review\n\nReview instructions" },
{ name: "strategy", description: "Strategic planning" },
],
});
// Mock fs operations: access returns "not exists" for all skill paths, mkdir and write succeed
mockFsAccess.mockReset();
mockFsMkdir.mockReset();
mockFsWriteFile.mockReset();
mockFsAccess.mockRejectedValue(new Error("ENOENT"));
mockFsMkdir.mockResolvedValue(undefined);
mockFsWriteFile.mockResolvedValue(undefined);
const response = await postImport(app, { source: sourceDir });
expect(response.status).toBe(200);
const body = response.body as any;
expect(body.skills).toBeDefined();
expect(body.skills.imported).toHaveLength(2);
expect(body.skills.imported[0].name).toBe("review");
expect(body.skills.imported[0].path).toContain("skills/imported/directory-co/");
expect(body.skills.skipped).toEqual([]);
expect(body.skills.errors).toEqual([]);
expect(mockFsMkdir).toHaveBeenCalled();
expect(mockFsWriteFile).toHaveBeenCalledTimes(2);
});
it("live import skips skills that already exist", async () => {
const sourceDir = join(testDir, "skills-skipped-company");
mkdirSync(join(sourceDir, "agents", "ceo"), { recursive: true });
writeFileSync(join(sourceDir, "agents", "ceo", "AGENTS.md"), "---\nname: CEO\n---\nLead");
mockParseCompanyDirectory.mockReturnValue({
company: { name: "Directory Co", slug: "directory-co" },
agents: [{ name: "Dir Agent", skills: ["review"] }],
teams: [{ name: "Engineering" }],
projects: [],
tasks: [],
skills: [{ name: "review" }],
});
// Mock fs: access returns success (file exists)
mockFsAccess.mockReset();
mockFsMkdir.mockReset();
mockFsWriteFile.mockReset();
mockFsAccess.mockResolvedValue(undefined);
mockFsMkdir.mockResolvedValue(undefined);
mockFsWriteFile.mockResolvedValue(undefined);
const response = await postImport(app, { source: sourceDir });
expect(response.status).toBe(200);
const body = response.body as any;
expect(body.skills).toBeDefined();
expect(body.skills.imported).toEqual([]);
expect(body.skills.skipped).toEqual(["review"]);
expect(body.skills.errors).toEqual([]);
expect(mockFsWriteFile).not.toHaveBeenCalled();
});
it("live import handles skill write errors gracefully", async () => {
const sourceDir = join(testDir, "skills-error-company");
mkdirSync(join(sourceDir, "agents", "ceo"), { recursive: true });
writeFileSync(join(sourceDir, "agents", "ceo", "AGENTS.md"), "---\nname: CEO\n---\nLead");
mockParseCompanyDirectory.mockReturnValue({
company: { name: "Directory Co", slug: "directory-co" },
agents: [{ name: "Dir Agent", skills: ["review"] }],
teams: [{ name: "Engineering" }],
projects: [],
tasks: [],
skills: [{ name: "review" }],
});
// Mock fs: access returns "not exists", but mkdir fails
mockFsAccess.mockReset();
mockFsMkdir.mockReset();
mockFsWriteFile.mockReset();
mockFsAccess.mockRejectedValue(new Error("ENOENT"));
mockFsMkdir.mockRejectedValue(new Error("Permission denied"));
mockFsWriteFile.mockResolvedValue(undefined);
const response = await postImport(app, { source: sourceDir });
expect(response.status).toBe(200);
const body = response.body as any;
expect(body.skills).toBeDefined();
expect(body.skills.imported).toEqual([]);
expect(body.skills.skipped).toEqual([]);
expect(body.skills.errors).toHaveLength(1);
expect(body.skills.errors[0].name).toBe("review");
expect(body.skills.errors[0].error).toContain("Permission denied");
});
it("live import uses fallback company slug when not provided", async () => {
const sourceDir = join(testDir, "no-slug-company");
mkdirSync(join(sourceDir, "agents", "ceo"), { recursive: true });
writeFileSync(join(sourceDir, "agents", "ceo", "AGENTS.md"), "---\nname: CEO\n---\nLead");
mockParseCompanyDirectory.mockReturnValue({
company: { name: "Directory Co" }, // no slug
agents: [{ name: "Dir Agent", skills: ["review"] }],
teams: [],
projects: [],
tasks: [],
skills: [{ name: "review" }],
});
// Reset and setup mocks
mockFsAccess.mockReset();
mockFsMkdir.mockReset();
mockFsWriteFile.mockReset();
mockFsAccess.mockRejectedValue(new Error("ENOENT"));
mockFsMkdir.mockResolvedValue(undefined);
mockFsWriteFile.mockResolvedValue(undefined);
const response = await postImport(app, { source: sourceDir });
expect(response.status).toBe(200);
const body = response.body as any;
expect(body.skills.imported).toBeDefined();
expect(body.skills.imported.length).toBeGreaterThan(0);
expect(body.skills.imported[0].path).toContain("skills/imported/unknown-company/");
});
it("live import returns empty skills when package has no skills", async () => {
const sourceDir = join(testDir, "no-skills-live-company");
mkdirSync(join(sourceDir, "agents", "ceo"), { recursive: true });
writeFileSync(join(sourceDir, "agents", "ceo", "AGENTS.md"), "---\nname: CEO\n---\nLead");
mockParseCompanyDirectory.mockReturnValue({
company: { name: "Directory Co", slug: "directory-co" },
agents: [{ name: "Dir Agent", skills: ["review"] }],
teams: [{ name: "Engineering" }],
projects: [],
tasks: [],
});
const response = await postImport(app, { source: sourceDir });
expect(response.status).toBe(200);
const body = response.body as any;
expect(body.skills).toBeDefined();
expect(body.skills.imported).toEqual([]);
expect(body.skills.skipped).toEqual([]);
expect(body.skills.errors).toEqual([]);
});
it("dry-run does not persist skills", async () => {
const sourceDir = join(testDir, "dryrun-skills");
mkdirSync(join(sourceDir, "agents", "ceo"), { recursive: true });
writeFileSync(join(sourceDir, "agents", "ceo", "AGENTS.md"), "---\nname: CEO\n---\nLead");
mockParseCompanyDirectory.mockReturnValue({
company: { name: "Directory Co", slug: "directory-co" },
agents: [{ name: "Dir Agent", skills: ["review"] }],
teams: [],
projects: [],
tasks: [],
skills: [{ name: "review" }],
});
// Reset mocks to ensure clean state
mockFsAccess.mockReset();
mockFsMkdir.mockReset();
mockFsWriteFile.mockReset();
const response = await postImport(app, { source: sourceDir, dryRun: true });
expect(response.status).toBe(200);
const body = response.body as any;
expect(body.dryRun).toBe(true);
// dryRun returns skills preview, not skill import result
// (skill import result has imported/skipped/errors, preview has name/description)
expect(Array.isArray(body.skills)).toBe(true);
expect(body.skills[0]?.name).toBe("review");
// dryRun should NOT call fs operations
expect(mockFsWriteFile).not.toHaveBeenCalled();
expect(mockFsMkdir).not.toHaveBeenCalled();
});
});
describe("GET /api/agents/companies", () => {
let store: MockStore;
let app: ReturnType<typeof createServer>;
const originalFetch = globalThis.fetch;
beforeEach(async () => {
vi.clearAllMocks();
vi.mock("../server.js", async () => {
const actual = await vi.importActual("../server.js");
return actual;
});
mockInit.mockResolvedValue(undefined);
store = new MockStore();
app = createServer(store as any);
});
afterEach(() => {
vi.restoreAllMocks();
globalThis.fetch = originalFetch;
});
it("returns companies when external API succeeds", async () => {
const mockCompanies = [
{ slug: "test-company", name: "Test Company", tagline: "A test company" },
{ slug: "another-company", name: "Another Company" },
];
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
headers: { get: () => "application/json" },
json: async () => mockCompanies,
});
const response = await request(app, "GET", "/api/agents/companies");
expect(response.status).toBe(200);
const body = response.body as any;
expect(body.companies).toHaveLength(2);
expect(body.companies[0].slug).toBe("test-company");
expect(body.error).toBeUndefined();
});
it("returns error message when external API is unreachable", async () => {
globalThis.fetch = vi.fn().mockRejectedValue(new Error("Network unreachable"));
const response = await request(app, "GET", "/api/agents/companies");
expect(response.status).toBe(200);
const body = response.body as any;
expect(body.companies).toEqual([]);
expect(body.error).toContain("Failed to fetch companies.sh catalog:");
expect(body.error).toContain("Network unreachable");
});
it("returns error message when external API returns non-JSON", async () => {
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
headers: { get: () => "text/html" },
json: async () => { throw new Error("Not JSON"); },
});
const response = await request(app, "GET", "/api/agents/companies");
expect(response.status).toBe(200);
const body = response.body as any;
expect(body.companies).toEqual([]);
expect(body.error).toContain("Failed to fetch companies.sh catalog:");
});
it("returns 500 when request times out", async () => {
// Create an AbortError by using a mock that throws with 'aborted' in the message
const abortError = new Error("The operation was aborted");
abortError.name = "AbortError";
globalThis.fetch = vi.fn().mockRejectedValue(abortError);
const response = await request(app, "GET", "/api/agents/companies");
expect(response.status).toBe(500);
const body = response.body as any;
expect(body.error).toContain("timed out");
});
});