WindsurfAdapter.test.ts 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269
  1. import { describe, it, expect, beforeEach } from "vitest";
  2. import { WindsurfAdapter } from "../../../src/adapters/WindsurfAdapter";
  3. import type { OpenAgent, AgentFrontmatter } from "../../../src/types";
  4. /**
  5. * Unit tests for WindsurfAdapter with 80%+ coverage
  6. *
  7. * Test strategy:
  8. * 1. toOAC() - Parse Windsurf JSON to OpenAgent
  9. * 2. fromOAC() - Convert OpenAgent to Windsurf JSON
  10. * 3. getCapabilities() - Feature matrix validation
  11. * 4. validateConversion() - Validation and warnings
  12. * 5. Helper methods - Model/tool/creativity mapping
  13. * 6. Temperature ↔ Creativity mapping (WINDSURF-SPECIFIC)
  14. * 7. Priority mapping (critical→high, medium→low)
  15. * 8. Context and skill handling
  16. * 9. Edge cases and roundtrip
  17. */
  18. describe("WindsurfAdapter", () => {
  19. let adapter: WindsurfAdapter;
  20. beforeEach(() => {
  21. adapter = new WindsurfAdapter();
  22. });
  23. // ============================================================================
  24. // ADAPTER IDENTITY
  25. // ============================================================================
  26. describe("adapter identity", () => {
  27. it("has correct name", () => {
  28. expect(adapter.name).toBe("windsurf");
  29. });
  30. it("has correct displayName", () => {
  31. expect(adapter.displayName).toBe("Windsurf");
  32. });
  33. it("returns correct config path", () => {
  34. expect(adapter.getConfigPath()).toBe(".windsurf/");
  35. });
  36. });
  37. // ============================================================================
  38. // CAPABILITIES
  39. // ============================================================================
  40. describe("getCapabilities()", () => {
  41. it("returns correct capabilities object", () => {
  42. const capabilities = adapter.getCapabilities();
  43. expect(capabilities.name).toBe("windsurf");
  44. expect(capabilities.displayName).toBe("Windsurf");
  45. expect(capabilities.supportsMultipleAgents).toBe(true);
  46. expect(capabilities.supportsSkills).toBe(true);
  47. expect(capabilities.supportsHooks).toBe(false);
  48. expect(capabilities.supportsGranularPermissions).toBe(false);
  49. expect(capabilities.supportsContexts).toBe(true);
  50. expect(capabilities.supportsCustomModels).toBe(true);
  51. expect(capabilities.supportsTemperature).toBe(true);
  52. expect(capabilities.supportsMaxSteps).toBe(false);
  53. expect(capabilities.configFormat).toBe("json");
  54. expect(capabilities.outputStructure).toBe("directory");
  55. });
  56. it("includes appropriate notes", () => {
  57. const capabilities = adapter.getCapabilities();
  58. expect(capabilities.notes).toBeDefined();
  59. expect(capabilities.notes && capabilities.notes.length > 0).toBe(true);
  60. });
  61. });
  62. // ============================================================================
  63. // toOAC() - PARSING WINDSURF JSON
  64. // ============================================================================
  65. describe("toOAC() - parsing Windsurf JSON", () => {
  66. it("parses minimal config", async () => {
  67. const source = JSON.stringify({
  68. name: "windsurf-agent",
  69. description: "Test agent",
  70. systemPrompt: "You are helpful",
  71. });
  72. const result = await adapter.toOAC(source);
  73. expect(result.frontmatter.name).toBe("windsurf-agent");
  74. expect(result.frontmatter.description).toBe("Test agent");
  75. expect(result.systemPrompt).toBe("You are helpful");
  76. expect(result.frontmatter.mode).toBe("primary");
  77. });
  78. it("parses agent with model", async () => {
  79. const source = JSON.stringify({
  80. name: "agent",
  81. description: "Test",
  82. model: "gpt-4",
  83. systemPrompt: "Prompt",
  84. });
  85. const result = await adapter.toOAC(source);
  86. expect(result.frontmatter.model).toBe("gpt-4");
  87. });
  88. it("parses agent with tools object", async () => {
  89. const source = JSON.stringify({
  90. name: "agent",
  91. description: "Test",
  92. tools: { read: true, write: true, bash: false },
  93. systemPrompt: "Prompt",
  94. });
  95. const result = await adapter.toOAC(source);
  96. expect(result.frontmatter.tools).toEqual({
  97. read: true,
  98. write: true,
  99. bash: false,
  100. });
  101. });
  102. it("parses agent with tools array", async () => {
  103. const source = JSON.stringify({
  104. name: "agent",
  105. description: "Test",
  106. tools: ["read", "write", "bash"],
  107. systemPrompt: "Prompt",
  108. });
  109. const result = await adapter.toOAC(source);
  110. expect(result.frontmatter.tools).toEqual({
  111. read: true,
  112. write: true,
  113. bash: true,
  114. });
  115. });
  116. it("parses agent with creativity setting", async () => {
  117. const source = JSON.stringify({
  118. name: "agent",
  119. description: "Test",
  120. creativity: "high",
  121. systemPrompt: "Prompt",
  122. });
  123. const result = await adapter.toOAC(source);
  124. expect(result.frontmatter.temperature).toBe(1.0);
  125. });
  126. it("parses numeric creativity value", async () => {
  127. const source = JSON.stringify({
  128. name: "agent",
  129. description: "Test",
  130. creativity: 0.6,
  131. systemPrompt: "Prompt",
  132. });
  133. const result = await adapter.toOAC(source);
  134. expect(result.frontmatter.temperature).toBe(0.6);
  135. });
  136. it("parses agent type (primary vs subagent)", async () => {
  137. const source = JSON.stringify({
  138. name: "agent",
  139. description: "Test",
  140. type: "subagent",
  141. systemPrompt: "Prompt",
  142. });
  143. const result = await adapter.toOAC(source);
  144. expect(result.frontmatter.mode).toBe("subagent");
  145. });
  146. it("parses contexts", async () => {
  147. const source = JSON.stringify({
  148. name: "agent",
  149. description: "Test",
  150. contexts: [
  151. { path: "context1.md", priority: "high", description: "Important" },
  152. { path: "context2.md" },
  153. ],
  154. systemPrompt: "Prompt",
  155. });
  156. const result = await adapter.toOAC(source);
  157. expect(result.contexts).toHaveLength(2);
  158. expect(result.contexts[0].path).toBe("context1.md");
  159. expect(result.contexts[0].priority).toBe("high");
  160. });
  161. it("parses contexts as string array", async () => {
  162. const source = JSON.stringify({
  163. name: "agent",
  164. description: "Test",
  165. contexts: ["context1.md", "context2.md"],
  166. systemPrompt: "Prompt",
  167. });
  168. const result = await adapter.toOAC(source);
  169. expect(result.contexts).toHaveLength(2);
  170. expect(result.contexts[0].path).toBe("context1.md");
  171. });
  172. it("throws error on invalid JSON", async () => {
  173. const source = "not valid json";
  174. await expect(adapter.toOAC(source)).rejects.toThrow(
  175. "Invalid Windsurf config format"
  176. );
  177. });
  178. it("throws error on non-object JSON", async () => {
  179. const source = JSON.stringify("just a string");
  180. await expect(adapter.toOAC(source)).rejects.toThrow(
  181. "Invalid Windsurf config format"
  182. );
  183. });
  184. it("handles prompt field as fallback for systemPrompt", async () => {
  185. const source = JSON.stringify({
  186. name: "agent",
  187. description: "Test",
  188. prompt: "This is the prompt",
  189. });
  190. const result = await adapter.toOAC(source);
  191. expect(result.systemPrompt).toBe("This is the prompt");
  192. });
  193. it("prefers systemPrompt over prompt field", async () => {
  194. const source = JSON.stringify({
  195. name: "agent",
  196. description: "Test",
  197. systemPrompt: "System",
  198. prompt: "Prompt",
  199. });
  200. const result = await adapter.toOAC(source);
  201. expect(result.systemPrompt).toBe("System");
  202. });
  203. it("uses category from config", async () => {
  204. const source = JSON.stringify({
  205. name: "agent",
  206. description: "Test",
  207. category: "specialist",
  208. systemPrompt: "Prompt",
  209. });
  210. const result = await adapter.toOAC(source);
  211. expect(result.metadata.category).toBe("specialist");
  212. });
  213. it("defaults to core category", async () => {
  214. const source = JSON.stringify({
  215. name: "agent",
  216. description: "Test",
  217. systemPrompt: "Prompt",
  218. });
  219. const result = await adapter.toOAC(source);
  220. expect(result.metadata.category).toBe("core");
  221. });
  222. });
  223. // ============================================================================
  224. // fromOAC() - GENERATING WINDSURF JSON
  225. // ============================================================================
  226. describe("fromOAC() - generating Windsurf config", () => {
  227. const createAgent = (overrides?: Partial<OpenAgent>): OpenAgent => ({
  228. frontmatter: {
  229. name: "test-agent",
  230. description: "A test agent",
  231. mode: "primary",
  232. ...overrides?.frontmatter,
  233. },
  234. metadata: {
  235. name: "test-agent",
  236. category: "core",
  237. type: "agent",
  238. },
  239. systemPrompt: "You are helpful",
  240. contexts: [],
  241. ...overrides,
  242. });
  243. it("converts agent to config.json file", async () => {
  244. const agent = createAgent();
  245. const result = await adapter.fromOAC(agent);
  246. expect(result.success).toBe(true);
  247. expect(result.configs).toHaveLength(1);
  248. expect(result.configs[0].fileName).toBe(".windsurf/config.json");
  249. });
  250. it("converts subagent to agents/{name}.json file", async () => {
  251. const agent = createAgent({
  252. frontmatter: {
  253. name: "test-agent",
  254. mode: "subagent",
  255. },
  256. });
  257. const result = await adapter.fromOAC(agent);
  258. expect(result.configs[0].fileName).toBe(".windsurf/agents/test-agent.json");
  259. });
  260. it("includes agent name and description", async () => {
  261. const agent = createAgent({
  262. frontmatter: {
  263. name: "analyzer",
  264. description: "Code analyzer",
  265. },
  266. });
  267. const result = await adapter.fromOAC(agent);
  268. const config = JSON.parse(result.configs[0].content);
  269. expect(config.name).toBe("analyzer");
  270. expect(config.description).toBe("Code analyzer");
  271. });
  272. it("includes system prompt", async () => {
  273. const agent = createAgent({
  274. systemPrompt: "You are a code reviewer",
  275. });
  276. const result = await adapter.fromOAC(agent);
  277. const config = JSON.parse(result.configs[0].content);
  278. expect(config.systemPrompt).toBe("You are a code reviewer");
  279. });
  280. it("maps model correctly", async () => {
  281. const agent = createAgent({
  282. frontmatter: {
  283. model: "gpt-4",
  284. },
  285. });
  286. const result = await adapter.fromOAC(agent);
  287. const config = JSON.parse(result.configs[0].content);
  288. expect(config.model).toBe("gpt-4");
  289. });
  290. it("includes type field (primary vs subagent)", async () => {
  291. const agent = createAgent({
  292. frontmatter: {
  293. mode: "subagent",
  294. },
  295. });
  296. const result = await adapter.fromOAC(agent);
  297. const config = JSON.parse(result.configs[0].content);
  298. expect(config.type).toBe("subagent");
  299. });
  300. it("maps temperature to creativity", async () => {
  301. const agent = createAgent({
  302. frontmatter: {
  303. temperature: 0.9,
  304. },
  305. });
  306. const result = await adapter.fromOAC(agent);
  307. const config = JSON.parse(result.configs[0].content);
  308. expect(config.creativity).toBe("high");
  309. });
  310. it("includes category from metadata", async () => {
  311. const agent = createAgent({
  312. metadata: {
  313. name: "agent",
  314. category: "specialist",
  315. type: "agent",
  316. },
  317. });
  318. const result = await adapter.fromOAC(agent);
  319. const config = JSON.parse(result.configs[0].content);
  320. expect(config.category).toBe("specialist");
  321. });
  322. it("warns on unsupported hooks", async () => {
  323. const agent = createAgent({
  324. frontmatter: {
  325. hooks: [{ event: "PreToolUse", commands: [{ type: "command", command: "log" }] }],
  326. },
  327. });
  328. const result = await adapter.fromOAC(agent);
  329. expect(result.warnings.some((w) => w.includes("hooks"))).toBe(true);
  330. });
  331. it("warns on unsupported maxSteps", async () => {
  332. const agent = createAgent({
  333. frontmatter: {
  334. maxSteps: 10,
  335. },
  336. });
  337. const result = await adapter.fromOAC(agent);
  338. expect(result.warnings.some((w) => w.includes("maxSteps"))).toBe(true);
  339. });
  340. it("warns on granular permissions", async () => {
  341. const agent = createAgent({
  342. frontmatter: {
  343. permission: {
  344. read: { "*.txt": "allow" },
  345. },
  346. },
  347. });
  348. const result = await adapter.fromOAC(agent);
  349. expect(result.warnings.some((w) => w.includes("granular"))).toBe(true);
  350. });
  351. it("warns on degraded skills", async () => {
  352. const agent = createAgent({
  353. frontmatter: {
  354. skills: ["skill1", "skill2"],
  355. },
  356. });
  357. const result = await adapter.fromOAC(agent);
  358. expect(result.warnings.some((w) => w.includes("skills"))).toBe(true);
  359. });
  360. it("maps skills to context references", async () => {
  361. const agent = createAgent({
  362. frontmatter: {
  363. skills: ["python-patterns", "typescript-tips"],
  364. },
  365. });
  366. const result = await adapter.fromOAC(agent);
  367. const config = JSON.parse(result.configs[0].content);
  368. expect(config.contexts).toBeDefined();
  369. expect(config.contexts.some((c: any) => c.path.includes("python-patterns"))).toBe(true);
  370. });
  371. it("includes tool access", async () => {
  372. const agent = createAgent({
  373. frontmatter: {
  374. tools: { read: true, write: true, bash: false },
  375. },
  376. });
  377. const result = await adapter.fromOAC(agent);
  378. const config = JSON.parse(result.configs[0].content);
  379. expect(config.tools).toEqual({ read: true, write: true, bash: false });
  380. });
  381. it("includes contexts with priority mapping", async () => {
  382. const agent = createAgent({
  383. contexts: [
  384. { path: "critical-context.md", priority: "critical" },
  385. { path: "medium-context.md", priority: "medium" },
  386. ],
  387. });
  388. const result = await adapter.fromOAC(agent);
  389. const config = JSON.parse(result.configs[0].content);
  390. expect(config.contexts).toHaveLength(2);
  391. // critical → high, medium → low
  392. expect(config.contexts[0].priority).toBe("high");
  393. expect(config.contexts[1].priority).toBe("low");
  394. });
  395. it("generates context warning", async () => {
  396. const agent = createAgent({
  397. contexts: [{ path: "context.md" }],
  398. });
  399. const result = await adapter.fromOAC(agent);
  400. expect(result.warnings.some((w) => w.includes("context"))).toBe(true);
  401. });
  402. it("maps permissions to binary on/off", async () => {
  403. const agent = createAgent({
  404. frontmatter: {
  405. permission: {
  406. read: "allow",
  407. write: "deny",
  408. bash: "ask",
  409. },
  410. },
  411. });
  412. const result = await adapter.fromOAC(agent);
  413. const config = JSON.parse(result.configs[0].content);
  414. expect(config.permissions.read).toBe(true);
  415. expect(config.permissions.write).toBe(false);
  416. expect(config.permissions.bash).toBe(false); // ask → false
  417. });
  418. });
  419. // ============================================================================
  420. // VALIDATION
  421. // ============================================================================
  422. describe("validateConversion()", () => {
  423. const createAgent = (overrides?: Partial<AgentFrontmatter>): OpenAgent => ({
  424. frontmatter: {
  425. name: "agent",
  426. description: "Test",
  427. mode: "primary",
  428. ...overrides,
  429. },
  430. metadata: { category: "core", type: "agent" },
  431. systemPrompt: "Prompt",
  432. contexts: [],
  433. });
  434. it("returns no warnings for valid agent", () => {
  435. const agent = createAgent();
  436. const warnings = adapter.validateConversion(agent);
  437. expect(warnings).toHaveLength(0);
  438. });
  439. it("warns when name is missing", () => {
  440. const agent = createAgent({ name: "" });
  441. const warnings = adapter.validateConversion(agent);
  442. expect(warnings.some((w) => w.includes("name"))).toBe(true);
  443. });
  444. it("warns when description is missing", () => {
  445. const agent = createAgent({ description: "" });
  446. const warnings = adapter.validateConversion(agent);
  447. expect(warnings.some((w) => w.includes("description"))).toBe(true);
  448. });
  449. });
  450. // ============================================================================
  451. // TEMPERATURE ↔ CREATIVITY MAPPING (WINDSURF-SPECIFIC)
  452. // ============================================================================
  453. describe("temperature to creativity mapping", () => {
  454. it("maps low temperature (0.3) to low creativity", async () => {
  455. const agent: OpenAgent = {
  456. frontmatter: {
  457. name: "agent",
  458. description: "Test",
  459. mode: "primary",
  460. temperature: 0.3,
  461. },
  462. metadata: { category: "core", type: "agent" },
  463. systemPrompt: "Prompt",
  464. contexts: [],
  465. };
  466. const result = await adapter.fromOAC(agent);
  467. const config = JSON.parse(result.configs[0].content);
  468. expect(config.creativity).toBe("low");
  469. });
  470. it("maps medium temperature (0.6) to medium creativity", async () => {
  471. const agent: OpenAgent = {
  472. frontmatter: {
  473. name: "agent",
  474. description: "Test",
  475. mode: "primary",
  476. temperature: 0.6,
  477. },
  478. metadata: { category: "core", type: "agent" },
  479. systemPrompt: "Prompt",
  480. contexts: [],
  481. };
  482. const result = await adapter.fromOAC(agent);
  483. const config = JSON.parse(result.configs[0].content);
  484. expect(config.creativity).toBe("medium");
  485. });
  486. it("maps high temperature (0.9) to high creativity", async () => {
  487. const agent: OpenAgent = {
  488. frontmatter: {
  489. name: "agent",
  490. description: "Test",
  491. mode: "primary",
  492. temperature: 0.9,
  493. },
  494. metadata: { category: "core", type: "agent" },
  495. systemPrompt: "Prompt",
  496. contexts: [],
  497. };
  498. const result = await adapter.fromOAC(agent);
  499. const config = JSON.parse(result.configs[0].content);
  500. expect(config.creativity).toBe("high");
  501. });
  502. it("maps creativity low string to 0.3 temperature", async () => {
  503. const source = JSON.stringify({
  504. name: "agent",
  505. description: "Test",
  506. creativity: "low",
  507. systemPrompt: "Prompt",
  508. });
  509. const result = await adapter.toOAC(source);
  510. expect(result.frontmatter.temperature).toBe(0.3);
  511. });
  512. it("maps creativity high string to 1.0 temperature", async () => {
  513. const source = JSON.stringify({
  514. name: "agent",
  515. description: "Test",
  516. creativity: "high",
  517. systemPrompt: "Prompt",
  518. });
  519. const result = await adapter.toOAC(source);
  520. expect(result.frontmatter.temperature).toBe(1.0);
  521. });
  522. it("maps creativity balanced to 0.5 temperature", async () => {
  523. const source = JSON.stringify({
  524. name: "agent",
  525. description: "Test",
  526. creativity: "balanced",
  527. systemPrompt: "Prompt",
  528. });
  529. const result = await adapter.toOAC(source);
  530. expect(result.frontmatter.temperature).toBe(0.5);
  531. });
  532. it("defaults to medium (0.7) for unknown creativity", async () => {
  533. const source = JSON.stringify({
  534. name: "agent",
  535. description: "Test",
  536. creativity: "unknown-value",
  537. systemPrompt: "Prompt",
  538. });
  539. const result = await adapter.toOAC(source);
  540. expect(result.frontmatter.temperature).toBe(0.7);
  541. });
  542. it("handles boundary temperature 0.4", async () => {
  543. const agent: OpenAgent = {
  544. frontmatter: {
  545. name: "agent",
  546. description: "Test",
  547. mode: "primary",
  548. temperature: 0.4,
  549. },
  550. metadata: { category: "core", type: "agent" },
  551. systemPrompt: "Prompt",
  552. contexts: [],
  553. };
  554. const result = await adapter.fromOAC(agent);
  555. const config = JSON.parse(result.configs[0].content);
  556. expect(config.creativity).toBe("low");
  557. });
  558. it("handles boundary temperature 0.8", async () => {
  559. const agent: OpenAgent = {
  560. frontmatter: {
  561. name: "agent",
  562. description: "Test",
  563. mode: "primary",
  564. temperature: 0.8,
  565. },
  566. metadata: { category: "core", type: "agent" },
  567. systemPrompt: "Prompt",
  568. contexts: [],
  569. };
  570. const result = await adapter.fromOAC(agent);
  571. const config = JSON.parse(result.configs[0].content);
  572. expect(config.creativity).toBe("medium");
  573. });
  574. });
  575. // ============================================================================
  576. // PRIORITY MAPPING
  577. // ============================================================================
  578. describe("priority mapping (OAC → Windsurf)", () => {
  579. it("maps critical to high", async () => {
  580. const agent: OpenAgent = {
  581. frontmatter: {
  582. name: "agent",
  583. description: "Test",
  584. mode: "primary",
  585. },
  586. metadata: { category: "core", type: "agent" },
  587. systemPrompt: "Prompt",
  588. contexts: [{ path: "critical.md", priority: "critical" }],
  589. };
  590. const result = await adapter.fromOAC(agent);
  591. const config = JSON.parse(result.configs[0].content);
  592. expect(config.contexts[0].priority).toBe("high");
  593. });
  594. it("maps medium to low", async () => {
  595. const agent: OpenAgent = {
  596. frontmatter: {
  597. name: "agent",
  598. description: "Test",
  599. mode: "primary",
  600. },
  601. metadata: { category: "core", type: "agent" },
  602. systemPrompt: "Prompt",
  603. contexts: [{ path: "medium.md", priority: "medium" }],
  604. };
  605. const result = await adapter.fromOAC(agent);
  606. const config = JSON.parse(result.configs[0].content);
  607. expect(config.contexts[0].priority).toBe("low");
  608. });
  609. it("normalizes invalid priority to medium", async () => {
  610. const source = JSON.stringify({
  611. name: "agent",
  612. description: "Test",
  613. contexts: [{ path: "context.md", priority: "invalid" }],
  614. systemPrompt: "Prompt",
  615. });
  616. const result = await adapter.toOAC(source);
  617. expect(result.contexts[0].priority).toBe("medium");
  618. });
  619. });
  620. // ============================================================================
  621. // MODEL MAPPING
  622. // ============================================================================
  623. describe("model mapping", () => {
  624. it("maps claude-4-sonnet to claude-sonnet-4", async () => {
  625. const source = JSON.stringify({
  626. name: "agent",
  627. description: "Test",
  628. model: "claude-4-sonnet",
  629. systemPrompt: "Prompt",
  630. });
  631. const result = await adapter.toOAC(source);
  632. expect(result.frontmatter.model).toBe("claude-sonnet-4");
  633. });
  634. it("maps claude-4-opus to claude-opus-4", async () => {
  635. const source = JSON.stringify({
  636. name: "agent",
  637. description: "Test",
  638. model: "claude-4-opus",
  639. systemPrompt: "Prompt",
  640. });
  641. const result = await adapter.toOAC(source);
  642. expect(result.frontmatter.model).toBe("claude-opus-4");
  643. });
  644. it("maps gpt-4 to gpt-4", async () => {
  645. const source = JSON.stringify({
  646. name: "agent",
  647. description: "Test",
  648. model: "gpt-4",
  649. systemPrompt: "Prompt",
  650. });
  651. const result = await adapter.toOAC(source);
  652. expect(result.frontmatter.model).toBe("gpt-4");
  653. });
  654. it("preserves unknown models", async () => {
  655. const source = JSON.stringify({
  656. name: "agent",
  657. description: "Test",
  658. model: "custom-model",
  659. systemPrompt: "Prompt",
  660. });
  661. const result = await adapter.toOAC(source);
  662. expect(result.frontmatter.model).toBe("custom-model");
  663. });
  664. it("maps claude-sonnet-4 back to claude-4-sonnet", async () => {
  665. const agent: OpenAgent = {
  666. frontmatter: {
  667. name: "agent",
  668. description: "Test",
  669. mode: "primary",
  670. model: "claude-sonnet-4",
  671. },
  672. metadata: { category: "core", type: "agent" },
  673. systemPrompt: "Prompt",
  674. contexts: [],
  675. };
  676. const result = await adapter.fromOAC(agent);
  677. const config = JSON.parse(result.configs[0].content);
  678. expect(config.model).toBe("claude-4-sonnet");
  679. });
  680. it("defaults to claude-4-sonnet for unknown models", async () => {
  681. const agent: OpenAgent = {
  682. frontmatter: {
  683. name: "agent",
  684. description: "Test",
  685. mode: "primary",
  686. model: "unknown-model",
  687. },
  688. metadata: { category: "core", type: "agent" },
  689. systemPrompt: "Prompt",
  690. contexts: [],
  691. };
  692. const result = await adapter.fromOAC(agent);
  693. const config = JSON.parse(result.configs[0].content);
  694. expect(config.model).toBe("claude-4-sonnet");
  695. });
  696. });
  697. // ============================================================================
  698. // TOOL MAPPING
  699. // ============================================================================
  700. describe("tool handling", () => {
  701. it("preserves tool boolean values", async () => {
  702. const source = JSON.stringify({
  703. name: "agent",
  704. description: "Test",
  705. tools: { read: true, write: false, bash: true },
  706. systemPrompt: "Prompt",
  707. });
  708. const result = await adapter.toOAC(source);
  709. expect(result.frontmatter.tools).toEqual({
  710. read: true,
  711. write: false,
  712. bash: true,
  713. });
  714. });
  715. it("normalizes tools array to object", async () => {
  716. const source = JSON.stringify({
  717. name: "agent",
  718. description: "Test",
  719. tools: ["Read", "WRITE", "Bash"],
  720. systemPrompt: "Prompt",
  721. });
  722. const result = await adapter.toOAC(source);
  723. expect(result.frontmatter.tools?.read).toBe(true);
  724. expect(result.frontmatter.tools?.write).toBe(true);
  725. expect(result.frontmatter.tools?.bash).toBe(true);
  726. });
  727. it("handles empty tools", async () => {
  728. const source = JSON.stringify({
  729. name: "agent",
  730. description: "Test",
  731. tools: {},
  732. systemPrompt: "Prompt",
  733. });
  734. const result = await adapter.toOAC(source);
  735. expect(result.frontmatter.tools).toBeUndefined();
  736. });
  737. });
  738. // ============================================================================
  739. // PERMISSION MAPPING
  740. // ============================================================================
  741. describe("permission mapping", () => {
  742. it("maps allow to true", async () => {
  743. const agent: OpenAgent = {
  744. frontmatter: {
  745. name: "agent",
  746. description: "Test",
  747. mode: "primary",
  748. permission: { read: "allow" },
  749. },
  750. metadata: { category: "core", type: "agent" },
  751. systemPrompt: "Prompt",
  752. contexts: [],
  753. };
  754. const result = await adapter.fromOAC(agent);
  755. const config = JSON.parse(result.configs[0].content);
  756. expect(config.permissions.read).toBe(true);
  757. });
  758. it("maps deny to false", async () => {
  759. const agent: OpenAgent = {
  760. frontmatter: {
  761. name: "agent",
  762. description: "Test",
  763. mode: "primary",
  764. permission: { write: "deny" },
  765. },
  766. metadata: { category: "core", type: "agent" },
  767. systemPrompt: "Prompt",
  768. contexts: [],
  769. };
  770. const result = await adapter.fromOAC(agent);
  771. const config = JSON.parse(result.configs[0].content);
  772. expect(config.permissions.write).toBe(false);
  773. });
  774. it("maps ask to false with warning", async () => {
  775. const agent: OpenAgent = {
  776. frontmatter: {
  777. name: "agent",
  778. description: "Test",
  779. mode: "primary",
  780. permission: { bash: "ask" },
  781. },
  782. metadata: { category: "core", type: "agent" },
  783. systemPrompt: "Prompt",
  784. contexts: [],
  785. };
  786. const result = await adapter.fromOAC(agent);
  787. const config = JSON.parse(result.configs[0].content);
  788. expect(config.permissions.bash).toBe(false);
  789. expect(result.warnings.some((w) => w.includes("ask"))).toBe(true);
  790. });
  791. it("handles boolean permissions", async () => {
  792. const agent: OpenAgent = {
  793. frontmatter: {
  794. name: "agent",
  795. description: "Test",
  796. mode: "primary",
  797. permission: { read: true, write: false },
  798. },
  799. metadata: { category: "core", type: "agent" },
  800. systemPrompt: "Prompt",
  801. contexts: [],
  802. };
  803. const result = await adapter.fromOAC(agent);
  804. const config = JSON.parse(result.configs[0].content);
  805. expect(config.permissions.read).toBe(true);
  806. expect(config.permissions.write).toBe(false);
  807. });
  808. });
  809. // ============================================================================
  810. // ROUNDTRIP & EDGE CASES
  811. // ============================================================================
  812. describe("roundtrip conversion", () => {
  813. it("preserves agent properties through roundtrip", async () => {
  814. const original: OpenAgent = {
  815. frontmatter: {
  816. name: "roundtrip-agent",
  817. description: "Test roundtrip",
  818. mode: "primary",
  819. model: "gpt-4o",
  820. temperature: 0.7,
  821. tools: { read: true, write: true },
  822. },
  823. metadata: { name: "roundtrip-agent", category: "core", type: "agent" },
  824. systemPrompt: "Be helpful",
  825. contexts: [{ path: "context.md", priority: "high" }],
  826. };
  827. // Convert to Windsurf
  828. const windsurfResult = await adapter.fromOAC(original);
  829. const windsurfConfig = JSON.parse(windsurfResult.configs[0].content);
  830. // Convert back to OAC
  831. const backToOAC = await adapter.toOAC(JSON.stringify(windsurfConfig));
  832. expect(backToOAC.frontmatter.name).toBe("roundtrip-agent");
  833. expect(backToOAC.systemPrompt).toBe("Be helpful");
  834. expect(backToOAC.frontmatter.temperature).toBe(0.7);
  835. });
  836. });
  837. describe("edge cases", () => {
  838. it("handles empty system prompt", async () => {
  839. const agent: OpenAgent = {
  840. frontmatter: {
  841. name: "agent",
  842. description: "Test",
  843. mode: "primary",
  844. },
  845. metadata: { category: "core", type: "agent" },
  846. systemPrompt: "",
  847. contexts: [],
  848. };
  849. const result = await adapter.fromOAC(agent);
  850. expect(result.success).toBe(true);
  851. });
  852. it("handles very long system prompt", async () => {
  853. const longPrompt = "A".repeat(5000);
  854. const agent: OpenAgent = {
  855. frontmatter: {
  856. name: "agent",
  857. description: "Test",
  858. mode: "primary",
  859. },
  860. metadata: { category: "core", type: "agent" },
  861. systemPrompt: longPrompt,
  862. contexts: [],
  863. };
  864. const result = await adapter.fromOAC(agent);
  865. const config = JSON.parse(result.configs[0].content);
  866. expect(config.systemPrompt.length).toBe(5000);
  867. });
  868. it("handles special characters in names", async () => {
  869. const agent: OpenAgent = {
  870. frontmatter: {
  871. name: "agent-with-dashes_and_underscores",
  872. description: "Test",
  873. mode: "primary",
  874. },
  875. metadata: { category: "core", type: "agent" },
  876. systemPrompt: "Prompt",
  877. contexts: [],
  878. };
  879. const result = await adapter.fromOAC(agent);
  880. const config = JSON.parse(result.configs[0].content);
  881. expect(config.name).toBe("agent-with-dashes_and_underscores");
  882. });
  883. it("handles zero temperature", async () => {
  884. const agent: OpenAgent = {
  885. frontmatter: {
  886. name: "agent",
  887. description: "Test",
  888. mode: "primary",
  889. temperature: 0,
  890. },
  891. metadata: { category: "core", type: "agent" },
  892. systemPrompt: "Prompt",
  893. contexts: [],
  894. };
  895. const result = await adapter.fromOAC(agent);
  896. const config = JSON.parse(result.configs[0].content);
  897. expect(config.creativity).toBe("low");
  898. });
  899. it("handles high temperature values", async () => {
  900. const agent: OpenAgent = {
  901. frontmatter: {
  902. name: "agent",
  903. description: "Test",
  904. mode: "primary",
  905. temperature: 1.5,
  906. },
  907. metadata: { category: "core", type: "agent" },
  908. systemPrompt: "Prompt",
  909. contexts: [],
  910. };
  911. const result = await adapter.fromOAC(agent);
  912. const config = JSON.parse(result.configs[0].content);
  913. expect(config.creativity).toBe("high");
  914. });
  915. it("handles null contexts", async () => {
  916. const agent: OpenAgent = {
  917. frontmatter: {
  918. name: "agent",
  919. description: "Test",
  920. mode: "primary",
  921. },
  922. metadata: { category: "core", type: "agent" },
  923. systemPrompt: "Prompt",
  924. contexts: null as any,
  925. };
  926. const result = await adapter.fromOAC(agent);
  927. expect(result.success).toBe(true);
  928. });
  929. it("handles undefined tools", async () => {
  930. const agent: OpenAgent = {
  931. frontmatter: {
  932. name: "agent",
  933. description: "Test",
  934. mode: "primary",
  935. tools: undefined,
  936. },
  937. metadata: { category: "core", type: "agent" },
  938. systemPrompt: "Prompt",
  939. contexts: [],
  940. };
  941. const result = await adapter.fromOAC(agent);
  942. const config = JSON.parse(result.configs[0].content);
  943. expect(config.tools).toBeUndefined();
  944. });
  945. it("combines contexts and skills", async () => {
  946. const agent: OpenAgent = {
  947. frontmatter: {
  948. name: "agent",
  949. description: "Test",
  950. mode: "primary",
  951. skills: ["python-tips", "typescript-guide"],
  952. },
  953. metadata: { category: "core", type: "agent" },
  954. systemPrompt: "Prompt",
  955. contexts: [{ path: "project-style.md", priority: "high" }],
  956. };
  957. const result = await adapter.fromOAC(agent);
  958. const config = JSON.parse(result.configs[0].content);
  959. // Should have 3 contexts: 1 original + 2 skills
  960. expect(config.contexts.length).toBe(3);
  961. });
  962. });
  963. // ============================================================================
  964. // CONVERSION RESULT STRUCTURE
  965. // ============================================================================
  966. describe("conversion result structure", () => {
  967. it("returns success true on valid conversion", async () => {
  968. const agent: OpenAgent = {
  969. frontmatter: {
  970. name: "agent",
  971. description: "Test",
  972. mode: "primary",
  973. },
  974. metadata: { category: "core", type: "agent" },
  975. systemPrompt: "Prompt",
  976. contexts: [],
  977. };
  978. const result = await adapter.fromOAC(agent);
  979. expect(result.success).toBe(true);
  980. });
  981. it("includes capabilities in result", async () => {
  982. const agent: OpenAgent = {
  983. frontmatter: {
  984. name: "agent",
  985. description: "Test",
  986. mode: "primary",
  987. },
  988. metadata: { category: "core", type: "agent" },
  989. systemPrompt: "Prompt",
  990. contexts: [],
  991. };
  992. const result = await adapter.fromOAC(agent);
  993. expect(result.capabilities).toBeDefined();
  994. expect(result.capabilities?.name).toBe("windsurf");
  995. });
  996. it("includes encoding in tool config", async () => {
  997. const agent: OpenAgent = {
  998. frontmatter: {
  999. name: "agent",
  1000. description: "Test",
  1001. mode: "primary",
  1002. },
  1003. metadata: { category: "core", type: "agent" },
  1004. systemPrompt: "Prompt",
  1005. contexts: [],
  1006. };
  1007. const result = await adapter.fromOAC(agent);
  1008. expect(result.configs[0].encoding).toBe("utf-8");
  1009. });
  1010. it("returns single config file per agent", async () => {
  1011. const agent: OpenAgent = {
  1012. frontmatter: {
  1013. name: "agent",
  1014. description: "Test",
  1015. mode: "primary",
  1016. tools: { read: true, write: true },
  1017. },
  1018. metadata: { category: "core", type: "agent" },
  1019. systemPrompt: "Prompt",
  1020. contexts: [{ path: "ctx1.md" }, { path: "ctx2.md" }],
  1021. };
  1022. const result = await adapter.fromOAC(agent);
  1023. expect(result.configs).toHaveLength(1);
  1024. expect(result.configs[0].fileName).toBe(".windsurf/config.json");
  1025. });
  1026. });
  1027. });