| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486 |
- 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>): 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>): 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<AgentFrontmatter>): 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");
- });
- });
- });
|