ClaudeAdapter.test.ts 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486
  1. import { describe, it, expect, beforeEach } from "vitest";
  2. import { ClaudeAdapter } from "../../../src/adapters/ClaudeAdapter";
  3. import type {
  4. OpenAgent,
  5. AgentFrontmatter,
  6. HookDefinition,
  7. SkillReference,
  8. } from "../../../src/types";
  9. /**
  10. * Unit tests for ClaudeAdapter with 80%+ coverage
  11. *
  12. * Test strategy:
  13. * 1. toOAC() - Parse Claude formats to OpenAgent
  14. * 2. fromOAC() - Convert OpenAgent to Claude formats
  15. * 3. getCapabilities() - Feature matrix validation
  16. * 4. validateConversion() - Validation and warnings
  17. * 5. Helper methods - Model/tool/hook/skill mapping
  18. * 6. Edge cases - Invalid input, missing fields, empty values
  19. * 7. Roundtrip - Data integrity checks
  20. */
  21. describe("ClaudeAdapter", () => {
  22. let adapter: ClaudeAdapter;
  23. beforeEach(() => {
  24. adapter = new ClaudeAdapter();
  25. });
  26. // ============================================================================
  27. // ADAPTER IDENTITY
  28. // ============================================================================
  29. describe("adapter identity", () => {
  30. it("has correct name", () => {
  31. expect(adapter.name).toBe("claude");
  32. });
  33. it("has correct displayName", () => {
  34. expect(adapter.displayName).toBe("Claude Code");
  35. });
  36. it("returns correct config path", () => {
  37. expect(adapter.getConfigPath()).toBe(".claude/");
  38. });
  39. });
  40. // ============================================================================
  41. // CAPABILITIES
  42. // ============================================================================
  43. describe("getCapabilities()", () => {
  44. it("returns correct capabilities object", () => {
  45. const capabilities = adapter.getCapabilities();
  46. expect(capabilities.name).toBe("claude");
  47. expect(capabilities.displayName).toBe("Claude Code");
  48. expect(capabilities.supportsMultipleAgents).toBe(true);
  49. expect(capabilities.supportsSkills).toBe(true);
  50. expect(capabilities.supportsHooks).toBe(true);
  51. expect(capabilities.supportsGranularPermissions).toBe(false);
  52. expect(capabilities.supportsContexts).toBe(true);
  53. expect(capabilities.supportsCustomModels).toBe(true);
  54. expect(capabilities.supportsTemperature).toBe(false);
  55. expect(capabilities.supportsMaxSteps).toBe(false);
  56. expect(capabilities.configFormat).toBe("markdown");
  57. expect(capabilities.outputStructure).toBe("directory");
  58. });
  59. it("includes appropriate notes", () => {
  60. const capabilities = adapter.getCapabilities();
  61. expect(capabilities.notes).toBeDefined();
  62. expect(capabilities.notes?.length).toBeGreaterThan(0);
  63. expect(capabilities.notes?.some((n) => n.includes("permissions"))).toBe(
  64. true
  65. );
  66. });
  67. });
  68. // ============================================================================
  69. // toOAC() - PARSING CLAUDE CONFIG.JSON
  70. // ============================================================================
  71. describe("toOAC() - parsing config.json", () => {
  72. it("parses minimal config.json", async () => {
  73. const source = JSON.stringify({
  74. name: "TestAgent",
  75. description: "Test description",
  76. systemPrompt: "You are helpful",
  77. });
  78. const result = await adapter.toOAC(source);
  79. expect(result.frontmatter.name).toBe("TestAgent");
  80. expect(result.frontmatter.description).toBe("Test description");
  81. expect(result.systemPrompt).toBe("You are helpful");
  82. expect(result.frontmatter.mode).toBe("primary");
  83. });
  84. it("parses config with model", async () => {
  85. const source = JSON.stringify({
  86. name: "Agent",
  87. description: "Test",
  88. model: "claude-sonnet-4-20250514",
  89. systemPrompt: "Prompt",
  90. });
  91. const result = await adapter.toOAC(source);
  92. expect(result.frontmatter.model).toBe("claude-sonnet-4");
  93. });
  94. it("parses config with tools array", async () => {
  95. const source = JSON.stringify({
  96. name: "Agent",
  97. description: "Test",
  98. tools: ["Read", "Write", "Bash"],
  99. systemPrompt: "Prompt",
  100. });
  101. const result = await adapter.toOAC(source);
  102. expect(result.frontmatter.tools).toEqual({
  103. read: true,
  104. write: true,
  105. bash: true,
  106. });
  107. });
  108. it("parses config with tools string", async () => {
  109. const source = JSON.stringify({
  110. name: "Agent",
  111. description: "Test",
  112. tools: "Read, Write, Edit",
  113. systemPrompt: "Prompt",
  114. });
  115. const result = await adapter.toOAC(source);
  116. expect(result.frontmatter.tools).toEqual({
  117. read: true,
  118. write: true,
  119. edit: true,
  120. });
  121. });
  122. it("parses config with skills", async () => {
  123. const source = JSON.stringify({
  124. name: "Agent",
  125. description: "Test",
  126. skills: ["skill1", "skill2"],
  127. systemPrompt: "Prompt",
  128. });
  129. const result = await adapter.toOAC(source);
  130. expect(result.frontmatter.skills).toEqual(["skill1", "skill2"]);
  131. });
  132. it("parses config with hooks", async () => {
  133. const source = JSON.stringify({
  134. name: "Agent",
  135. description: "Test",
  136. hooks: {
  137. PreToolUse: [
  138. {
  139. matcher: "*.txt",
  140. hooks: [{ type: "command", command: "validate" }],
  141. },
  142. ],
  143. },
  144. systemPrompt: "Prompt",
  145. });
  146. const result = await adapter.toOAC(source);
  147. expect(result.frontmatter.hooks).toBeDefined();
  148. expect(result.frontmatter.hooks?.length).toBe(1);
  149. expect(result.frontmatter.hooks?.[0].event).toBe("PreToolUse");
  150. });
  151. it("handles missing optional fields gracefully", async () => {
  152. const source = JSON.stringify({
  153. name: "MinimalAgent",
  154. description: "Minimal",
  155. });
  156. const result = await adapter.toOAC(source);
  157. expect(result.frontmatter.name).toBe("MinimalAgent");
  158. expect(result.systemPrompt).toBe("");
  159. expect(result.frontmatter.tools).toBeUndefined();
  160. expect(result.frontmatter.skills).toBeUndefined();
  161. });
  162. it("parses invalid JSON as markdown (subagent fallback)", async () => {
  163. const source = "not valid json";
  164. const result = await adapter.toOAC(source);
  165. // Falls back to markdown parsing
  166. expect(result.frontmatter.mode).toBe("subagent");
  167. expect(result.systemPrompt).toBe("not valid json");
  168. });
  169. it("parses non-object JSON string as markdown (subagent fallback)", async () => {
  170. const source = JSON.stringify("just a string");
  171. const result = await adapter.toOAC(source);
  172. // Falls back to markdown parsing
  173. expect(result.frontmatter.mode).toBe("subagent");
  174. });
  175. });
  176. // ============================================================================
  177. // toOAC() - PARSING CLAUDE AGENT.MD (SUBAGENTS)
  178. // ============================================================================
  179. describe("toOAC() - parsing agent.md with YAML frontmatter", () => {
  180. it("parses agent.md with minimal frontmatter", async () => {
  181. const source = `---
  182. name: SubAgent
  183. description: A subagent
  184. ---
  185. This is the system prompt for the agent.`;
  186. const result = await adapter.toOAC(source);
  187. expect(result.frontmatter.name).toBe("SubAgent");
  188. expect(result.frontmatter.description).toBe("A subagent");
  189. expect(result.frontmatter.mode).toBe("subagent");
  190. expect(result.systemPrompt).toBe("This is the system prompt for the agent.");
  191. });
  192. it("parses agent.md with model in frontmatter", async () => {
  193. const source = `---
  194. name: Agent
  195. description: Test
  196. model: claude-opus-4
  197. ---
  198. Prompt`;
  199. const result = await adapter.toOAC(source);
  200. expect(result.frontmatter.model).toBe("claude-opus-4");
  201. });
  202. it("parses agent.md with tools array in frontmatter", async () => {
  203. const source = `---
  204. name: Agent
  205. description: Test
  206. tools: ["Read", "Write", "Bash"]
  207. ---
  208. Prompt`;
  209. const result = await adapter.toOAC(source);
  210. expect(result.frontmatter.tools).toEqual({
  211. read: true,
  212. write: true,
  213. bash: true,
  214. });
  215. });
  216. it("parses agent.md with skills in frontmatter", async () => {
  217. const source = `---
  218. name: Agent
  219. description: Test
  220. skills: ["skill1", "skill2"]
  221. ---
  222. Prompt`;
  223. const result = await adapter.toOAC(source);
  224. expect(result.frontmatter.skills).toEqual(["skill1", "skill2"]);
  225. });
  226. it("handles agent.md without frontmatter as markdown content", async () => {
  227. const source = "No frontmatter here, just markdown content";
  228. const result = await adapter.toOAC(source);
  229. expect(result.systemPrompt).toBe("No frontmatter here, just markdown content");
  230. expect(result.frontmatter.mode).toBe("subagent");
  231. });
  232. it("preserves multiline system prompt", async () => {
  233. const source = `---
  234. name: Agent
  235. description: Test
  236. ---
  237. You are a helpful assistant.
  238. You should be friendly and professional.
  239. Always provide detailed responses.`;
  240. const result = await adapter.toOAC(source);
  241. expect(result.systemPrompt).toContain("You are a helpful assistant");
  242. expect(result.systemPrompt).toContain("Always provide detailed responses");
  243. });
  244. });
  245. // ============================================================================
  246. // fromOAC() - CONVERTING TO CLAUDE CONFIG.JSON
  247. // ============================================================================
  248. describe("fromOAC() - converting to config.json", () => {
  249. const createOpenAgent = (overrides?: Partial<OpenAgent>): OpenAgent => ({
  250. frontmatter: {
  251. name: "TestAgent",
  252. description: "Test agent",
  253. mode: "primary",
  254. model: "claude-sonnet-4",
  255. tools: { read: true, write: true },
  256. ...overrides?.frontmatter,
  257. },
  258. metadata: {
  259. name: "TestAgent",
  260. category: "core",
  261. type: "agent",
  262. },
  263. systemPrompt: "You are helpful",
  264. contexts: [],
  265. ...overrides,
  266. });
  267. it("converts primary agent to config.json", async () => {
  268. const agent = createOpenAgent();
  269. const result = await adapter.fromOAC(agent);
  270. expect(result.success).toBe(true);
  271. expect(result.configs).toHaveLength(1);
  272. expect(result.configs[0].fileName).toBe(".claude/config.json");
  273. const config = JSON.parse(result.configs[0].content);
  274. expect(config.name).toBe("TestAgent");
  275. expect(config.description).toBe("Test agent");
  276. expect(config.model).toBe("claude-sonnet-4-20250514");
  277. });
  278. it("maps tools correctly in config.json", async () => {
  279. const agent = createOpenAgent({
  280. frontmatter: {
  281. tools: { read: true, write: true, bash: true },
  282. },
  283. });
  284. const result = await adapter.fromOAC(agent);
  285. const config = JSON.parse(result.configs[0].content);
  286. expect(config.tools).toEqual(["Read", "Write", "Bash"]);
  287. });
  288. it("includes skills in config.json", async () => {
  289. const agent = createOpenAgent({
  290. frontmatter: {
  291. skills: ["skill1", "skill2"],
  292. },
  293. });
  294. const result = await adapter.fromOAC(agent);
  295. const config = JSON.parse(result.configs[0].content);
  296. expect(config.skills).toEqual(["skill1", "skill2"]);
  297. });
  298. it("includes hooks in config.json", async () => {
  299. const hook: HookDefinition = {
  300. event: "PreToolUse",
  301. matchers: ["*.txt"],
  302. commands: [{ type: "command", command: "validate" }],
  303. };
  304. const agent = createOpenAgent({
  305. frontmatter: {
  306. hooks: [hook],
  307. },
  308. });
  309. const result = await adapter.fromOAC(agent);
  310. const config = JSON.parse(result.configs[0].content);
  311. expect(config.hooks).toBeDefined();
  312. expect(config.hooks.PreToolUse).toBeDefined();
  313. });
  314. it("warns when temperature is set (unsupported)", async () => {
  315. const agent = createOpenAgent({
  316. frontmatter: {
  317. temperature: 0.7,
  318. },
  319. });
  320. const result = await adapter.fromOAC(agent);
  321. expect(result.warnings.some((w) => w.includes("temperature"))).toBe(true);
  322. });
  323. it("warns when maxSteps is set (unsupported)", async () => {
  324. const agent = createOpenAgent({
  325. frontmatter: {
  326. maxSteps: 10,
  327. },
  328. });
  329. const result = await adapter.fromOAC(agent);
  330. expect(result.warnings.some((w) => w.includes("maxSteps"))).toBe(true);
  331. });
  332. it("includes validationWarnings in result", async () => {
  333. const agent = createOpenAgent({
  334. frontmatter: {
  335. name: "",
  336. description: "",
  337. },
  338. });
  339. const result = await adapter.fromOAC(agent);
  340. expect(result.warnings.length).toBeGreaterThan(0);
  341. });
  342. it("handles mixed permission rules gracefully", async () => {
  343. const agent = createOpenAgent({
  344. frontmatter: {
  345. permission: {
  346. read: "allow",
  347. write: "ask",
  348. bash: "deny",
  349. },
  350. },
  351. });
  352. const result = await adapter.fromOAC(agent);
  353. // Should include some form of warning or degradation
  354. expect(result.warnings.length).toBeGreaterThanOrEqual(0);
  355. expect(result.success).toBe(true);
  356. });
  357. });
  358. // ============================================================================
  359. // fromOAC() - CONVERTING TO CLAUDE AGENT.MD (SUBAGENTS)
  360. // ============================================================================
  361. describe("fromOAC() - converting to agent.md for subagents", () => {
  362. const createSubagent = (overrides?: Partial<OpenAgent>): OpenAgent => ({
  363. frontmatter: {
  364. name: "CodeAnalyzer",
  365. description: "Analyzes code",
  366. mode: "subagent",
  367. model: "claude-sonnet-4",
  368. ...overrides?.frontmatter,
  369. },
  370. metadata: {
  371. name: "CodeAnalyzer",
  372. category: "specialist",
  373. type: "subagent",
  374. },
  375. systemPrompt: "Analyze code quality",
  376. contexts: [],
  377. ...overrides,
  378. });
  379. it("converts subagent to agent.md file", async () => {
  380. const agent = createSubagent();
  381. const result = await adapter.fromOAC(agent);
  382. expect(result.success).toBe(true);
  383. expect(result.configs).toHaveLength(1);
  384. expect(result.configs[0].fileName).toBe(".claude/agents/CodeAnalyzer.md");
  385. });
  386. it("generates proper YAML frontmatter in agent.md", async () => {
  387. const agent = createSubagent();
  388. const result = await adapter.fromOAC(agent);
  389. const content = result.configs[0].content;
  390. expect(content).toMatch(/^---/);
  391. expect(content).toMatch(/name: "CodeAnalyzer"/);
  392. expect(content).toMatch(/description: "Analyzes code"/);
  393. expect(content).toContain("---\n\n");
  394. });
  395. it("includes system prompt in agent.md body", async () => {
  396. const agent = createSubagent({
  397. systemPrompt: "Analyze code quality\nCheck for best practices",
  398. });
  399. const result = await adapter.fromOAC(agent);
  400. const content = result.configs[0].content;
  401. expect(content).toContain("Analyze code quality");
  402. expect(content).toContain("Check for best practices");
  403. });
  404. it("includes tools in agent.md frontmatter", async () => {
  405. const agent = createSubagent({
  406. frontmatter: {
  407. tools: { read: true, bash: true },
  408. },
  409. });
  410. const result = await adapter.fromOAC(agent);
  411. const content = result.configs[0].content;
  412. expect(content).toContain("Read");
  413. expect(content).toContain("Bash");
  414. expect(content).toContain("tools");
  415. });
  416. it("includes model in agent.md frontmatter", async () => {
  417. const agent = createSubagent({
  418. frontmatter: {
  419. model: "claude-opus-4",
  420. },
  421. });
  422. const result = await adapter.fromOAC(agent);
  423. const content = result.configs[0].content;
  424. expect(content).toContain("model");
  425. expect(content).toContain("claude-opus-4");
  426. });
  427. it("includes permission mode in agent.md frontmatter", async () => {
  428. const agent = createSubagent({
  429. frontmatter: {
  430. permission: { read: "allow", write: "allow" },
  431. },
  432. });
  433. const result = await adapter.fromOAC(agent);
  434. const content = result.configs[0].content;
  435. expect(content).toContain("permissionMode");
  436. expect(content).toContain("bypassPermissions");
  437. });
  438. });
  439. // ============================================================================
  440. // fromOAC() - SKILLS GENERATION FROM CONTEXTS
  441. // ============================================================================
  442. describe("fromOAC() - generating skills from contexts", () => {
  443. it("generates skill files from contexts", async () => {
  444. const agent: OpenAgent = {
  445. frontmatter: {
  446. name: "Agent",
  447. description: "Test",
  448. mode: "primary",
  449. },
  450. metadata: {
  451. name: "Agent",
  452. category: "core",
  453. type: "agent",
  454. },
  455. systemPrompt: "Prompt",
  456. contexts: [
  457. {
  458. path: ".opencode/context/skills/python-best-practices.md",
  459. description: "Python coding standards",
  460. },
  461. ],
  462. };
  463. const result = await adapter.fromOAC(agent);
  464. expect(result.configs.length).toBeGreaterThan(1);
  465. const skillConfig = result.configs.find((c) =>
  466. c.fileName.includes(".claude/skills/")
  467. );
  468. expect(skillConfig).toBeDefined();
  469. expect(skillConfig?.fileName).toMatch(/\.claude\/skills\/.*\/SKILL\.md/);
  470. });
  471. it("generates correct skill name from context path", async () => {
  472. const agent: OpenAgent = {
  473. frontmatter: {
  474. name: "Agent",
  475. description: "Test",
  476. mode: "primary",
  477. },
  478. metadata: {
  479. name: "Agent",
  480. category: "core",
  481. type: "agent",
  482. },
  483. systemPrompt: "Prompt",
  484. contexts: [
  485. {
  486. path: "docs/React Hooks Guide.md",
  487. description: "React hooks documentation",
  488. },
  489. ],
  490. };
  491. const result = await adapter.fromOAC(agent);
  492. const skillConfig = result.configs.find((c) =>
  493. c.fileName.includes(".claude/skills/")
  494. );
  495. expect(skillConfig?.fileName).toMatch(/react-hooks-guide/);
  496. });
  497. it("includes context priority in skill content", async () => {
  498. const agent: OpenAgent = {
  499. frontmatter: {
  500. name: "Agent",
  501. description: "Test",
  502. mode: "primary",
  503. },
  504. metadata: {
  505. name: "Agent",
  506. category: "core",
  507. type: "agent",
  508. },
  509. systemPrompt: "Prompt",
  510. contexts: [
  511. {
  512. path: "context/important.md",
  513. priority: "high",
  514. description: "Important context",
  515. },
  516. ],
  517. };
  518. const result = await adapter.fromOAC(agent);
  519. const skillConfig = result.configs.find((c) =>
  520. c.fileName.includes(".claude/skills/")
  521. );
  522. expect(skillConfig?.content).toContain("Priority: high");
  523. });
  524. it("generates multiple skills from multiple contexts", async () => {
  525. const agent: OpenAgent = {
  526. frontmatter: {
  527. name: "Agent",
  528. description: "Test",
  529. mode: "primary",
  530. },
  531. metadata: {
  532. name: "Agent",
  533. category: "core",
  534. type: "agent",
  535. },
  536. systemPrompt: "Prompt",
  537. contexts: [
  538. { path: "context1.md", description: "First" },
  539. { path: "context2.md", description: "Second" },
  540. { path: "context3.md", description: "Third" },
  541. ],
  542. };
  543. const result = await adapter.fromOAC(agent);
  544. const skillConfigs = result.configs.filter((c) =>
  545. c.fileName.includes(".claude/skills/")
  546. );
  547. expect(skillConfigs).toHaveLength(3);
  548. });
  549. it("includes skill metadata when context lacks description", async () => {
  550. const agent: OpenAgent = {
  551. frontmatter: {
  552. name: "Agent",
  553. description: "Test",
  554. mode: "primary",
  555. },
  556. metadata: {
  557. name: "Agent",
  558. category: "core",
  559. type: "agent",
  560. },
  561. systemPrompt: "Prompt",
  562. contexts: [{ path: ".opencode/context/styles.md" }],
  563. };
  564. const result = await adapter.fromOAC(agent);
  565. const skillConfig = result.configs.find((c) =>
  566. c.fileName.includes(".claude/skills/")
  567. );
  568. expect(skillConfig?.content).toContain("Context from");
  569. expect(skillConfig?.content).toContain("styles.md");
  570. });
  571. it("handles skill names with special characters", async () => {
  572. const agent: OpenAgent = {
  573. frontmatter: {
  574. name: "Agent",
  575. description: "Test",
  576. mode: "primary",
  577. },
  578. metadata: {
  579. name: "Agent",
  580. category: "core",
  581. type: "agent",
  582. },
  583. systemPrompt: "Prompt",
  584. contexts: [
  585. { path: "docs/Type Script Advanced Rules.md", description: "Test" },
  586. ],
  587. };
  588. const result = await adapter.fromOAC(agent);
  589. const skillConfig = result.configs.find((c) =>
  590. c.fileName.includes(".claude/skills/")
  591. );
  592. expect(skillConfig).toBeDefined();
  593. // Should have lowercase and dashed name from path
  594. expect(skillConfig?.fileName).toMatch(/type-script-advanced-rules/);
  595. });
  596. });
  597. // ============================================================================
  598. // VALIDATION
  599. // ============================================================================
  600. describe("validateConversion()", () => {
  601. const createAgent = (overrides?: Partial<AgentFrontmatter>): OpenAgent => ({
  602. frontmatter: {
  603. name: "Agent",
  604. description: "Test",
  605. mode: "primary",
  606. ...overrides,
  607. },
  608. metadata: { name: "Agent", category: "core", type: "agent" },
  609. systemPrompt: "Prompt",
  610. contexts: [],
  611. });
  612. it("returns no warnings for valid agent", () => {
  613. const agent = createAgent();
  614. const warnings = adapter.validateConversion(agent);
  615. expect(warnings).toHaveLength(0);
  616. });
  617. it("warns when name is missing", () => {
  618. const agent = createAgent({ name: "" });
  619. const warnings = adapter.validateConversion(agent);
  620. expect(warnings.some((w) => w.includes("name"))).toBe(true);
  621. });
  622. it("warns when description is missing", () => {
  623. const agent = createAgent({ description: "" });
  624. const warnings = adapter.validateConversion(agent);
  625. expect(warnings.some((w) => w.includes("description"))).toBe(true);
  626. });
  627. it("warns about granular permission degradation", () => {
  628. const agent = createAgent({
  629. permission: {
  630. read: { "file1.txt": "allow", "file2.txt": "deny" },
  631. },
  632. });
  633. const warnings = adapter.validateConversion(agent);
  634. expect(
  635. warnings.some((w) => w.includes("granular permissions"))
  636. ).toBe(true);
  637. });
  638. it("does not warn about simple permission rules", () => {
  639. const agent = createAgent({
  640. permission: { read: "allow", write: "allow" },
  641. });
  642. const warnings = adapter.validateConversion(agent);
  643. expect(
  644. warnings.some((w) => w.includes("granular permissions"))
  645. ).toBe(false);
  646. });
  647. });
  648. // ============================================================================
  649. // MODEL MAPPING
  650. // ============================================================================
  651. describe("model mapping (Claude to OAC)", () => {
  652. it("maps claude-sonnet-4-20250514 to claude-sonnet-4", async () => {
  653. const source = JSON.stringify({
  654. name: "Agent",
  655. description: "Test",
  656. model: "claude-sonnet-4-20250514",
  657. systemPrompt: "Prompt",
  658. });
  659. const result = await adapter.toOAC(source);
  660. expect(result.frontmatter.model).toBe("claude-sonnet-4");
  661. });
  662. it("maps short model names", async () => {
  663. const source = JSON.stringify({
  664. name: "Agent",
  665. description: "Test",
  666. model: "opus",
  667. systemPrompt: "Prompt",
  668. });
  669. const result = await adapter.toOAC(source);
  670. expect(result.frontmatter.model).toBe("claude-opus-4");
  671. });
  672. it("preserves unknown models", async () => {
  673. const source = JSON.stringify({
  674. name: "Agent",
  675. description: "Test",
  676. model: "claude-custom-model",
  677. systemPrompt: "Prompt",
  678. });
  679. const result = await adapter.toOAC(source);
  680. expect(result.frontmatter.model).toBe("claude-custom-model");
  681. });
  682. it("handles missing model gracefully", async () => {
  683. const source = JSON.stringify({
  684. name: "Agent",
  685. description: "Test",
  686. systemPrompt: "Prompt",
  687. });
  688. const result = await adapter.toOAC(source);
  689. expect(result.frontmatter.model).toBeUndefined();
  690. });
  691. });
  692. // ============================================================================
  693. // MODEL MAPPING (OAC to Claude)
  694. // ============================================================================
  695. describe("model mapping (OAC to Claude)", () => {
  696. it("maps claude-sonnet-4 to full version", async () => {
  697. const agent: OpenAgent = {
  698. frontmatter: {
  699. name: "Agent",
  700. description: "Test",
  701. mode: "primary",
  702. model: "claude-sonnet-4",
  703. },
  704. metadata: { name: "Agent", category: "core", type: "agent" },
  705. systemPrompt: "Prompt",
  706. contexts: [],
  707. };
  708. const result = await adapter.fromOAC(agent);
  709. const config = JSON.parse(result.configs[0].content);
  710. expect(config.model).toBe("claude-sonnet-4-20250514");
  711. });
  712. it("preserves other model IDs", async () => {
  713. const agent: OpenAgent = {
  714. frontmatter: {
  715. name: "Agent",
  716. description: "Test",
  717. mode: "primary",
  718. model: "claude-opus-4",
  719. },
  720. metadata: { name: "Agent", category: "core", type: "agent" },
  721. systemPrompt: "Prompt",
  722. contexts: [],
  723. };
  724. const result = await adapter.fromOAC(agent);
  725. const config = JSON.parse(result.configs[0].content);
  726. expect(config.model).toBe("claude-opus-4");
  727. });
  728. it("does not set model when not provided", async () => {
  729. const agent: OpenAgent = {
  730. frontmatter: {
  731. name: "Agent",
  732. description: "Test",
  733. mode: "primary",
  734. },
  735. metadata: { name: "Agent", category: "core", type: "agent" },
  736. systemPrompt: "Prompt",
  737. contexts: [],
  738. };
  739. const result = await adapter.fromOAC(agent);
  740. const config = JSON.parse(result.configs[0].content);
  741. expect(config.model).toBeUndefined();
  742. });
  743. });
  744. // ============================================================================
  745. // TOOL MAPPING
  746. // ============================================================================
  747. describe("tool mapping and parsing", () => {
  748. it("parses comma-separated tools string", async () => {
  749. const source = JSON.stringify({
  750. name: "Agent",
  751. description: "Test",
  752. tools: "Read, Write, Edit, Bash",
  753. systemPrompt: "Prompt",
  754. });
  755. const result = await adapter.toOAC(source);
  756. expect(result.frontmatter.tools).toEqual({
  757. read: true,
  758. write: true,
  759. edit: true,
  760. bash: true,
  761. });
  762. });
  763. it("normalizes tool names to lowercase", async () => {
  764. const source = JSON.stringify({
  765. name: "Agent",
  766. description: "Test",
  767. tools: ["Read", "WRITE", "BaSh"],
  768. systemPrompt: "Prompt",
  769. });
  770. const result = await adapter.toOAC(source);
  771. expect(Object.keys(result.frontmatter.tools || {})).toContain("read");
  772. expect(Object.keys(result.frontmatter.tools || {})).toContain("write");
  773. expect(Object.keys(result.frontmatter.tools || {})).toContain("bash");
  774. });
  775. it("capitalizes tools for Claude format", async () => {
  776. const agent: OpenAgent = {
  777. frontmatter: {
  778. name: "Agent",
  779. description: "Test",
  780. mode: "primary",
  781. tools: { read: true, write: true, bash: true },
  782. },
  783. metadata: { name: "Agent", category: "core", type: "agent" },
  784. systemPrompt: "Prompt",
  785. contexts: [],
  786. };
  787. const result = await adapter.fromOAC(agent);
  788. const config = JSON.parse(result.configs[0].content);
  789. expect(config.tools).toEqual(
  790. expect.arrayContaining(["Read", "Write", "Bash"])
  791. );
  792. });
  793. it("omits disabled tools", async () => {
  794. const agent: OpenAgent = {
  795. frontmatter: {
  796. name: "Agent",
  797. description: "Test",
  798. mode: "primary",
  799. tools: { read: true, write: false, bash: true },
  800. },
  801. metadata: { name: "Agent", category: "core", type: "agent" },
  802. systemPrompt: "Prompt",
  803. contexts: [],
  804. };
  805. const result = await adapter.fromOAC(agent);
  806. const config = JSON.parse(result.configs[0].content);
  807. expect(config.tools).not.toContain("Write");
  808. expect(config.tools).toContain("Read");
  809. });
  810. });
  811. // ============================================================================
  812. // PERMISSION MAPPING
  813. // ============================================================================
  814. describe("permission mapping", () => {
  815. it("maps all-allow permissions to bypassPermissions", async () => {
  816. const agent: OpenAgent = {
  817. frontmatter: {
  818. name: "Agent",
  819. description: "Test",
  820. mode: "primary",
  821. permission: { read: "allow", write: "allow", bash: true },
  822. },
  823. metadata: { name: "Agent", category: "core", type: "agent" },
  824. systemPrompt: "Prompt",
  825. contexts: [],
  826. };
  827. const result = await adapter.fromOAC(agent);
  828. const config = JSON.parse(result.configs[0].content);
  829. expect(config.permissionMode).toBe("bypassPermissions");
  830. });
  831. it("maps all-deny permissions to dontAsk", async () => {
  832. const agent: OpenAgent = {
  833. frontmatter: {
  834. name: "Agent",
  835. description: "Test",
  836. mode: "primary",
  837. permission: { read: "deny", write: "deny", bash: false },
  838. },
  839. metadata: { name: "Agent", category: "core", type: "agent" },
  840. systemPrompt: "Prompt",
  841. contexts: [],
  842. };
  843. const result = await adapter.fromOAC(agent);
  844. const config = JSON.parse(result.configs[0].content);
  845. expect(config.permissionMode).toBe("dontAsk");
  846. });
  847. it("maps ask permissions to default", async () => {
  848. const agent: OpenAgent = {
  849. frontmatter: {
  850. name: "Agent",
  851. description: "Test",
  852. mode: "primary",
  853. permission: { read: "ask", write: "allow" },
  854. },
  855. metadata: { name: "Agent", category: "core", type: "agent" },
  856. systemPrompt: "Prompt",
  857. contexts: [],
  858. };
  859. const result = await adapter.fromOAC(agent);
  860. const config = JSON.parse(result.configs[0].content);
  861. expect(config.permissionMode).toBe("default");
  862. });
  863. it("warns on mixed granular permissions", async () => {
  864. const agent: OpenAgent = {
  865. frontmatter: {
  866. name: "Agent",
  867. description: "Test",
  868. mode: "primary",
  869. permission: {
  870. read: "allow",
  871. "write.file1": "deny",
  872. },
  873. },
  874. metadata: { name: "Agent", category: "core", type: "agent" },
  875. systemPrompt: "Prompt",
  876. contexts: [],
  877. };
  878. const result = await adapter.fromOAC(agent);
  879. expect(result.warnings.some((w) => w.includes("permission"))).toBe(true);
  880. });
  881. });
  882. // ============================================================================
  883. // SKILL MAPPING
  884. // ============================================================================
  885. describe("skill parsing and mapping", () => {
  886. it("parses skills as comma-separated string", async () => {
  887. const source = JSON.stringify({
  888. name: "Agent",
  889. description: "Test",
  890. skills: "skill1, skill2, skill3",
  891. systemPrompt: "Prompt",
  892. });
  893. const result = await adapter.toOAC(source);
  894. expect(result.frontmatter.skills).toEqual(["skill1", "skill2", "skill3"]);
  895. });
  896. it("parses skills as array", async () => {
  897. const source = JSON.stringify({
  898. name: "Agent",
  899. description: "Test",
  900. skills: ["skill1", "skill2"],
  901. systemPrompt: "Prompt",
  902. });
  903. const result = await adapter.toOAC(source);
  904. expect(result.frontmatter.skills).toEqual(["skill1", "skill2"]);
  905. });
  906. it("handles empty skills gracefully", async () => {
  907. const source = JSON.stringify({
  908. name: "Agent",
  909. description: "Test",
  910. skills: [],
  911. systemPrompt: "Prompt",
  912. });
  913. const result = await adapter.toOAC(source);
  914. expect(result.frontmatter.skills).toEqual([]);
  915. });
  916. it("includes skills in config.json", async () => {
  917. const agent: OpenAgent = {
  918. frontmatter: {
  919. name: "Agent",
  920. description: "Test",
  921. mode: "primary",
  922. skills: ["skill1", "skill2"],
  923. },
  924. metadata: { name: "Agent", category: "core", type: "agent" },
  925. systemPrompt: "Prompt",
  926. contexts: [],
  927. };
  928. const result = await adapter.fromOAC(agent);
  929. const config = JSON.parse(result.configs[0].content);
  930. expect(config.skills).toEqual(["skill1", "skill2"]);
  931. });
  932. it("handles skill objects with name property", async () => {
  933. const agent: OpenAgent = {
  934. frontmatter: {
  935. name: "Agent",
  936. description: "Test",
  937. mode: "primary",
  938. skills: [{ name: "skill1" }],
  939. },
  940. metadata: { name: "Agent", category: "core", type: "agent" },
  941. systemPrompt: "Prompt",
  942. contexts: [],
  943. };
  944. const result = await adapter.fromOAC(agent);
  945. const config = JSON.parse(result.configs[0].content);
  946. expect(config.skills).toContain("skill1");
  947. });
  948. });
  949. // ============================================================================
  950. // HOOK MAPPING
  951. // ============================================================================
  952. describe("hook parsing and mapping", () => {
  953. it("parses hooks with matchers", async () => {
  954. const source = JSON.stringify({
  955. name: "Agent",
  956. description: "Test",
  957. hooks: {
  958. PreToolUse: [
  959. {
  960. matcher: "*.txt",
  961. hooks: [{ type: "command", command: "validate" }],
  962. },
  963. ],
  964. },
  965. systemPrompt: "Prompt",
  966. });
  967. const result = await adapter.toOAC(source);
  968. expect(result.frontmatter.hooks).toBeDefined();
  969. expect(result.frontmatter.hooks![0].matchers).toEqual(["*.txt"]);
  970. });
  971. it("maps hook commands correctly", async () => {
  972. const source = JSON.stringify({
  973. name: "Agent",
  974. description: "Test",
  975. hooks: {
  976. PostToolUse: [
  977. {
  978. matcher: "*",
  979. hooks: [
  980. { type: "command", command: "log" },
  981. { type: "command", command: "notify" },
  982. ],
  983. },
  984. ],
  985. },
  986. systemPrompt: "Prompt",
  987. });
  988. const result = await adapter.toOAC(source);
  989. expect(result.frontmatter.hooks![0].commands.length).toBe(2);
  990. });
  991. it("converts hooks back to Claude format", async () => {
  992. const agent: OpenAgent = {
  993. frontmatter: {
  994. name: "Agent",
  995. description: "Test",
  996. mode: "primary",
  997. hooks: [
  998. {
  999. event: "PreToolUse",
  1000. matchers: ["*.txt"],
  1001. commands: [{ type: "command", command: "validate" }],
  1002. },
  1003. ],
  1004. },
  1005. metadata: { name: "Agent", category: "core", type: "agent" },
  1006. systemPrompt: "Prompt",
  1007. contexts: [],
  1008. };
  1009. const result = await adapter.fromOAC(agent);
  1010. const config = JSON.parse(result.configs[0].content);
  1011. expect(config.hooks.PreToolUse).toBeDefined();
  1012. expect(config.hooks.PreToolUse[0].matcher).toBe("*.txt");
  1013. });
  1014. it("defaults matcher to wildcard", async () => {
  1015. const agent: OpenAgent = {
  1016. frontmatter: {
  1017. name: "Agent",
  1018. description: "Test",
  1019. mode: "primary",
  1020. hooks: [
  1021. {
  1022. event: "AgentStart",
  1023. commands: [{ type: "command", command: "init" }],
  1024. },
  1025. ],
  1026. },
  1027. metadata: { name: "Agent", category: "core", type: "agent" },
  1028. systemPrompt: "Prompt",
  1029. contexts: [],
  1030. };
  1031. const result = await adapter.fromOAC(agent);
  1032. const config = JSON.parse(result.configs[0].content);
  1033. expect(config.hooks.AgentStart[0].matcher).toBe("*");
  1034. });
  1035. });
  1036. // ============================================================================
  1037. // ROUNDTRIP CONVERSION
  1038. // ============================================================================
  1039. describe("roundtrip conversion", () => {
  1040. it("roundtrip primary agent config", async () => {
  1041. // Start with OAC
  1042. const original: OpenAgent = {
  1043. frontmatter: {
  1044. name: "TestAgent",
  1045. description: "A test agent",
  1046. mode: "primary",
  1047. model: "claude-sonnet-4",
  1048. tools: { read: true, write: true },
  1049. skills: ["skill1"],
  1050. },
  1051. metadata: { name: "TestAgent", category: "core", type: "agent" },
  1052. systemPrompt: "You are helpful",
  1053. contexts: [],
  1054. };
  1055. // Convert to Claude
  1056. const toClaudeResult = await adapter.fromOAC(original);
  1057. const claudeConfig = JSON.parse(toClaudeResult.configs[0].content);
  1058. // Convert back to OAC
  1059. const backToOAC = await adapter.toOAC(JSON.stringify(claudeConfig));
  1060. // Verify core properties are preserved
  1061. expect(backToOAC.frontmatter.name).toBe(original.frontmatter.name);
  1062. expect(backToOAC.frontmatter.description).toBe(
  1063. original.frontmatter.description
  1064. );
  1065. expect(backToOAC.systemPrompt).toBe(original.systemPrompt);
  1066. expect(backToOAC.frontmatter.tools).toEqual(original.frontmatter.tools);
  1067. });
  1068. it("roundtrip preserves model through conversion", async () => {
  1069. const original: OpenAgent = {
  1070. frontmatter: {
  1071. name: "Agent",
  1072. description: "Test",
  1073. mode: "primary",
  1074. model: "claude-opus-4",
  1075. },
  1076. metadata: { name: "Agent", category: "core", type: "agent" },
  1077. systemPrompt: "Prompt",
  1078. contexts: [],
  1079. };
  1080. const toClaudeResult = await adapter.fromOAC(original);
  1081. const claudeConfig = JSON.parse(toClaudeResult.configs[0].content);
  1082. const backToOAC = await adapter.toOAC(JSON.stringify(claudeConfig));
  1083. expect(backToOAC.frontmatter.model).toBe("claude-opus-4");
  1084. });
  1085. });
  1086. // ============================================================================
  1087. // EDGE CASES & ERROR HANDLING
  1088. // ============================================================================
  1089. describe("edge cases and error handling", () => {
  1090. it("omits empty tools from config", async () => {
  1091. const agent: OpenAgent = {
  1092. frontmatter: {
  1093. name: "Agent",
  1094. description: "Test",
  1095. mode: "primary",
  1096. tools: {},
  1097. },
  1098. metadata: { name: "Agent", category: "core", type: "agent" },
  1099. systemPrompt: "Prompt",
  1100. contexts: [],
  1101. };
  1102. const result = await adapter.fromOAC(agent);
  1103. const config = JSON.parse(result.configs[0].content);
  1104. // Empty tools should not be included in config
  1105. expect(config.tools === undefined || config.tools?.length === 0).toBe(true);
  1106. });
  1107. it("handles null system prompt", async () => {
  1108. const source = JSON.stringify({
  1109. name: "Agent",
  1110. description: "Test",
  1111. systemPrompt: null,
  1112. });
  1113. const result = await adapter.toOAC(source);
  1114. expect(result.systemPrompt).toBe("");
  1115. });
  1116. it("handles empty system prompt", async () => {
  1117. const agent: OpenAgent = {
  1118. frontmatter: {
  1119. name: "Agent",
  1120. description: "Test",
  1121. mode: "primary",
  1122. },
  1123. metadata: { name: "Agent", category: "core", type: "agent" },
  1124. systemPrompt: "",
  1125. contexts: [],
  1126. };
  1127. const result = await adapter.fromOAC(agent);
  1128. const config = JSON.parse(result.configs[0].content);
  1129. expect(config.systemPrompt).toBe("");
  1130. });
  1131. it("handles special characters in names", async () => {
  1132. const agent: OpenAgent = {
  1133. frontmatter: {
  1134. name: "Agent-With-Dashes_and_underscores",
  1135. description: "Test with special chars: @#$",
  1136. mode: "primary",
  1137. },
  1138. metadata: { name: "Agent", category: "core", type: "agent" },
  1139. systemPrompt: "Prompt",
  1140. contexts: [],
  1141. };
  1142. const result = await adapter.fromOAC(agent);
  1143. expect(result.success).toBe(true);
  1144. const config = JSON.parse(result.configs[0].content);
  1145. expect(config.name).toContain("-");
  1146. });
  1147. it("handles very long system prompt", async () => {
  1148. const longPrompt = "A".repeat(5000);
  1149. const agent: OpenAgent = {
  1150. frontmatter: {
  1151. name: "Agent",
  1152. description: "Test",
  1153. mode: "primary",
  1154. },
  1155. metadata: { name: "Agent", category: "core", type: "agent" },
  1156. systemPrompt: longPrompt,
  1157. contexts: [],
  1158. };
  1159. const result = await adapter.fromOAC(agent);
  1160. const config = JSON.parse(result.configs[0].content);
  1161. expect(config.systemPrompt.length).toBe(5000);
  1162. });
  1163. it("handles multiline YAML values in frontmatter", async () => {
  1164. const source = `---
  1165. name: Agent
  1166. description: Test description
  1167. ---
  1168. System prompt here`;
  1169. const result = await adapter.toOAC(source);
  1170. expect(result.frontmatter.name).toBe("Agent");
  1171. expect(result.systemPrompt).toBe("System prompt here");
  1172. });
  1173. });
  1174. // ============================================================================
  1175. // CONVERSION RESULT STRUCTURE
  1176. // ============================================================================
  1177. describe("conversion result structure", () => {
  1178. it("returns success true on valid conversion", async () => {
  1179. const agent: OpenAgent = {
  1180. frontmatter: {
  1181. name: "Agent",
  1182. description: "Test",
  1183. mode: "primary",
  1184. },
  1185. metadata: { name: "Agent", category: "core", type: "agent" },
  1186. systemPrompt: "Prompt",
  1187. contexts: [],
  1188. };
  1189. const result = await adapter.fromOAC(agent);
  1190. expect(result.success).toBe(true);
  1191. });
  1192. it("includes capabilities in result", async () => {
  1193. const agent: OpenAgent = {
  1194. frontmatter: {
  1195. name: "Agent",
  1196. description: "Test",
  1197. mode: "primary",
  1198. },
  1199. metadata: { name: "Agent", category: "core", type: "agent" },
  1200. systemPrompt: "Prompt",
  1201. contexts: [],
  1202. };
  1203. const result = await adapter.fromOAC(agent);
  1204. expect(result.capabilities).toBeDefined();
  1205. expect(result.capabilities?.name).toBe("claude");
  1206. });
  1207. it("includes encoding in tool config", async () => {
  1208. const agent: OpenAgent = {
  1209. frontmatter: {
  1210. name: "Agent",
  1211. description: "Test",
  1212. mode: "primary",
  1213. },
  1214. metadata: { name: "Agent", category: "core", type: "agent" },
  1215. systemPrompt: "Prompt",
  1216. contexts: [],
  1217. };
  1218. const result = await adapter.fromOAC(agent);
  1219. expect(result.configs[0].encoding).toBe("utf-8");
  1220. });
  1221. });
  1222. });