Files
br-os/tests/spawnAgent.test.ts
2025-12-01 16:23:11 -06:00

302 lines
10 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { execSync } from "child_process";
import fs from "fs";
import 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";
const TEST_AGENT_NAME = "test-agent-xyz";
const ROOT_DIR = path.join(__dirname, "..");
describe("spawn-agent utilities", () => {
beforeEach(() => {
if (!fs.existsSync(TEST_OUTPUT_DIR)) {
fs.mkdirSync(TEST_OUTPUT_DIR, { recursive: true });
}
});
afterEach(() => {
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 as any[]).map((t) => (typeof t === "string" ? t : t.emoji));
expect(triggerEmojis).toContain("📝");
});
it("should include custom emoji trigger", () => {
const agent = spawnAgent("my-agent", { emoji: "🔮" });
const triggerEmojis = (agent.triggers as any[]).map((t) => (typeof t === "string" ? 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) as Agent;
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) => (typeof t === "string" ? 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);
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");
});
});
describe("spawn-agent script", () => {
const cleanupFiles = () => {
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);
}
}
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 });
}
}
};
beforeEach(() => {
cleanupFiles();
});
afterEach(() => {
cleanupFiles();
});
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}`);
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");
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");
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");
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 {
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);
}
}
}
});
});