import { describe, it, expect, beforeEach } from "vitest"; import { ClaudeAdapter } from "../../../src/adapters/ClaudeAdapter"; import type { OpenAgent, AgentFrontmatter, HookDefinition, SkillReference, } from "../../../src/types"; /** * Unit tests for ClaudeAdapter with 80%+ coverage * * Test strategy: * 1. toOAC() - Parse Claude formats to OpenAgent * 2. fromOAC() - Convert OpenAgent to Claude formats * 3. getCapabilities() - Feature matrix validation * 4. validateConversion() - Validation and warnings * 5. Helper methods - Model/tool/hook/skill mapping * 6. Edge cases - Invalid input, missing fields, empty values * 7. Roundtrip - Data integrity checks */ describe("ClaudeAdapter", () => { let adapter: ClaudeAdapter; beforeEach(() => { adapter = new ClaudeAdapter(); }); // ============================================================================ // ADAPTER IDENTITY // ============================================================================ describe("adapter identity", () => { it("has correct name", () => { expect(adapter.name).toBe("claude"); }); it("has correct displayName", () => { expect(adapter.displayName).toBe("Claude Code"); }); it("returns correct config path", () => { expect(adapter.getConfigPath()).toBe(".claude/"); }); }); // ============================================================================ // CAPABILITIES // ============================================================================ describe("getCapabilities()", () => { it("returns correct capabilities object", () => { const capabilities = adapter.getCapabilities(); expect(capabilities.name).toBe("claude"); expect(capabilities.displayName).toBe("Claude Code"); expect(capabilities.supportsMultipleAgents).toBe(true); expect(capabilities.supportsSkills).toBe(true); expect(capabilities.supportsHooks).toBe(true); expect(capabilities.supportsGranularPermissions).toBe(false); expect(capabilities.supportsContexts).toBe(true); expect(capabilities.supportsCustomModels).toBe(true); expect(capabilities.supportsTemperature).toBe(false); expect(capabilities.supportsMaxSteps).toBe(false); expect(capabilities.configFormat).toBe("markdown"); expect(capabilities.outputStructure).toBe("directory"); }); it("includes appropriate notes", () => { const capabilities = adapter.getCapabilities(); expect(capabilities.notes).toBeDefined(); expect(capabilities.notes?.length).toBeGreaterThan(0); expect(capabilities.notes?.some((n) => n.includes("permissions"))).toBe( true ); }); }); // ============================================================================ // toOAC() - PARSING CLAUDE CONFIG.JSON // ============================================================================ describe("toOAC() - parsing config.json", () => { it("parses minimal config.json", async () => { const source = JSON.stringify({ name: "TestAgent", description: "Test description", systemPrompt: "You are helpful", }); const result = await adapter.toOAC(source); expect(result.frontmatter.name).toBe("TestAgent"); expect(result.frontmatter.description).toBe("Test description"); expect(result.systemPrompt).toBe("You are helpful"); expect(result.frontmatter.mode).toBe("primary"); }); it("parses config with model", async () => { const source = JSON.stringify({ name: "Agent", description: "Test", model: "claude-sonnet-4-20250514", systemPrompt: "Prompt", }); const result = await adapter.toOAC(source); expect(result.frontmatter.model).toBe("claude-sonnet-4"); }); it("parses config with tools array", async () => { const source = JSON.stringify({ name: "Agent", description: "Test", tools: ["Read", "Write", "Bash"], systemPrompt: "Prompt", }); const result = await adapter.toOAC(source); expect(result.frontmatter.tools).toEqual({ read: true, write: true, bash: true, }); }); it("parses config with tools string", async () => { const source = JSON.stringify({ name: "Agent", description: "Test", tools: "Read, Write, Edit", systemPrompt: "Prompt", }); const result = await adapter.toOAC(source); expect(result.frontmatter.tools).toEqual({ read: true, write: true, edit: true, }); }); it("parses config with skills", async () => { const source = JSON.stringify({ name: "Agent", description: "Test", skills: ["skill1", "skill2"], systemPrompt: "Prompt", }); const result = await adapter.toOAC(source); expect(result.frontmatter.skills).toEqual(["skill1", "skill2"]); }); it("parses config with hooks", async () => { const source = JSON.stringify({ name: "Agent", description: "Test", hooks: { PreToolUse: [ { matcher: "*.txt", hooks: [{ type: "command", command: "validate" }], }, ], }, systemPrompt: "Prompt", }); const result = await adapter.toOAC(source); expect(result.frontmatter.hooks).toBeDefined(); expect(result.frontmatter.hooks?.length).toBe(1); expect(result.frontmatter.hooks?.[0].event).toBe("PreToolUse"); }); it("handles missing optional fields gracefully", async () => { const source = JSON.stringify({ name: "MinimalAgent", description: "Minimal", }); const result = await adapter.toOAC(source); expect(result.frontmatter.name).toBe("MinimalAgent"); expect(result.systemPrompt).toBe(""); expect(result.frontmatter.tools).toBeUndefined(); expect(result.frontmatter.skills).toBeUndefined(); }); it("parses invalid JSON as markdown (subagent fallback)", async () => { const source = "not valid json"; const result = await adapter.toOAC(source); // Falls back to markdown parsing expect(result.frontmatter.mode).toBe("subagent"); expect(result.systemPrompt).toBe("not valid json"); }); it("parses non-object JSON string as markdown (subagent fallback)", async () => { const source = JSON.stringify("just a string"); const result = await adapter.toOAC(source); // Falls back to markdown parsing expect(result.frontmatter.mode).toBe("subagent"); }); }); // ============================================================================ // toOAC() - PARSING CLAUDE AGENT.MD (SUBAGENTS) // ============================================================================ describe("toOAC() - parsing agent.md with YAML frontmatter", () => { it("parses agent.md with minimal frontmatter", async () => { const source = `--- name: SubAgent description: A subagent --- This is the system prompt for the agent.`; const result = await adapter.toOAC(source); expect(result.frontmatter.name).toBe("SubAgent"); expect(result.frontmatter.description).toBe("A subagent"); expect(result.frontmatter.mode).toBe("subagent"); expect(result.systemPrompt).toBe("This is the system prompt for the agent."); }); it("parses agent.md with model in frontmatter", async () => { const source = `--- name: Agent description: Test model: claude-opus-4 --- Prompt`; const result = await adapter.toOAC(source); expect(result.frontmatter.model).toBe("claude-opus-4"); }); it("parses agent.md with tools array in frontmatter", async () => { const source = `--- name: Agent description: Test tools: ["Read", "Write", "Bash"] --- Prompt`; const result = await adapter.toOAC(source); expect(result.frontmatter.tools).toEqual({ read: true, write: true, bash: true, }); }); it("parses agent.md with skills in frontmatter", async () => { const source = `--- name: Agent description: Test skills: ["skill1", "skill2"] --- Prompt`; const result = await adapter.toOAC(source); expect(result.frontmatter.skills).toEqual(["skill1", "skill2"]); }); it("handles agent.md without frontmatter as markdown content", async () => { const source = "No frontmatter here, just markdown content"; const result = await adapter.toOAC(source); expect(result.systemPrompt).toBe("No frontmatter here, just markdown content"); expect(result.frontmatter.mode).toBe("subagent"); }); it("preserves multiline system prompt", async () => { const source = `--- name: Agent description: Test --- You are a helpful assistant. You should be friendly and professional. Always provide detailed responses.`; const result = await adapter.toOAC(source); expect(result.systemPrompt).toContain("You are a helpful assistant"); expect(result.systemPrompt).toContain("Always provide detailed responses"); }); }); // ============================================================================ // fromOAC() - CONVERTING TO CLAUDE CONFIG.JSON // ============================================================================ describe("fromOAC() - converting to config.json", () => { const createOpenAgent = (overrides?: Partial): OpenAgent => ({ frontmatter: { name: "TestAgent", description: "Test agent", mode: "primary", model: "claude-sonnet-4", tools: { read: true, write: true }, ...overrides?.frontmatter, }, metadata: { name: "TestAgent", category: "core", type: "agent", }, systemPrompt: "You are helpful", contexts: [], ...overrides, }); it("converts primary agent to config.json", async () => { const agent = createOpenAgent(); const result = await adapter.fromOAC(agent); expect(result.success).toBe(true); expect(result.configs).toHaveLength(1); expect(result.configs[0].fileName).toBe(".claude/config.json"); const config = JSON.parse(result.configs[0].content); expect(config.name).toBe("TestAgent"); expect(config.description).toBe("Test agent"); expect(config.model).toBe("claude-sonnet-4-20250514"); }); it("maps tools correctly in config.json", async () => { const agent = createOpenAgent({ frontmatter: { tools: { read: true, write: true, bash: true }, }, }); const result = await adapter.fromOAC(agent); const config = JSON.parse(result.configs[0].content); expect(config.tools).toEqual(["Read", "Write", "Bash"]); }); it("includes skills in config.json", async () => { const agent = createOpenAgent({ frontmatter: { skills: ["skill1", "skill2"], }, }); const result = await adapter.fromOAC(agent); const config = JSON.parse(result.configs[0].content); expect(config.skills).toEqual(["skill1", "skill2"]); }); it("includes hooks in config.json", async () => { const hook: HookDefinition = { event: "PreToolUse", matchers: ["*.txt"], commands: [{ type: "command", command: "validate" }], }; const agent = createOpenAgent({ frontmatter: { hooks: [hook], }, }); const result = await adapter.fromOAC(agent); const config = JSON.parse(result.configs[0].content); expect(config.hooks).toBeDefined(); expect(config.hooks.PreToolUse).toBeDefined(); }); it("warns when temperature is set (unsupported)", async () => { const agent = createOpenAgent({ frontmatter: { temperature: 0.7, }, }); const result = await adapter.fromOAC(agent); expect(result.warnings.some((w) => w.includes("temperature"))).toBe(true); }); it("warns when maxSteps is set (unsupported)", async () => { const agent = createOpenAgent({ frontmatter: { maxSteps: 10, }, }); const result = await adapter.fromOAC(agent); expect(result.warnings.some((w) => w.includes("maxSteps"))).toBe(true); }); it("includes validationWarnings in result", async () => { const agent = createOpenAgent({ frontmatter: { name: "", description: "", }, }); const result = await adapter.fromOAC(agent); expect(result.warnings.length).toBeGreaterThan(0); }); it("handles mixed permission rules gracefully", async () => { const agent = createOpenAgent({ frontmatter: { permission: { read: "allow", write: "ask", bash: "deny", }, }, }); const result = await adapter.fromOAC(agent); // Should include some form of warning or degradation expect(result.warnings.length).toBeGreaterThanOrEqual(0); expect(result.success).toBe(true); }); }); // ============================================================================ // fromOAC() - CONVERTING TO CLAUDE AGENT.MD (SUBAGENTS) // ============================================================================ describe("fromOAC() - converting to agent.md for subagents", () => { const createSubagent = (overrides?: Partial): OpenAgent => ({ frontmatter: { name: "CodeAnalyzer", description: "Analyzes code", mode: "subagent", model: "claude-sonnet-4", ...overrides?.frontmatter, }, metadata: { name: "CodeAnalyzer", category: "specialist", type: "subagent", }, systemPrompt: "Analyze code quality", contexts: [], ...overrides, }); it("converts subagent to agent.md file", async () => { const agent = createSubagent(); const result = await adapter.fromOAC(agent); expect(result.success).toBe(true); expect(result.configs).toHaveLength(1); expect(result.configs[0].fileName).toBe(".claude/agents/CodeAnalyzer.md"); }); it("generates proper YAML frontmatter in agent.md", async () => { const agent = createSubagent(); const result = await adapter.fromOAC(agent); const content = result.configs[0].content; expect(content).toMatch(/^---/); expect(content).toMatch(/name: "CodeAnalyzer"/); expect(content).toMatch(/description: "Analyzes code"/); expect(content).toContain("---\n\n"); }); it("includes system prompt in agent.md body", async () => { const agent = createSubagent({ systemPrompt: "Analyze code quality\nCheck for best practices", }); const result = await adapter.fromOAC(agent); const content = result.configs[0].content; expect(content).toContain("Analyze code quality"); expect(content).toContain("Check for best practices"); }); it("includes tools in agent.md frontmatter", async () => { const agent = createSubagent({ frontmatter: { tools: { read: true, bash: true }, }, }); const result = await adapter.fromOAC(agent); const content = result.configs[0].content; expect(content).toContain("Read"); expect(content).toContain("Bash"); expect(content).toContain("tools"); }); it("includes model in agent.md frontmatter", async () => { const agent = createSubagent({ frontmatter: { model: "claude-opus-4", }, }); const result = await adapter.fromOAC(agent); const content = result.configs[0].content; expect(content).toContain("model"); expect(content).toContain("claude-opus-4"); }); it("includes permission mode in agent.md frontmatter", async () => { const agent = createSubagent({ frontmatter: { permission: { read: "allow", write: "allow" }, }, }); const result = await adapter.fromOAC(agent); const content = result.configs[0].content; expect(content).toContain("permissionMode"); expect(content).toContain("bypassPermissions"); }); }); // ============================================================================ // fromOAC() - SKILLS GENERATION FROM CONTEXTS // ============================================================================ describe("fromOAC() - generating skills from contexts", () => { it("generates skill files from contexts", async () => { const agent: OpenAgent = { frontmatter: { name: "Agent", description: "Test", mode: "primary", }, metadata: { name: "Agent", category: "core", type: "agent", }, systemPrompt: "Prompt", contexts: [ { path: ".opencode/context/skills/python-best-practices.md", description: "Python coding standards", }, ], }; const result = await adapter.fromOAC(agent); expect(result.configs.length).toBeGreaterThan(1); const skillConfig = result.configs.find((c) => c.fileName.includes(".claude/skills/") ); expect(skillConfig).toBeDefined(); expect(skillConfig?.fileName).toMatch(/\.claude\/skills\/.*\/SKILL\.md/); }); it("generates correct skill name from context path", async () => { const agent: OpenAgent = { frontmatter: { name: "Agent", description: "Test", mode: "primary", }, metadata: { name: "Agent", category: "core", type: "agent", }, systemPrompt: "Prompt", contexts: [ { path: "docs/React Hooks Guide.md", description: "React hooks documentation", }, ], }; const result = await adapter.fromOAC(agent); const skillConfig = result.configs.find((c) => c.fileName.includes(".claude/skills/") ); expect(skillConfig?.fileName).toMatch(/react-hooks-guide/); }); it("includes context priority in skill content", async () => { const agent: OpenAgent = { frontmatter: { name: "Agent", description: "Test", mode: "primary", }, metadata: { name: "Agent", category: "core", type: "agent", }, systemPrompt: "Prompt", contexts: [ { path: "context/important.md", priority: "high", description: "Important context", }, ], }; const result = await adapter.fromOAC(agent); const skillConfig = result.configs.find((c) => c.fileName.includes(".claude/skills/") ); expect(skillConfig?.content).toContain("Priority: high"); }); it("generates multiple skills from multiple contexts", async () => { const agent: OpenAgent = { frontmatter: { name: "Agent", description: "Test", mode: "primary", }, metadata: { name: "Agent", category: "core", type: "agent", }, systemPrompt: "Prompt", contexts: [ { path: "context1.md", description: "First" }, { path: "context2.md", description: "Second" }, { path: "context3.md", description: "Third" }, ], }; const result = await adapter.fromOAC(agent); const skillConfigs = result.configs.filter((c) => c.fileName.includes(".claude/skills/") ); expect(skillConfigs).toHaveLength(3); }); it("includes skill metadata when context lacks description", async () => { const agent: OpenAgent = { frontmatter: { name: "Agent", description: "Test", mode: "primary", }, metadata: { name: "Agent", category: "core", type: "agent", }, systemPrompt: "Prompt", contexts: [{ path: ".opencode/context/styles.md" }], }; const result = await adapter.fromOAC(agent); const skillConfig = result.configs.find((c) => c.fileName.includes(".claude/skills/") ); expect(skillConfig?.content).toContain("Context from"); expect(skillConfig?.content).toContain("styles.md"); }); it("handles skill names with special characters", async () => { const agent: OpenAgent = { frontmatter: { name: "Agent", description: "Test", mode: "primary", }, metadata: { name: "Agent", category: "core", type: "agent", }, systemPrompt: "Prompt", contexts: [ { path: "docs/Type Script Advanced Rules.md", description: "Test" }, ], }; const result = await adapter.fromOAC(agent); const skillConfig = result.configs.find((c) => c.fileName.includes(".claude/skills/") ); expect(skillConfig).toBeDefined(); // Should have lowercase and dashed name from path expect(skillConfig?.fileName).toMatch(/type-script-advanced-rules/); }); }); // ============================================================================ // VALIDATION // ============================================================================ describe("validateConversion()", () => { const createAgent = (overrides?: Partial): OpenAgent => ({ frontmatter: { name: "Agent", description: "Test", mode: "primary", ...overrides, }, metadata: { name: "Agent", category: "core", type: "agent" }, systemPrompt: "Prompt", contexts: [], }); it("returns no warnings for valid agent", () => { const agent = createAgent(); const warnings = adapter.validateConversion(agent); expect(warnings).toHaveLength(0); }); it("warns when name is missing", () => { const agent = createAgent({ name: "" }); const warnings = adapter.validateConversion(agent); expect(warnings.some((w) => w.includes("name"))).toBe(true); }); it("warns when description is missing", () => { const agent = createAgent({ description: "" }); const warnings = adapter.validateConversion(agent); expect(warnings.some((w) => w.includes("description"))).toBe(true); }); it("warns about granular permission degradation", () => { const agent = createAgent({ permission: { read: { "file1.txt": "allow", "file2.txt": "deny" }, }, }); const warnings = adapter.validateConversion(agent); expect( warnings.some((w) => w.includes("granular permissions")) ).toBe(true); }); it("does not warn about simple permission rules", () => { const agent = createAgent({ permission: { read: "allow", write: "allow" }, }); const warnings = adapter.validateConversion(agent); expect( warnings.some((w) => w.includes("granular permissions")) ).toBe(false); }); }); // ============================================================================ // MODEL MAPPING // ============================================================================ describe("model mapping (Claude to OAC)", () => { it("maps claude-sonnet-4-20250514 to claude-sonnet-4", async () => { const source = JSON.stringify({ name: "Agent", description: "Test", model: "claude-sonnet-4-20250514", systemPrompt: "Prompt", }); const result = await adapter.toOAC(source); expect(result.frontmatter.model).toBe("claude-sonnet-4"); }); it("maps short model names", async () => { const source = JSON.stringify({ name: "Agent", description: "Test", model: "opus", systemPrompt: "Prompt", }); const result = await adapter.toOAC(source); expect(result.frontmatter.model).toBe("claude-opus-4"); }); it("preserves unknown models", async () => { const source = JSON.stringify({ name: "Agent", description: "Test", model: "claude-custom-model", systemPrompt: "Prompt", }); const result = await adapter.toOAC(source); expect(result.frontmatter.model).toBe("claude-custom-model"); }); it("handles missing model gracefully", async () => { const source = JSON.stringify({ name: "Agent", description: "Test", systemPrompt: "Prompt", }); const result = await adapter.toOAC(source); expect(result.frontmatter.model).toBeUndefined(); }); }); // ============================================================================ // MODEL MAPPING (OAC to Claude) // ============================================================================ describe("model mapping (OAC to Claude)", () => { it("maps claude-sonnet-4 to full version", async () => { const agent: OpenAgent = { frontmatter: { name: "Agent", description: "Test", mode: "primary", model: "claude-sonnet-4", }, metadata: { name: "Agent", category: "core", type: "agent" }, systemPrompt: "Prompt", contexts: [], }; const result = await adapter.fromOAC(agent); const config = JSON.parse(result.configs[0].content); expect(config.model).toBe("claude-sonnet-4-20250514"); }); it("preserves other model IDs", async () => { const agent: OpenAgent = { frontmatter: { name: "Agent", description: "Test", mode: "primary", model: "claude-opus-4", }, metadata: { name: "Agent", category: "core", type: "agent" }, systemPrompt: "Prompt", contexts: [], }; const result = await adapter.fromOAC(agent); const config = JSON.parse(result.configs[0].content); expect(config.model).toBe("claude-opus-4"); }); it("does not set model when not provided", async () => { const agent: OpenAgent = { frontmatter: { name: "Agent", description: "Test", mode: "primary", }, metadata: { name: "Agent", category: "core", type: "agent" }, systemPrompt: "Prompt", contexts: [], }; const result = await adapter.fromOAC(agent); const config = JSON.parse(result.configs[0].content); expect(config.model).toBeUndefined(); }); }); // ============================================================================ // TOOL MAPPING // ============================================================================ describe("tool mapping and parsing", () => { it("parses comma-separated tools string", async () => { const source = JSON.stringify({ name: "Agent", description: "Test", tools: "Read, Write, Edit, Bash", systemPrompt: "Prompt", }); const result = await adapter.toOAC(source); expect(result.frontmatter.tools).toEqual({ read: true, write: true, edit: true, bash: true, }); }); it("normalizes tool names to lowercase", async () => { const source = JSON.stringify({ name: "Agent", description: "Test", tools: ["Read", "WRITE", "BaSh"], systemPrompt: "Prompt", }); const result = await adapter.toOAC(source); expect(Object.keys(result.frontmatter.tools || {})).toContain("read"); expect(Object.keys(result.frontmatter.tools || {})).toContain("write"); expect(Object.keys(result.frontmatter.tools || {})).toContain("bash"); }); it("capitalizes tools for Claude format", async () => { const agent: OpenAgent = { frontmatter: { name: "Agent", description: "Test", mode: "primary", tools: { read: true, write: true, bash: true }, }, metadata: { name: "Agent", category: "core", type: "agent" }, systemPrompt: "Prompt", contexts: [], }; const result = await adapter.fromOAC(agent); const config = JSON.parse(result.configs[0].content); expect(config.tools).toEqual( expect.arrayContaining(["Read", "Write", "Bash"]) ); }); it("omits disabled tools", async () => { const agent: OpenAgent = { frontmatter: { name: "Agent", description: "Test", mode: "primary", tools: { read: true, write: false, bash: true }, }, metadata: { name: "Agent", category: "core", type: "agent" }, systemPrompt: "Prompt", contexts: [], }; const result = await adapter.fromOAC(agent); const config = JSON.parse(result.configs[0].content); expect(config.tools).not.toContain("Write"); expect(config.tools).toContain("Read"); }); }); // ============================================================================ // PERMISSION MAPPING // ============================================================================ describe("permission mapping", () => { it("maps all-allow permissions to bypassPermissions", async () => { const agent: OpenAgent = { frontmatter: { name: "Agent", description: "Test", mode: "primary", permission: { read: "allow", write: "allow", bash: true }, }, metadata: { name: "Agent", category: "core", type: "agent" }, systemPrompt: "Prompt", contexts: [], }; const result = await adapter.fromOAC(agent); const config = JSON.parse(result.configs[0].content); expect(config.permissionMode).toBe("bypassPermissions"); }); it("maps all-deny permissions to dontAsk", async () => { const agent: OpenAgent = { frontmatter: { name: "Agent", description: "Test", mode: "primary", permission: { read: "deny", write: "deny", bash: false }, }, metadata: { name: "Agent", category: "core", type: "agent" }, systemPrompt: "Prompt", contexts: [], }; const result = await adapter.fromOAC(agent); const config = JSON.parse(result.configs[0].content); expect(config.permissionMode).toBe("dontAsk"); }); it("maps ask permissions to default", async () => { const agent: OpenAgent = { frontmatter: { name: "Agent", description: "Test", mode: "primary", permission: { read: "ask", write: "allow" }, }, metadata: { name: "Agent", category: "core", type: "agent" }, systemPrompt: "Prompt", contexts: [], }; const result = await adapter.fromOAC(agent); const config = JSON.parse(result.configs[0].content); expect(config.permissionMode).toBe("default"); }); it("warns on mixed granular permissions", async () => { const agent: OpenAgent = { frontmatter: { name: "Agent", description: "Test", mode: "primary", permission: { read: "allow", "write.file1": "deny", }, }, metadata: { name: "Agent", category: "core", type: "agent" }, systemPrompt: "Prompt", contexts: [], }; const result = await adapter.fromOAC(agent); expect(result.warnings.some((w) => w.includes("permission"))).toBe(true); }); }); // ============================================================================ // SKILL MAPPING // ============================================================================ describe("skill parsing and mapping", () => { it("parses skills as comma-separated string", async () => { const source = JSON.stringify({ name: "Agent", description: "Test", skills: "skill1, skill2, skill3", systemPrompt: "Prompt", }); const result = await adapter.toOAC(source); expect(result.frontmatter.skills).toEqual(["skill1", "skill2", "skill3"]); }); it("parses skills as array", async () => { const source = JSON.stringify({ name: "Agent", description: "Test", skills: ["skill1", "skill2"], systemPrompt: "Prompt", }); const result = await adapter.toOAC(source); expect(result.frontmatter.skills).toEqual(["skill1", "skill2"]); }); it("handles empty skills gracefully", async () => { const source = JSON.stringify({ name: "Agent", description: "Test", skills: [], systemPrompt: "Prompt", }); const result = await adapter.toOAC(source); expect(result.frontmatter.skills).toEqual([]); }); it("includes skills in config.json", async () => { const agent: OpenAgent = { frontmatter: { name: "Agent", description: "Test", mode: "primary", skills: ["skill1", "skill2"], }, metadata: { name: "Agent", category: "core", type: "agent" }, systemPrompt: "Prompt", contexts: [], }; const result = await adapter.fromOAC(agent); const config = JSON.parse(result.configs[0].content); expect(config.skills).toEqual(["skill1", "skill2"]); }); it("handles skill objects with name property", async () => { const agent: OpenAgent = { frontmatter: { name: "Agent", description: "Test", mode: "primary", skills: [{ name: "skill1" }], }, metadata: { name: "Agent", category: "core", type: "agent" }, systemPrompt: "Prompt", contexts: [], }; const result = await adapter.fromOAC(agent); const config = JSON.parse(result.configs[0].content); expect(config.skills).toContain("skill1"); }); }); // ============================================================================ // HOOK MAPPING // ============================================================================ describe("hook parsing and mapping", () => { it("parses hooks with matchers", async () => { const source = JSON.stringify({ name: "Agent", description: "Test", hooks: { PreToolUse: [ { matcher: "*.txt", hooks: [{ type: "command", command: "validate" }], }, ], }, systemPrompt: "Prompt", }); const result = await adapter.toOAC(source); expect(result.frontmatter.hooks).toBeDefined(); expect(result.frontmatter.hooks![0].matchers).toEqual(["*.txt"]); }); it("maps hook commands correctly", async () => { const source = JSON.stringify({ name: "Agent", description: "Test", hooks: { PostToolUse: [ { matcher: "*", hooks: [ { type: "command", command: "log" }, { type: "command", command: "notify" }, ], }, ], }, systemPrompt: "Prompt", }); const result = await adapter.toOAC(source); expect(result.frontmatter.hooks![0].commands.length).toBe(2); }); it("converts hooks back to Claude format", async () => { const agent: OpenAgent = { frontmatter: { name: "Agent", description: "Test", mode: "primary", hooks: [ { event: "PreToolUse", matchers: ["*.txt"], commands: [{ type: "command", command: "validate" }], }, ], }, metadata: { name: "Agent", category: "core", type: "agent" }, systemPrompt: "Prompt", contexts: [], }; const result = await adapter.fromOAC(agent); const config = JSON.parse(result.configs[0].content); expect(config.hooks.PreToolUse).toBeDefined(); expect(config.hooks.PreToolUse[0].matcher).toBe("*.txt"); }); it("defaults matcher to wildcard", async () => { const agent: OpenAgent = { frontmatter: { name: "Agent", description: "Test", mode: "primary", hooks: [ { event: "AgentStart", commands: [{ type: "command", command: "init" }], }, ], }, metadata: { name: "Agent", category: "core", type: "agent" }, systemPrompt: "Prompt", contexts: [], }; const result = await adapter.fromOAC(agent); const config = JSON.parse(result.configs[0].content); expect(config.hooks.AgentStart[0].matcher).toBe("*"); }); }); // ============================================================================ // ROUNDTRIP CONVERSION // ============================================================================ describe("roundtrip conversion", () => { it("roundtrip primary agent config", async () => { // Start with OAC const original: OpenAgent = { frontmatter: { name: "TestAgent", description: "A test agent", mode: "primary", model: "claude-sonnet-4", tools: { read: true, write: true }, skills: ["skill1"], }, metadata: { name: "TestAgent", category: "core", type: "agent" }, systemPrompt: "You are helpful", contexts: [], }; // Convert to Claude const toClaudeResult = await adapter.fromOAC(original); const claudeConfig = JSON.parse(toClaudeResult.configs[0].content); // Convert back to OAC const backToOAC = await adapter.toOAC(JSON.stringify(claudeConfig)); // Verify core properties are preserved expect(backToOAC.frontmatter.name).toBe(original.frontmatter.name); expect(backToOAC.frontmatter.description).toBe( original.frontmatter.description ); expect(backToOAC.systemPrompt).toBe(original.systemPrompt); expect(backToOAC.frontmatter.tools).toEqual(original.frontmatter.tools); }); it("roundtrip preserves model through conversion", async () => { const original: OpenAgent = { frontmatter: { name: "Agent", description: "Test", mode: "primary", model: "claude-opus-4", }, metadata: { name: "Agent", category: "core", type: "agent" }, systemPrompt: "Prompt", contexts: [], }; const toClaudeResult = await adapter.fromOAC(original); const claudeConfig = JSON.parse(toClaudeResult.configs[0].content); const backToOAC = await adapter.toOAC(JSON.stringify(claudeConfig)); expect(backToOAC.frontmatter.model).toBe("claude-opus-4"); }); }); // ============================================================================ // EDGE CASES & ERROR HANDLING // ============================================================================ describe("edge cases and error handling", () => { it("omits empty tools from config", async () => { const agent: OpenAgent = { frontmatter: { name: "Agent", description: "Test", mode: "primary", tools: {}, }, metadata: { name: "Agent", category: "core", type: "agent" }, systemPrompt: "Prompt", contexts: [], }; const result = await adapter.fromOAC(agent); const config = JSON.parse(result.configs[0].content); // Empty tools should not be included in config expect(config.tools === undefined || config.tools?.length === 0).toBe(true); }); it("handles null system prompt", async () => { const source = JSON.stringify({ name: "Agent", description: "Test", systemPrompt: null, }); const result = await adapter.toOAC(source); expect(result.systemPrompt).toBe(""); }); it("handles empty system prompt", async () => { const agent: OpenAgent = { frontmatter: { name: "Agent", description: "Test", mode: "primary", }, metadata: { name: "Agent", category: "core", type: "agent" }, systemPrompt: "", contexts: [], }; const result = await adapter.fromOAC(agent); const config = JSON.parse(result.configs[0].content); expect(config.systemPrompt).toBe(""); }); it("handles special characters in names", async () => { const agent: OpenAgent = { frontmatter: { name: "Agent-With-Dashes_and_underscores", description: "Test with special chars: @#$", mode: "primary", }, metadata: { name: "Agent", category: "core", type: "agent" }, systemPrompt: "Prompt", contexts: [], }; const result = await adapter.fromOAC(agent); expect(result.success).toBe(true); const config = JSON.parse(result.configs[0].content); expect(config.name).toContain("-"); }); it("handles very long system prompt", async () => { const longPrompt = "A".repeat(5000); const agent: OpenAgent = { frontmatter: { name: "Agent", description: "Test", mode: "primary", }, metadata: { name: "Agent", category: "core", type: "agent" }, systemPrompt: longPrompt, contexts: [], }; const result = await adapter.fromOAC(agent); const config = JSON.parse(result.configs[0].content); expect(config.systemPrompt.length).toBe(5000); }); it("handles multiline YAML values in frontmatter", async () => { const source = `--- name: Agent description: Test description --- System prompt here`; const result = await adapter.toOAC(source); expect(result.frontmatter.name).toBe("Agent"); expect(result.systemPrompt).toBe("System prompt here"); }); }); // ============================================================================ // CONVERSION RESULT STRUCTURE // ============================================================================ describe("conversion result structure", () => { it("returns success true on valid conversion", async () => { const agent: OpenAgent = { frontmatter: { name: "Agent", description: "Test", mode: "primary", }, metadata: { name: "Agent", category: "core", type: "agent" }, systemPrompt: "Prompt", contexts: [], }; const result = await adapter.fromOAC(agent); expect(result.success).toBe(true); }); it("includes capabilities in result", async () => { const agent: OpenAgent = { frontmatter: { name: "Agent", description: "Test", mode: "primary", }, metadata: { name: "Agent", category: "core", type: "agent" }, systemPrompt: "Prompt", contexts: [], }; const result = await adapter.fromOAC(agent); expect(result.capabilities).toBeDefined(); expect(result.capabilities?.name).toBe("claude"); }); it("includes encoding in tool config", async () => { const agent: OpenAgent = { frontmatter: { name: "Agent", description: "Test", mode: "primary", }, metadata: { name: "Agent", category: "core", type: "agent" }, systemPrompt: "Prompt", contexts: [], }; const result = await adapter.fromOAC(agent); expect(result.configs[0].encoding).toBe("utf-8"); }); }); });