CursorAdapter.test.ts 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244
  1. import { describe, it, expect, beforeEach } from "vitest";
  2. import { CursorAdapter } from "../../../src/adapters/CursorAdapter";
  3. import type { OpenAgent, AgentFrontmatter } from "../../../src/types";
  4. /**
  5. * Unit tests for CursorAdapter with 80%+ coverage
  6. *
  7. * Test strategy:
  8. * 1. toOAC() - Parse .cursorrules to OpenAgent
  9. * 2. fromOAC() - Convert OpenAgent to .cursorrules
  10. * 3. getCapabilities() - Feature matrix validation
  11. * 4. validateConversion() - Validation and warnings
  12. * 5. Helper methods - Model/tool mapping
  13. * 6. Agent Merging - Multiple agents to single file (CURSOR-SPECIFIC)
  14. * 7. Edge cases - Invalid input, missing fields, empty values
  15. * 8. Roundtrip - Data integrity checks
  16. */
  17. describe("CursorAdapter", () => {
  18. let adapter: CursorAdapter;
  19. beforeEach(() => {
  20. adapter = new CursorAdapter();
  21. });
  22. // ============================================================================
  23. // ADAPTER IDENTITY
  24. // ============================================================================
  25. describe("adapter identity", () => {
  26. it("has correct name", () => {
  27. expect(adapter.name).toBe("cursor");
  28. });
  29. it("has correct displayName", () => {
  30. expect(adapter.displayName).toBe("Cursor IDE");
  31. });
  32. it("returns correct config path", () => {
  33. expect(adapter.getConfigPath()).toBe(".cursorrules");
  34. });
  35. });
  36. // ============================================================================
  37. // CAPABILITIES
  38. // ============================================================================
  39. describe("getCapabilities()", () => {
  40. it("returns correct capabilities object", () => {
  41. const capabilities = adapter.getCapabilities();
  42. expect(capabilities.name).toBe("cursor");
  43. expect(capabilities.displayName).toBe("Cursor IDE");
  44. expect(capabilities.supportsMultipleAgents).toBe(false);
  45. expect(capabilities.supportsSkills).toBe(false);
  46. expect(capabilities.supportsHooks).toBe(false);
  47. expect(capabilities.supportsGranularPermissions).toBe(false);
  48. // Was `true` until 2026-07-15, pinning the wrong side of a live contradiction: the
  49. // adapter hand-wrote `supportsContexts: true` ("✅ Can inline context") while the matrix
  50. // said `externalContext: "none"` ("context must be inline in .cursorrules"). This
  51. // assertion is why that survived — it froze the adapter's answer instead of comparing it
  52. // to the matrix's. Ruled for the matrix: inlining delivers the bytes but loses the
  53. // reference, the file boundary and the priority, so it is degradation, not support.
  54. // The comparison this test should have been making now lives in capability-agreement.test.ts.
  55. expect(capabilities.supportsContexts).toBe(false);
  56. expect(capabilities.supportsCustomModels).toBe(true);
  57. expect(capabilities.supportsTemperature).toBe(true);
  58. expect(capabilities.supportsMaxSteps).toBe(false);
  59. expect(capabilities.configFormat).toBe("plain");
  60. expect(capabilities.outputStructure).toBe("single-file");
  61. });
  62. it("includes appropriate notes", () => {
  63. const capabilities = adapter.getCapabilities();
  64. expect(capabilities.notes).toBeDefined();
  65. expect(capabilities.notes && capabilities.notes.length > 0).toBe(true);
  66. });
  67. });
  68. // ============================================================================
  69. // toOAC() - PARSING .cursorrules (PLAIN TEXT)
  70. // ============================================================================
  71. describe("toOAC() - parsing plain .cursorrules", () => {
  72. it("parses plain text .cursorrules", async () => {
  73. const source = `You are a helpful assistant.
  74. Do not delete files.
  75. Always confirm before bash commands.`;
  76. const result = await adapter.toOAC(source);
  77. expect(result.frontmatter.name).toBe("cursor-agent");
  78. expect(result.systemPrompt).toBe(source);
  79. expect(result.frontmatter.mode).toBe("primary");
  80. });
  81. it("handles very simple rules", async () => {
  82. const source = "Be helpful.";
  83. const result = await adapter.toOAC(source);
  84. expect(result.systemPrompt).toBe("Be helpful.");
  85. expect(result.frontmatter.name).toBe("cursor-agent");
  86. });
  87. it("preserves multiline plain text", async () => {
  88. const source = `Line 1
  89. Line 2
  90. Line 3
  91. Line 4`;
  92. const result = await adapter.toOAC(source);
  93. expect(result.systemPrompt).toContain("Line 1");
  94. expect(result.systemPrompt).toContain("Line 4");
  95. });
  96. });
  97. // ============================================================================
  98. // toOAC() - PARSING .cursorrules WITH YAML FRONTMATTER
  99. // ============================================================================
  100. describe("toOAC() - parsing with YAML frontmatter", () => {
  101. it("parses .cursorrules with name in frontmatter", async () => {
  102. const source = `---
  103. name: my-agent
  104. ---
  105. Rules here`;
  106. const result = await adapter.toOAC(source);
  107. expect(result.frontmatter.name).toBe("my-agent");
  108. expect(result.systemPrompt).toBe("Rules here");
  109. });
  110. it("parses model in frontmatter", async () => {
  111. const source = `---
  112. name: analyzer
  113. model: gpt-4
  114. ---
  115. Analyze code`;
  116. const result = await adapter.toOAC(source);
  117. expect(result.frontmatter.model).toBe("gpt-4");
  118. });
  119. it("parses temperature in frontmatter", async () => {
  120. const source = `---
  121. name: creative
  122. temperature: 0.8
  123. ---
  124. Be creative`;
  125. const result = await adapter.toOAC(source);
  126. expect(result.frontmatter.temperature).toBe(0.8);
  127. });
  128. it("parses tools as comma-separated string", async () => {
  129. const source = `---
  130. name: agent
  131. tools: read, write, bash
  132. ---
  133. Prompt`;
  134. const result = await adapter.toOAC(source);
  135. expect(result.frontmatter.tools).toEqual({
  136. read: true,
  137. write: true,
  138. bash: true,
  139. });
  140. });
  141. it("parses tools as array", async () => {
  142. const source = `---
  143. name: agent
  144. tools: [read, write]
  145. ---
  146. Prompt`;
  147. // Note: Simple YAML parser doesn't handle arrays in the implementation
  148. // This tests current behavior
  149. const result = await adapter.toOAC(source);
  150. expect(result.frontmatter.tools).toBeDefined();
  151. });
  152. it("parses description in frontmatter", async () => {
  153. const source = `---
  154. name: agent
  155. description: A test agent
  156. ---
  157. Content`;
  158. const result = await adapter.toOAC(source);
  159. expect(result.systemPrompt).toContain("Content");
  160. });
  161. it("handles numeric values in frontmatter", async () => {
  162. const source = `---
  163. name: agent
  164. temperature: 0.5
  165. max_tokens: 2000
  166. ---
  167. Prompt`;
  168. const result = await adapter.toOAC(source);
  169. expect(result.frontmatter.temperature).toBe(0.5);
  170. expect(typeof result.frontmatter.temperature).toBe("number");
  171. });
  172. it("handles quoted values in frontmatter", async () => {
  173. const source = `---
  174. name: "quoted-agent"
  175. model: "gpt-4"
  176. ---
  177. Rules`;
  178. const result = await adapter.toOAC(source);
  179. expect(result.frontmatter.name).toBe("quoted-agent");
  180. expect(result.frontmatter.model).toBe("gpt-4");
  181. });
  182. it("handles boolean values in frontmatter", async () => {
  183. const source = `---
  184. name: agent
  185. some_flag: true
  186. ---
  187. Content`;
  188. const result = await adapter.toOAC(source);
  189. expect(result.systemPrompt).toContain("Content");
  190. });
  191. it("preserves multiline content after frontmatter", async () => {
  192. const source = `---
  193. name: agent
  194. ---
  195. Line 1
  196. Line 2
  197. Line 3`;
  198. const result = await adapter.toOAC(source);
  199. expect(result.systemPrompt).toContain("Line 1");
  200. expect(result.systemPrompt).toContain("Line 3");
  201. });
  202. it("handles empty body with frontmatter", async () => {
  203. const source = `---
  204. name: agent
  205. model: gpt-4
  206. ---
  207. `;
  208. const result = await adapter.toOAC(source);
  209. expect(result.frontmatter.name).toBe("agent");
  210. });
  211. });
  212. // ============================================================================
  213. // fromOAC() - GENERATING .cursorrules
  214. // ============================================================================
  215. describe("fromOAC() - generating .cursorrules", () => {
  216. const createAgent = (overrides?: Partial<OpenAgent>): OpenAgent => ({
  217. frontmatter: {
  218. name: "test-agent",
  219. description: "A test agent",
  220. mode: "primary",
  221. ...overrides?.frontmatter,
  222. },
  223. metadata: {
  224. name: "test-agent",
  225. category: "core",
  226. type: "agent",
  227. },
  228. systemPrompt: "You are helpful",
  229. contexts: [],
  230. ...overrides,
  231. });
  232. it("converts agent to .cursorrules file", async () => {
  233. const agent = createAgent();
  234. const result = await adapter.fromOAC(agent);
  235. expect(result.success).toBe(true);
  236. expect(result.configs).toHaveLength(1);
  237. expect(result.configs[0].fileName).toBe(".cursorrules");
  238. });
  239. it("includes agent name in frontmatter", async () => {
  240. const agent = createAgent();
  241. const result = await adapter.fromOAC(agent);
  242. const content = result.configs[0].content;
  243. expect(content).toContain("name: test-agent");
  244. });
  245. it("includes system prompt in body", async () => {
  246. const agent = createAgent({
  247. systemPrompt: "Be helpful and kind",
  248. });
  249. const result = await adapter.fromOAC(agent);
  250. const content = result.configs[0].content;
  251. expect(content).toContain("Be helpful and kind");
  252. });
  253. it("includes model in frontmatter", async () => {
  254. const agent = createAgent({
  255. frontmatter: {
  256. model: "gpt-4",
  257. },
  258. });
  259. const result = await adapter.fromOAC(agent);
  260. const content = result.configs[0].content;
  261. expect(content).toContain("model: gpt-4");
  262. });
  263. it("includes temperature in frontmatter", async () => {
  264. const agent = createAgent({
  265. frontmatter: {
  266. temperature: 0.7,
  267. },
  268. });
  269. const result = await adapter.fromOAC(agent);
  270. const content = result.configs[0].content;
  271. expect(content).toContain("temperature: 0.7");
  272. });
  273. it("includes description in frontmatter when agent has one", async () => {
  274. const agent = createAgent({
  275. frontmatter: {
  276. name: "analyzer",
  277. description: "Code analyzer",
  278. },
  279. });
  280. const result = await adapter.fromOAC(agent);
  281. const content = result.configs[0].content;
  282. // Description should be in frontmatter only if other frontmatter exists
  283. expect(content).toBeDefined();
  284. expect(result.success).toBe(true);
  285. });
  286. it("warns on unsupported skills", async () => {
  287. const agent = createAgent({
  288. frontmatter: {
  289. skills: ["skill1", "skill2"],
  290. },
  291. });
  292. const result = await adapter.fromOAC(agent);
  293. expect(result.warnings.some((w) => w.includes("skills"))).toBe(true);
  294. });
  295. it("warns on unsupported hooks", async () => {
  296. const agent = createAgent({
  297. frontmatter: {
  298. hooks: [
  299. { event: "PreToolUse", commands: [{ type: "command", command: "log" }] },
  300. ],
  301. },
  302. });
  303. const result = await adapter.fromOAC(agent);
  304. expect(result.warnings.some((w) => w.includes("hooks"))).toBe(true);
  305. });
  306. it("warns on unsupported maxSteps", async () => {
  307. const agent = createAgent({
  308. frontmatter: {
  309. maxSteps: 10,
  310. },
  311. });
  312. const result = await adapter.fromOAC(agent);
  313. expect(result.warnings.some((w) => w.includes("maxSteps"))).toBe(true);
  314. });
  315. it("warns on mixed permissions", async () => {
  316. const agent = createAgent({
  317. frontmatter: {
  318. permission: {
  319. read: { "*.txt": "allow", "*.js": "deny" },
  320. write: "ask",
  321. },
  322. },
  323. });
  324. const result = await adapter.fromOAC(agent);
  325. // Should warn about granular permissions if they're detected
  326. expect(result.warnings.length >= 0).toBe(true);
  327. expect(result.success).toBe(true);
  328. });
  329. it("handles agent with tools", async () => {
  330. const agent = createAgent({
  331. frontmatter: {
  332. tools: { read: true, write: true, bash: true },
  333. },
  334. });
  335. const result = await adapter.fromOAC(agent);
  336. const content = result.configs[0].content;
  337. expect(content).toContain("read");
  338. expect(content).toContain("write");
  339. expect(content).toContain("bash");
  340. });
  341. it("generates frontmatter only when needed", async () => {
  342. const agent: OpenAgent = {
  343. frontmatter: {
  344. mode: "primary",
  345. },
  346. metadata: {
  347. category: "core",
  348. type: "agent",
  349. },
  350. systemPrompt: "Rules here",
  351. contexts: [],
  352. };
  353. const result = await adapter.fromOAC(agent);
  354. const content = result.configs[0].content;
  355. // If no frontmatter data, should not include frontmatter section
  356. expect(content).toBe("Rules here");
  357. });
  358. });
  359. // ============================================================================
  360. // fromOAC() - CONTEXT INLINING
  361. // ============================================================================
  362. describe("fromOAC() - context inlining", () => {
  363. it("inlines single context", async () => {
  364. const agent: OpenAgent = {
  365. frontmatter: {
  366. name: "agent",
  367. description: "Test",
  368. mode: "primary",
  369. },
  370. metadata: { name: "agent", category: "core", type: "agent" },
  371. systemPrompt: "Prompt",
  372. contexts: [
  373. {
  374. path: ".opencode/context/python-style.md",
  375. description: "Python style guide",
  376. },
  377. ],
  378. };
  379. const result = await adapter.fromOAC(agent);
  380. const content = result.configs[0].content;
  381. expect(content).toContain("# Context Files");
  382. expect(content).toContain("python-style.md");
  383. expect(content).toContain("Python style guide");
  384. });
  385. it("inlines multiple contexts", async () => {
  386. const agent: OpenAgent = {
  387. frontmatter: {
  388. name: "agent",
  389. description: "Test",
  390. mode: "primary",
  391. },
  392. metadata: { name: "agent", category: "core", type: "agent" },
  393. systemPrompt: "Prompt",
  394. contexts: [
  395. { path: "context1.md", description: "First" },
  396. { path: "context2.md", description: "Second" },
  397. { path: "context3.md", description: "Third" },
  398. ],
  399. };
  400. const result = await adapter.fromOAC(agent);
  401. const content = result.configs[0].content;
  402. expect(content).toContain("context1.md");
  403. expect(content).toContain("context2.md");
  404. expect(content).toContain("context3.md");
  405. });
  406. it("includes context priority information in inlined content", async () => {
  407. const agent: OpenAgent = {
  408. frontmatter: {
  409. name: "agent",
  410. description: "Test",
  411. mode: "primary",
  412. },
  413. metadata: { name: "agent", category: "core", type: "agent" },
  414. systemPrompt: "Prompt",
  415. contexts: [
  416. {
  417. path: "important.md",
  418. priority: "critical",
  419. description: "Critical context",
  420. },
  421. ],
  422. };
  423. const result = await adapter.fromOAC(agent);
  424. const content = result.configs[0].content;
  425. // Priority should be mentioned in the content
  426. expect(content).toContain("critical");
  427. expect(content).toContain("important.md");
  428. });
  429. it("generates warning for inlined contexts", async () => {
  430. const agent: OpenAgent = {
  431. frontmatter: {
  432. name: "agent",
  433. description: "Test",
  434. mode: "primary",
  435. },
  436. metadata: { name: "agent", category: "core", type: "agent" },
  437. systemPrompt: "Prompt",
  438. contexts: [{ path: "context.md", description: "Test" }],
  439. };
  440. const result = await adapter.fromOAC(agent);
  441. expect(result.warnings.some((w) => w.includes("context"))).toBe(true);
  442. });
  443. it("handles contexts without description", async () => {
  444. const agent: OpenAgent = {
  445. frontmatter: {
  446. name: "agent",
  447. description: "Test",
  448. mode: "primary",
  449. },
  450. metadata: { name: "agent", category: "core", type: "agent" },
  451. systemPrompt: "Prompt",
  452. contexts: [{ path: "context.md" }],
  453. };
  454. const result = await adapter.fromOAC(agent);
  455. const content = result.configs[0].content;
  456. expect(content).toContain("context.md");
  457. });
  458. it("includes tool access information", async () => {
  459. const agent: OpenAgent = {
  460. frontmatter: {
  461. name: "agent",
  462. description: "Test",
  463. mode: "primary",
  464. tools: { read: true, write: true, bash: false },
  465. },
  466. metadata: { name: "agent", category: "core", type: "agent" },
  467. systemPrompt: "Prompt",
  468. contexts: [],
  469. };
  470. const result = await adapter.fromOAC(agent);
  471. const content = result.configs[0].content;
  472. expect(content).toContain("# Tool Access");
  473. expect(content).toContain("read");
  474. expect(content).toContain("write");
  475. expect(content).not.toContain("- bash");
  476. });
  477. });
  478. // ============================================================================
  479. // VALIDATION
  480. // ============================================================================
  481. describe("validateConversion()", () => {
  482. const createAgent = (overrides?: Partial<AgentFrontmatter>): OpenAgent => ({
  483. frontmatter: {
  484. name: "agent",
  485. mode: "primary",
  486. ...overrides,
  487. },
  488. metadata: { category: "core", type: "agent" },
  489. systemPrompt: "Prompt",
  490. contexts: [],
  491. });
  492. it("returns no warnings for valid agent", () => {
  493. const agent = createAgent();
  494. const warnings = adapter.validateConversion(agent);
  495. expect(warnings).toHaveLength(0);
  496. });
  497. it("warns when name is missing", () => {
  498. const agent = createAgent({ name: "" });
  499. const warnings = adapter.validateConversion(agent);
  500. expect(warnings.some((w) => w.includes("name"))).toBe(true);
  501. });
  502. it("warns about subagent mode", () => {
  503. const agent = createAgent({ mode: "subagent" });
  504. const warnings = adapter.validateConversion(agent);
  505. expect(warnings.some((w) => w.includes("subagent"))).toBe(true);
  506. });
  507. it("accepts primary mode", () => {
  508. const agent = createAgent({ mode: "primary" });
  509. const warnings = adapter.validateConversion(agent);
  510. expect(warnings.length).toBe(0);
  511. });
  512. });
  513. // ============================================================================
  514. // AGENT MERGING (CURSOR-SPECIFIC)
  515. // ============================================================================
  516. describe("mergeAgents() - merging multiple agents", () => {
  517. const createAgent = (name: string, prompt: string): OpenAgent => ({
  518. frontmatter: {
  519. name,
  520. description: `Agent ${name}`,
  521. mode: "primary",
  522. tools: { read: true },
  523. },
  524. metadata: { name, category: "core", type: "agent" },
  525. systemPrompt: prompt,
  526. contexts: [],
  527. });
  528. it("returns cloned agent for single agent", () => {
  529. const agent = createAgent("single", "Single agent prompt");
  530. const result = adapter.mergeAgents([agent]);
  531. expect(result.frontmatter.name).toBe("single");
  532. expect(result.systemPrompt).toBe("Single agent prompt");
  533. });
  534. it("merges two agents", () => {
  535. const agents = [
  536. createAgent("agent1", "First prompt"),
  537. createAgent("agent2", "Second prompt"),
  538. ];
  539. const result = adapter.mergeAgents(agents);
  540. expect(result.frontmatter.name).toContain("agent1");
  541. expect(result.frontmatter.name).toContain("agent2");
  542. expect(result.systemPrompt).toContain("First prompt");
  543. expect(result.systemPrompt).toContain("Second prompt");
  544. });
  545. it("merges three agents", () => {
  546. const agents = [
  547. createAgent("a1", "P1"),
  548. createAgent("a2", "P2"),
  549. createAgent("a3", "P3"),
  550. ];
  551. const result = adapter.mergeAgents(agents);
  552. expect(result.systemPrompt).toContain("P1");
  553. expect(result.systemPrompt).toContain("P2");
  554. expect(result.systemPrompt).toContain("P3");
  555. });
  556. it("uses union of tools from all agents", () => {
  557. const agents = [
  558. {
  559. ...createAgent("a1", "P1"),
  560. frontmatter: { ...createAgent("a1", "P1").frontmatter, tools: { read: true, write: false } },
  561. },
  562. {
  563. ...createAgent("a2", "P2"),
  564. frontmatter: { ...createAgent("a2", "P2").frontmatter, tools: { write: true, bash: true } },
  565. },
  566. ];
  567. const result = adapter.mergeAgents(agents);
  568. expect(result.frontmatter.tools?.read).toBe(true);
  569. expect(result.frontmatter.tools?.write).toBe(true);
  570. expect(result.frontmatter.tools?.bash).toBe(true);
  571. });
  572. it("uses maximum temperature", () => {
  573. const agents = [
  574. { ...createAgent("a1", "P1"), frontmatter: { ...createAgent("a1", "P1").frontmatter, temperature: 0.5 } },
  575. { ...createAgent("a2", "P2"), frontmatter: { ...createAgent("a2", "P2").frontmatter, temperature: 0.8 } },
  576. { ...createAgent("a3", "P3"), frontmatter: { ...createAgent("a3", "P3").frontmatter, temperature: 0.6 } },
  577. ];
  578. const result = adapter.mergeAgents(agents);
  579. expect(result.frontmatter.temperature).toBe(0.8);
  580. });
  581. it("uses first available model", () => {
  582. const agents = [
  583. { ...createAgent("a1", "P1"), frontmatter: { ...createAgent("a1", "P1").frontmatter, model: undefined } },
  584. { ...createAgent("a2", "P2"), frontmatter: { ...createAgent("a2", "P2").frontmatter, model: "gpt-4" } },
  585. { ...createAgent("a3", "P3"), frontmatter: { ...createAgent("a3", "P3").frontmatter, model: "gpt-3.5" } },
  586. ];
  587. const result = adapter.mergeAgents(agents);
  588. expect(result.frontmatter.model).toBe("gpt-4");
  589. });
  590. it("flattens contexts from all agents", () => {
  591. const agents = [
  592. { ...createAgent("a1", "P1"), contexts: [{ path: "ctx1.md" }] },
  593. { ...createAgent("a2", "P2"), contexts: [{ path: "ctx2.md" }, { path: "ctx3.md" }] },
  594. ];
  595. const result = adapter.mergeAgents(agents);
  596. expect(result.contexts).toHaveLength(3);
  597. expect(result.contexts?.map((c) => c.path)).toContain("ctx1.md");
  598. });
  599. it("throws error on empty array", () => {
  600. expect(() => adapter.mergeAgents([])).toThrow("Cannot merge empty agent array");
  601. });
  602. it("throws error on undefined agent", () => {
  603. expect(() => adapter.mergeAgents([undefined as any])).toThrow("Agent at index 0 is undefined");
  604. });
  605. it("includes divider between prompts", () => {
  606. const agents = [
  607. createAgent("a1", "First"),
  608. createAgent("a2", "Second"),
  609. ];
  610. const result = adapter.mergeAgents(agents);
  611. expect(result.systemPrompt).toContain("---");
  612. });
  613. it("includes agent descriptions in merged prompt", () => {
  614. const agents = [
  615. { ...createAgent("a1", "First"), frontmatter: { ...createAgent("a1", "First").frontmatter, description: "First agent description" } },
  616. { ...createAgent("a2", "Second"), frontmatter: { ...createAgent("a2", "Second").frontmatter, description: "Second agent description" } },
  617. ];
  618. const result = adapter.mergeAgents(agents);
  619. expect(result.systemPrompt).toContain("First agent description");
  620. expect(result.systemPrompt).toContain("Second agent description");
  621. });
  622. });
  623. // ============================================================================
  624. // MODEL MAPPING
  625. // ============================================================================
  626. describe("model mapping (Cursor to OAC)", () => {
  627. it("maps gpt-4 to gpt-4", async () => {
  628. const source = `---
  629. name: agent
  630. model: gpt-4
  631. ---
  632. Prompt`;
  633. const result = await adapter.toOAC(source);
  634. expect(result.frontmatter.model).toBe("gpt-4");
  635. });
  636. it("maps gpt-4o to gpt-4o", async () => {
  637. const source = `---
  638. name: agent
  639. model: gpt-4o
  640. ---
  641. Prompt`;
  642. const result = await adapter.toOAC(source);
  643. expect(result.frontmatter.model).toBe("gpt-4o");
  644. });
  645. it("maps claude-3-opus to claude-opus-3", async () => {
  646. const source = `---
  647. name: agent
  648. model: claude-3-opus
  649. ---
  650. Prompt`;
  651. const result = await adapter.toOAC(source);
  652. expect(result.frontmatter.model).toBe("claude-opus-3");
  653. });
  654. it("preserves unknown models", async () => {
  655. const source = `---
  656. name: agent
  657. model: custom-model
  658. ---
  659. Prompt`;
  660. const result = await adapter.toOAC(source);
  661. expect(result.frontmatter.model).toBe("custom-model");
  662. });
  663. });
  664. describe("model mapping (OAC to Cursor)", () => {
  665. it("maps gpt-4 to gpt-4", async () => {
  666. const agent: OpenAgent = {
  667. frontmatter: {
  668. name: "agent",
  669. mode: "primary",
  670. model: "gpt-4",
  671. },
  672. metadata: { category: "core", type: "agent" },
  673. systemPrompt: "Prompt",
  674. contexts: [],
  675. };
  676. const result = await adapter.fromOAC(agent);
  677. const content = result.configs[0].content;
  678. expect(content).toContain("model: gpt-4");
  679. });
  680. it("maps gpt-4o to gpt-4o", async () => {
  681. const agent: OpenAgent = {
  682. frontmatter: {
  683. name: "agent",
  684. mode: "primary",
  685. model: "gpt-4o",
  686. },
  687. metadata: { category: "core", type: "agent" },
  688. systemPrompt: "Prompt",
  689. contexts: [],
  690. };
  691. const result = await adapter.fromOAC(agent);
  692. const content = result.configs[0].content;
  693. expect(content).toContain("model: gpt-4o");
  694. });
  695. it("maps claude-sonnet-4 to claude-3-sonnet (v3 fallback)", async () => {
  696. const agent: OpenAgent = {
  697. frontmatter: {
  698. name: "agent",
  699. mode: "primary",
  700. model: "claude-sonnet-4",
  701. },
  702. metadata: { category: "core", type: "agent" },
  703. systemPrompt: "Prompt",
  704. contexts: [],
  705. };
  706. const result = await adapter.fromOAC(agent);
  707. const content = result.configs[0].content;
  708. expect(content).toContain("model: claude-3-sonnet");
  709. });
  710. it("defaults to gpt-4 for unknown models", async () => {
  711. const agent: OpenAgent = {
  712. frontmatter: {
  713. name: "agent",
  714. mode: "primary",
  715. model: "unknown-model",
  716. },
  717. metadata: { category: "core", type: "agent" },
  718. systemPrompt: "Prompt",
  719. contexts: [],
  720. };
  721. const result = await adapter.fromOAC(agent);
  722. const content = result.configs[0].content;
  723. expect(content).toContain("model: gpt-4");
  724. });
  725. it("omits model when not provided", async () => {
  726. const agent: OpenAgent = {
  727. frontmatter: {
  728. name: "agent",
  729. mode: "primary",
  730. },
  731. metadata: { category: "core", type: "agent" },
  732. systemPrompt: "Prompt",
  733. contexts: [],
  734. };
  735. const result = await adapter.fromOAC(agent);
  736. const content = result.configs[0].content;
  737. expect(content).not.toContain("model:");
  738. });
  739. });
  740. // ============================================================================
  741. // TOOL PARSING
  742. // ============================================================================
  743. describe("tool parsing", () => {
  744. it("parses comma-separated tools", async () => {
  745. const source = `---
  746. name: agent
  747. tools: read, write, bash
  748. ---
  749. Prompt`;
  750. const result = await adapter.toOAC(source);
  751. expect(result.frontmatter.tools).toEqual({
  752. read: true,
  753. write: true,
  754. bash: true,
  755. });
  756. });
  757. it("normalizes tools to lowercase", async () => {
  758. const source = `---
  759. name: agent
  760. tools: Read, WRITE, BaSh
  761. ---
  762. Prompt`;
  763. const result = await adapter.toOAC(source);
  764. expect(Object.keys(result.frontmatter.tools || {})).toContain("read");
  765. expect(Object.keys(result.frontmatter.tools || {})).toContain("write");
  766. });
  767. it("handles empty tools", async () => {
  768. const source = `---
  769. name: agent
  770. tools:
  771. ---
  772. Prompt`;
  773. const result = await adapter.toOAC(source);
  774. expect(result.frontmatter.tools).toBeUndefined();
  775. });
  776. it("omits disabled tools from output", async () => {
  777. const agent: OpenAgent = {
  778. frontmatter: {
  779. name: "agent",
  780. mode: "primary",
  781. tools: { read: true, write: false, bash: true },
  782. },
  783. metadata: { category: "core", type: "agent" },
  784. systemPrompt: "Prompt",
  785. contexts: [],
  786. };
  787. const result = await adapter.fromOAC(agent);
  788. const content = result.configs[0].content;
  789. expect(content).toContain("- read");
  790. expect(content).toContain("- bash");
  791. expect(content).not.toContain("- write");
  792. });
  793. });
  794. // ============================================================================
  795. // ROUNDTRIP & EDGE CASES
  796. // ============================================================================
  797. describe("roundtrip conversion", () => {
  798. it("preserves agent properties through roundtrip", async () => {
  799. const original: OpenAgent = {
  800. frontmatter: {
  801. name: "roundtrip-agent",
  802. description: "Test roundtrip",
  803. mode: "primary",
  804. model: "gpt-4",
  805. temperature: 0.6,
  806. },
  807. metadata: { name: "roundtrip-agent", category: "core", type: "agent" },
  808. systemPrompt: "Be helpful",
  809. contexts: [],
  810. };
  811. // Convert to Cursor
  812. const cursorResult = await adapter.fromOAC(original);
  813. const cursorRules = cursorResult.configs[0].content;
  814. // Convert back to OAC
  815. const backToOAC = await adapter.toOAC(cursorRules);
  816. expect(backToOAC.frontmatter.name).toBe("roundtrip-agent");
  817. expect(backToOAC.systemPrompt).toContain("Be helpful");
  818. expect(backToOAC.frontmatter.temperature).toBe(0.6);
  819. });
  820. });
  821. describe("edge cases", () => {
  822. it("handles empty system prompt", async () => {
  823. const agent: OpenAgent = {
  824. frontmatter: {
  825. name: "agent",
  826. mode: "primary",
  827. },
  828. metadata: { category: "core", type: "agent" },
  829. systemPrompt: "",
  830. contexts: [],
  831. };
  832. const result = await adapter.fromOAC(agent);
  833. expect(result.success).toBe(true);
  834. expect(result.configs[0].content).toBeDefined();
  835. });
  836. it("handles very long system prompt", async () => {
  837. const longPrompt = "A".repeat(5000);
  838. const agent: OpenAgent = {
  839. frontmatter: {
  840. name: "agent",
  841. mode: "primary",
  842. },
  843. metadata: { category: "core", type: "agent" },
  844. systemPrompt: longPrompt,
  845. contexts: [],
  846. };
  847. const result = await adapter.fromOAC(agent);
  848. const content = result.configs[0].content;
  849. expect(content.length).toBeGreaterThan(5000);
  850. });
  851. it("handles special characters in names", async () => {
  852. const agent: OpenAgent = {
  853. frontmatter: {
  854. name: "agent-with-dashes_and_underscores",
  855. mode: "primary",
  856. },
  857. metadata: { category: "core", type: "agent" },
  858. systemPrompt: "Prompt",
  859. contexts: [],
  860. };
  861. const result = await adapter.fromOAC(agent);
  862. const content = result.configs[0].content;
  863. expect(content).toContain("agent-with-dashes_and_underscores");
  864. });
  865. it("handles null context array", async () => {
  866. const agent: OpenAgent = {
  867. frontmatter: {
  868. name: "agent",
  869. mode: "primary",
  870. },
  871. metadata: { category: "core", type: "agent" },
  872. systemPrompt: "Prompt",
  873. contexts: null as any,
  874. };
  875. const result = await adapter.fromOAC(agent);
  876. expect(result.success).toBe(true);
  877. });
  878. it("handles undefined tools", async () => {
  879. const agent: OpenAgent = {
  880. frontmatter: {
  881. name: "agent",
  882. mode: "primary",
  883. tools: undefined,
  884. },
  885. metadata: { category: "core", type: "agent" },
  886. systemPrompt: "Prompt",
  887. contexts: [],
  888. };
  889. const result = await adapter.fromOAC(agent);
  890. expect(result.success).toBe(true);
  891. });
  892. it("handles decimal temperature values", async () => {
  893. const source = `---
  894. name: agent
  895. temperature: 0.123
  896. ---
  897. Prompt`;
  898. const result = await adapter.toOAC(source);
  899. expect(result.frontmatter.temperature).toBe(0.123);
  900. });
  901. it("handles zero temperature", async () => {
  902. const source = `---
  903. name: agent
  904. temperature: 0
  905. ---
  906. Prompt`;
  907. const result = await adapter.toOAC(source);
  908. expect(result.frontmatter.temperature).toBe(0);
  909. });
  910. it("preserves whitespace in prompts", async () => {
  911. const source = `---
  912. name: agent
  913. ---
  914. Indented line 1
  915. Indented line 2
  916. Regular line`;
  917. const result = await adapter.toOAC(source);
  918. expect(result.systemPrompt).toContain(" Indented");
  919. expect(result.systemPrompt).toContain(" Indented");
  920. });
  921. });
  922. // ============================================================================
  923. // CONVERSION RESULT STRUCTURE
  924. // ============================================================================
  925. describe("conversion result structure", () => {
  926. it("returns success true on valid conversion", async () => {
  927. const agent: OpenAgent = {
  928. frontmatter: {
  929. name: "agent",
  930. mode: "primary",
  931. },
  932. metadata: { category: "core", type: "agent" },
  933. systemPrompt: "Prompt",
  934. contexts: [],
  935. };
  936. const result = await adapter.fromOAC(agent);
  937. expect(result.success).toBe(true);
  938. });
  939. it("includes capabilities in result", async () => {
  940. const agent: OpenAgent = {
  941. frontmatter: {
  942. name: "agent",
  943. mode: "primary",
  944. },
  945. metadata: { category: "core", type: "agent" },
  946. systemPrompt: "Prompt",
  947. contexts: [],
  948. };
  949. const result = await adapter.fromOAC(agent);
  950. expect(result.capabilities).toBeDefined();
  951. expect(result.capabilities?.name).toBe("cursor");
  952. });
  953. it("includes encoding in tool config", async () => {
  954. const agent: OpenAgent = {
  955. frontmatter: {
  956. name: "agent",
  957. mode: "primary",
  958. },
  959. metadata: { category: "core", type: "agent" },
  960. systemPrompt: "Prompt",
  961. contexts: [],
  962. };
  963. const result = await adapter.fromOAC(agent);
  964. expect(result.configs[0].encoding).toBe("utf-8");
  965. });
  966. it("always returns single .cursorrules file", async () => {
  967. const agent: OpenAgent = {
  968. frontmatter: {
  969. name: "agent",
  970. mode: "primary",
  971. tools: { read: true, write: true },
  972. },
  973. metadata: { category: "core", type: "agent" },
  974. systemPrompt: "Prompt",
  975. contexts: [{ path: "ctx1.md" }, { path: "ctx2.md" }],
  976. };
  977. const result = await adapter.fromOAC(agent);
  978. expect(result.configs).toHaveLength(1);
  979. expect(result.configs[0].fileName).toBe(".cursorrules");
  980. });
  981. });
  982. });