import { describe, it, expect, beforeEach, afterEach } from "vitest"; import * as fs from "fs"; import * as path from "path"; import { spawnAgent, saveAgent } from "../src/agents/spawn-agent"; import { AgentBuilder, AgentRegistry, createAgent } from "../src/agents/lucidia-agent-builder"; import type { Agent } from "../src/agents/types"; const TEST_AGENTS_DIR = path.join(__dirname, "../src/agents"); const TEST_OUTPUT_DIR = "/tmp/test-agents"; describe("spawn-agent", () => { beforeEach(() => { // Create test output directory if (!fs.existsSync(TEST_OUTPUT_DIR)) { fs.mkdirSync(TEST_OUTPUT_DIR, { recursive: true }); import { describe, expect, it, beforeEach, afterEach } from "vitest"; import { execSync } from "child_process"; import fs from "fs"; import path from "path"; const TEST_AGENT_NAME = "test-agent-xyz"; const ROOT_DIR = path.join(__dirname, ".."); describe("spawn-agent.js", () => { beforeEach(() => { // Clean up any existing test agent files before each test const paths = [ path.join(ROOT_DIR, "agents", `${TEST_AGENT_NAME}.agent.json`), path.join(ROOT_DIR, "agents", `${TEST_AGENT_NAME}.prompt.txt`), path.join(ROOT_DIR, ".github", "workflows", `${TEST_AGENT_NAME}.workflow.yml`), path.join(ROOT_DIR, "docs", "agents", `${TEST_AGENT_NAME}.mdx`), ]; for (const p of paths) { if (fs.existsSync(p)) { fs.unlinkSync(p); } } }); afterEach(() => { // Clean up test files if (fs.existsSync(TEST_OUTPUT_DIR)) { const files = fs.readdirSync(TEST_OUTPUT_DIR); for (const file of files) { fs.unlinkSync(path.join(TEST_OUTPUT_DIR, file)); } fs.rmdirSync(TEST_OUTPUT_DIR); } }); describe("spawnAgent", () => { it("should create an agent with correct ID from name", () => { const agent = spawnAgent("Test Agent"); expect(agent.id).toBe("test-agent"); expect(agent.name).toBe("Test Agent"); }); it("should infer scribe role from name", () => { const agent = spawnAgent("scribe-support"); expect(agent.role).toBe("scribe"); }); it("should infer qa role from name", () => { const agent = spawnAgent("qa-validator"); expect(agent.role).toBe("qa"); }); it("should infer archive role from name", () => { const agent = spawnAgent("archive-manager"); expect(agent.role).toBe("archive"); }); it("should use custom role when specified", () => { const agent = spawnAgent("my-agent", { role: "guardian" }); expect(agent.role).toBe("guardian"); }); it("should use custom description when provided", () => { const agent = spawnAgent("my-agent", { description: "Custom description" }); expect(agent.description).toBe("Custom description"); }); it("should include parent agent reference", () => { const agent = spawnAgent("child-agent", { parent: "parent-agent" }); expect(agent.parentAgent).toBe("parent-agent"); expect(agent.metadata.createdBy).toContain("spawned-by"); }); it("should include custom traits", () => { const agent = spawnAgent("my-agent", { traits: ["fast", "reliable"] }); expect(agent.traits).toContain("fast"); expect(agent.traits).toContain("reliable"); }); it("should include base capabilities", () => { const agent = spawnAgent("my-agent"); const capabilityNames = agent.capabilities.map(c => c.name); expect(capabilityNames).toContain("self-describe"); expect(capabilityNames).toContain("issue-link"); expect(capabilityNames).toContain("pr-link"); }); it("should include role-specific triggers", () => { const agent = spawnAgent("scribe-agent"); const triggerEmojis = agent.triggers.map(t => t.emoji); expect(triggerEmojis).toContain("📝"); }); it("should include custom emoji trigger", () => { const agent = spawnAgent("my-agent", { emoji: "🔮" }); const triggerEmojis = agent.triggers.map(t => t.emoji); expect(triggerEmojis).toContain("🔮"); }); it("should include metadata with timestamp", () => { const agent = spawnAgent("my-agent"); expect(agent.metadata.createdAt).toBeDefined(); expect(agent.metadata.version).toBe("1.0.0"); }); }); describe("saveAgent", () => { it("should save agent to JSON file", () => { const agent = spawnAgent("test-save-agent"); const filePath = saveAgent(agent, TEST_OUTPUT_DIR); expect(fs.existsSync(filePath)).toBe(true); const content = fs.readFileSync(filePath, "utf-8"); const savedAgent = JSON.parse(content); expect(savedAgent.id).toBe("test-save-agent"); }); }); }); describe("AgentBuilder", () => { it("should build agent with fluent API", () => { const agent = createAgent("Custom Agent") .withRole("guardian") .withDescription("A custom guardian agent") .withTraits("vigilant", "protective") .withTrigger("🛡️", "protect") .withCapability("monitoring", "Monitors the system", true) .build(); expect(agent.id).toBe("custom-agent"); expect(agent.name).toBe("Custom Agent"); expect(agent.role).toBe("guardian"); expect(agent.description).toBe("A custom guardian agent"); expect(agent.traits).toContain("vigilant"); expect(agent.triggers.some(t => t.emoji === "🛡️")).toBe(true); expect(agent.capabilities.some(c => c.name === "monitoring")).toBe(true); }); it("should set parent agent", () => { const agent = createAgent("Child Agent") .withRole("support") .withParent("parent-id") .build(); expect(agent.parentAgent).toBe("parent-id"); }); it("should add default capabilities if none provided", () => { const agent = createAgent("Simple Agent") .withRole("custom") .build(); const capabilityNames = agent.capabilities.map(c => c.name); expect(capabilityNames).toContain("self-describe"); }); }); describe("AgentRegistry", () => { it("should load existing agents", () => { const registry = new AgentRegistry(TEST_AGENTS_DIR); const agents = registry.loadAll(); expect(agents.length).toBeGreaterThan(0); // Should have loaded the predefined agents const agentIds = agents.map(a => a.id); expect(agentIds).toContain("scribe-agent"); expect(agentIds).toContain("qa-agent"); expect(agentIds).toContain("guardian-agent"); }); it("should get agent by ID", () => { const registry = new AgentRegistry(TEST_AGENTS_DIR); const agent = registry.get("scribe-agent"); expect(agent).toBeDefined(); expect(agent?.role).toBe("scribe"); }); it("should find agents by role", () => { const registry = new AgentRegistry(TEST_AGENTS_DIR); registry.loadAll(); const qaAgents = registry.findByRole("qa"); expect(qaAgents.length).toBeGreaterThan(0); expect(qaAgents[0].role).toBe("qa"); }); it("should find agents by trigger emoji", () => { const registry = new AgentRegistry(TEST_AGENTS_DIR); registry.loadAll(); const agents = registry.findByTrigger("📝"); expect(agents.length).toBeGreaterThan(0); }); it("should list all agent IDs", () => { const registry = new AgentRegistry(TEST_AGENTS_DIR); const ids = registry.list(); expect(ids.length).toBeGreaterThan(0); expect(ids).toContain("scribe-agent"); // Clean up test agent files after each test const paths = [ path.join(ROOT_DIR, "agents", `${TEST_AGENT_NAME}.agent.json`), path.join(ROOT_DIR, "agents", `${TEST_AGENT_NAME}.prompt.txt`), path.join(ROOT_DIR, ".github", "workflows", `${TEST_AGENT_NAME}.workflow.yml`), path.join(ROOT_DIR, "docs", "agents", `${TEST_AGENT_NAME}.mdx`), ]; for (const p of paths) { if (fs.existsSync(p)) { fs.unlinkSync(p); } } // Clean up directories if empty const dirs = [ path.join(ROOT_DIR, "agents"), path.join(ROOT_DIR, "docs", "agents"), ]; for (const d of dirs) { if (fs.existsSync(d) && fs.readdirSync(d).length === 0) { fs.rmSync(d, { recursive: true }); } } }); it("should show error when no agent name provided", () => { let error: Error | null = null; try { execSync("node scripts/spawn-agent.js", { cwd: ROOT_DIR, encoding: "utf-8" }); } catch (e: any) { error = e; } expect(error).not.toBeNull(); expect(error!.message).toContain("Please provide an agent name"); }); it("should create all required files when agent name provided", () => { const output = execSync(`node scripts/spawn-agent.js ${TEST_AGENT_NAME}`, { cwd: ROOT_DIR, encoding: "utf-8", }); expect(output).toContain(`Created agent: ${TEST_AGENT_NAME}`); // Check agent JSON file const jsonPath = path.join(ROOT_DIR, "agents", `${TEST_AGENT_NAME}.agent.json`); expect(fs.existsSync(jsonPath)).toBe(true); const jsonContent = JSON.parse(fs.readFileSync(jsonPath, "utf-8")); expect(jsonContent.id).toBe(TEST_AGENT_NAME); expect(jsonContent.name).toBe("Test Agent Xyz"); expect(jsonContent.role).toBe("worker"); expect(jsonContent.traits).toContain("emoji-native"); expect(jsonContent.inherits_from).toBe("base-agent"); // Check prompt file const promptPath = path.join(ROOT_DIR, "agents", `${TEST_AGENT_NAME}.prompt.txt`); expect(fs.existsSync(promptPath)).toBe(true); const promptContent = fs.readFileSync(promptPath, "utf-8"); expect(promptContent).toContain("SYSTEM:"); expect(promptContent).toContain("Test Agent Xyz"); // Check workflow file const workflowPath = path.join(ROOT_DIR, ".github", "workflows", `${TEST_AGENT_NAME}.workflow.yml`); expect(fs.existsSync(workflowPath)).toBe(true); const workflowContent = fs.readFileSync(workflowPath, "utf-8"); expect(workflowContent).toContain("name: Test Agent Xyz Workflow"); expect(workflowContent).toContain("workflow_dispatch"); // Check docs file const docPath = path.join(ROOT_DIR, "docs", "agents", `${TEST_AGENT_NAME}.mdx`); expect(fs.existsSync(docPath)).toBe(true); const docContent = fs.readFileSync(docPath, "utf-8"); expect(docContent).toContain("# Test Agent Xyz Agent"); expect(docContent).toContain("Auto-generated"); }); it("should convert spaces in agent name to hyphens for agent id", () => { const agentNameWithSpaces = "my cool agent"; const expectedId = "my-cool-agent"; try { const output = execSync(`node scripts/spawn-agent.js "${agentNameWithSpaces}"`, { cwd: ROOT_DIR, encoding: "utf-8", }); expect(output).toContain(`Created agent: ${expectedId}`); const jsonPath = path.join(ROOT_DIR, "agents", `${expectedId}.agent.json`); expect(fs.existsSync(jsonPath)).toBe(true); } finally { // Cleanup const paths = [ path.join(ROOT_DIR, "agents", `${expectedId}.agent.json`), path.join(ROOT_DIR, "agents", `${expectedId}.prompt.txt`), path.join(ROOT_DIR, ".github", "workflows", `${expectedId}.workflow.yml`), path.join(ROOT_DIR, "docs", "agents", `${expectedId}.mdx`), ]; for (const p of paths) { if (fs.existsSync(p)) { fs.unlinkSync(p); } } } }); });