TranslationEngine.test.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522
  1. /**
  2. * Unit tests for TranslationEngine
  3. *
  4. * Tests the orchestration of all mappers for complete agent translation.
  5. */
  6. import { describe, it, expect, beforeEach } from "vitest";
  7. import {
  8. TranslationEngine,
  9. createTranslationEngine,
  10. translate,
  11. previewTranslation,
  12. type TranslationTarget,
  13. type TranslationOptions,
  14. } from "../../../src/core/TranslationEngine";
  15. import type { OpenAgent, ToolAccess, GranularPermission } from "../../../src/types";
  16. // =============================================================================
  17. // Test Fixtures
  18. // =============================================================================
  19. const createTestAgent = (overrides: Partial<OpenAgent> = {}): OpenAgent => ({
  20. frontmatter: {
  21. name: "Test Agent",
  22. description: "A comprehensive test agent",
  23. mode: "primary",
  24. model: "claude-sonnet-4",
  25. temperature: 0.7,
  26. tools: {
  27. bash: true,
  28. read: true,
  29. write: true,
  30. edit: true,
  31. grep: true,
  32. glob: true,
  33. task: true,
  34. },
  35. permission: {
  36. bash: { allow: ["npm *", "git *"], deny: ["rm -rf *"] },
  37. read: { allow: ["**/*"] },
  38. write: { allow: ["src/**/*"], deny: ["node_modules/**"] },
  39. },
  40. skills: ["coding", "testing"],
  41. ...overrides.frontmatter,
  42. },
  43. systemPrompt: "You are a helpful coding assistant.",
  44. contexts: [
  45. { path: ".opencode/context/core/standards.md", priority: "critical" },
  46. { path: ".opencode/context/guides/workflow.md", priority: "high" },
  47. ],
  48. ...overrides,
  49. });
  50. const createMinimalAgent = (overrides: Partial<OpenAgent> = {}): OpenAgent => ({
  51. frontmatter: {
  52. name: "Minimal Agent",
  53. description: "A minimal test agent",
  54. mode: "primary",
  55. ...overrides.frontmatter,
  56. },
  57. systemPrompt: "You are a minimal agent.",
  58. contexts: [],
  59. ...overrides,
  60. });
  61. describe("TranslationEngine", () => {
  62. let engine: TranslationEngine;
  63. beforeEach(() => {
  64. engine = new TranslationEngine();
  65. });
  66. // ==========================================================================
  67. // Constructor and Options
  68. // ==========================================================================
  69. describe("constructor", () => {
  70. it("creates engine with default options", () => {
  71. const engine = new TranslationEngine();
  72. const opts = engine.getOptions();
  73. expect(opts.permissionStrategy).toBe("permissive");
  74. expect(opts.analyzeCompatibility).toBe(true);
  75. expect(opts.preserveAsComments).toBe(false);
  76. expect(opts.modelFallback).toBe("claude-sonnet-4");
  77. });
  78. it("creates engine with custom options", () => {
  79. const engine = new TranslationEngine({
  80. permissionStrategy: "restrictive",
  81. analyzeCompatibility: false,
  82. });
  83. const opts = engine.getOptions();
  84. expect(opts.permissionStrategy).toBe("restrictive");
  85. expect(opts.analyzeCompatibility).toBe(false);
  86. });
  87. });
  88. describe("getOptions()", () => {
  89. it("returns a copy of options", () => {
  90. const opts1 = engine.getOptions();
  91. const opts2 = engine.getOptions();
  92. expect(opts1).not.toBe(opts2);
  93. expect(opts1).toEqual(opts2);
  94. });
  95. });
  96. describe("setOptions()", () => {
  97. it("updates options", () => {
  98. engine.setOptions({ permissionStrategy: "restrictive" });
  99. const opts = engine.getOptions();
  100. expect(opts.permissionStrategy).toBe("restrictive");
  101. });
  102. it("preserves unset options", () => {
  103. engine.setOptions({ permissionStrategy: "restrictive" });
  104. const opts = engine.getOptions();
  105. expect(opts.analyzeCompatibility).toBe(true);
  106. });
  107. });
  108. // ==========================================================================
  109. // translate() - OAC to Platform
  110. // ==========================================================================
  111. describe("translate()", () => {
  112. describe("basic translation", () => {
  113. it("translates minimal agent successfully", () => {
  114. const agent = createMinimalAgent();
  115. const result = engine.translate(agent, "claude");
  116. expect(result.success).toBe(true);
  117. expect(result.frontmatter.name).toBe("Minimal Agent");
  118. expect(result.frontmatter.description).toBe("A minimal test agent");
  119. });
  120. it("includes compatibility analysis by default", () => {
  121. const agent = createMinimalAgent();
  122. const result = engine.translate(agent, "claude");
  123. expect(result.compatibility).toBeDefined();
  124. expect(result.compatibility?.compatible).toBe(true);
  125. });
  126. it("skips compatibility analysis when disabled", () => {
  127. const agent = createMinimalAgent();
  128. const result = engine.translate(agent, "claude", {
  129. analyzeCompatibility: false,
  130. });
  131. expect(result.compatibility).toBeUndefined();
  132. });
  133. });
  134. describe("tool translation", () => {
  135. it("translates tools to target platform format", () => {
  136. const agent = createTestAgent();
  137. const result = engine.translate(agent, "claude");
  138. expect(result.tools).toBeDefined();
  139. expect(result.tools?.Bash).toBe(true);
  140. expect(result.tools?.Read).toBe(true);
  141. expect(result.tools?.Write).toBe(true);
  142. });
  143. it("translates tools to Cursor format", () => {
  144. const agent = createTestAgent();
  145. const result = engine.translate(agent, "cursor");
  146. expect(result.tools).toBeDefined();
  147. expect(result.tools?.terminal).toBe(true);
  148. expect(result.tools?.file_read).toBe(true);
  149. });
  150. });
  151. describe("permission translation", () => {
  152. it("translates granular permissions to binary format", () => {
  153. const agent = createTestAgent();
  154. const result = engine.translate(agent, "claude");
  155. expect(result.permissions).toBeDefined();
  156. expect(typeof result.permissions?.bash).toBe("boolean");
  157. expect(typeof result.permissions?.read).toBe("boolean");
  158. expect(typeof result.permissions?.write).toBe("boolean");
  159. });
  160. it("uses permissive strategy by default", () => {
  161. const agent = createTestAgent();
  162. const result = engine.translate(agent, "claude");
  163. // With permissive strategy, allow patterns enable the permission
  164. expect(result.permissions?.bash).toBe(true);
  165. });
  166. it("respects restrictive strategy option", () => {
  167. const agent = createTestAgent();
  168. const result = engine.translate(agent, "claude", {
  169. permissionStrategy: "restrictive",
  170. });
  171. // Even with restrictive, basic permissions should be enabled if allow exists
  172. expect(result.permissions).toBeDefined();
  173. });
  174. });
  175. describe("model translation", () => {
  176. it("translates model ID to target platform format", () => {
  177. const agent = createTestAgent();
  178. const result = engine.translate(agent, "claude");
  179. expect(result.model).toBeDefined();
  180. expect(result.model).toBe("claude-sonnet-4-20250514");
  181. });
  182. it("handles missing model", () => {
  183. const agent = createMinimalAgent();
  184. const result = engine.translate(agent, "claude");
  185. expect(result.model).toBeUndefined();
  186. });
  187. });
  188. describe("context translation", () => {
  189. it("translates context paths to target platform", () => {
  190. const agent = createTestAgent();
  191. const result = engine.translate(agent, "claude");
  192. expect(result.contextPaths).toBeDefined();
  193. expect(result.contextPaths?.length).toBe(2);
  194. expect(result.contextPaths?.[0]).toContain(".claude/skills");
  195. });
  196. it("returns empty paths for Cursor (inline-only)", () => {
  197. const agent = createTestAgent();
  198. const result = engine.translate(agent, "cursor");
  199. // Cursor doesn't support external context, paths should be filtered
  200. expect(result.contextPaths).toBeDefined();
  201. expect(result.contextPaths?.length).toBe(0);
  202. });
  203. it("collects context priority warnings", () => {
  204. const agent = createTestAgent();
  205. const result = engine.translate(agent, "claude");
  206. expect(result.warnings.some((w) => w.includes("priority"))).toBe(true);
  207. });
  208. });
  209. describe("skill translation", () => {
  210. it("translates skills to Claude format", () => {
  211. const agent = createTestAgent();
  212. const result = engine.translate(agent, "claude");
  213. expect(result.skills).toBeDefined();
  214. expect(result.skills?.length).toBe(2);
  215. expect(result.skills?.[0]).toBe(".claude/skills/coding.md");
  216. });
  217. it("does not include skills for non-Claude targets", () => {
  218. const agent = createTestAgent();
  219. const result = engine.translate(agent, "cursor");
  220. expect(result.skills).toBeUndefined();
  221. });
  222. });
  223. describe("temperature handling", () => {
  224. it("includes temperature when supported", () => {
  225. const agent = createTestAgent();
  226. const result = engine.translate(agent, "cursor");
  227. // Cursor has partial temperature support
  228. expect(result.frontmatter.temperature).toBeDefined();
  229. });
  230. it("excludes temperature when not supported", () => {
  231. const agent = createTestAgent();
  232. const result = engine.translate(agent, "claude");
  233. // Claude doesn't support temperature
  234. expect(result.frontmatter.temperature).toBeUndefined();
  235. });
  236. });
  237. describe("warnings collection", () => {
  238. it("collects all warnings from sub-mappers", () => {
  239. const agent = createTestAgent();
  240. const result = engine.translate(agent, "cursor");
  241. expect(result.warnings.length).toBeGreaterThan(0);
  242. });
  243. it("includes compatibility warnings", () => {
  244. const agent = createTestAgent({
  245. frontmatter: {
  246. name: "Agent with hooks",
  247. description: "Agent",
  248. mode: "primary",
  249. hooks: [{ event: "PreToolUse", command: "echo test" }],
  250. },
  251. });
  252. const result = engine.translate(agent, "cursor");
  253. expect(result.success).toBe(false);
  254. expect(result.compatibility?.compatible).toBe(false);
  255. expect(result.compatibility?.blockers.length).toBeGreaterThan(0);
  256. });
  257. });
  258. });
  259. // ==========================================================================
  260. // translateToOAC() - Platform to OAC
  261. // ==========================================================================
  262. describe("translateToOAC()", () => {
  263. it("translates Claude format to OAC", () => {
  264. const source = {
  265. name: "Claude Agent",
  266. description: "An agent from Claude",
  267. tools: { Bash: true, Read: true, Write: true },
  268. permissions: { bash: true, read: true, write: false },
  269. model: "claude-sonnet-4-20250514",
  270. skills: [".claude/skills/coding.md"],
  271. systemPrompt: "You are helpful.",
  272. };
  273. const result = engine.translateToOAC(source, "claude");
  274. expect(result.success).toBe(true);
  275. expect(result.agent.frontmatter?.name).toBe("Claude Agent");
  276. expect(result.agent.frontmatter?.tools).toBeDefined();
  277. expect(result.agent.frontmatter?.model).toBe("claude-sonnet-4");
  278. });
  279. it("translates Cursor format to OAC", () => {
  280. const source = {
  281. name: "Cursor Agent",
  282. tools: { terminal: true, file_read: true },
  283. permissions: { bash: true, read: true },
  284. };
  285. const result = engine.translateToOAC(source, "cursor");
  286. expect(result.success).toBe(true);
  287. expect(result.agent.frontmatter?.name).toBe("Cursor Agent");
  288. expect(result.agent.frontmatter?.tools?.bash).toBe(true);
  289. expect(result.agent.frontmatter?.tools?.read).toBe(true);
  290. });
  291. it("handles missing fields with defaults", () => {
  292. const source = {};
  293. const result = engine.translateToOAC(source, "claude");
  294. expect(result.success).toBe(true);
  295. expect(result.agent.frontmatter?.name).toBe("Unnamed Agent");
  296. expect(result.agent.frontmatter?.description).toBe("Imported agent");
  297. expect(result.agent.frontmatter?.mode).toBe("primary");
  298. });
  299. it("translates context paths back to OAC format", () => {
  300. const source = {
  301. name: "Agent",
  302. contextPaths: [".claude/skills/core/standards.md"],
  303. };
  304. const result = engine.translateToOAC(source, "claude");
  305. expect(result.agent.contexts).toBeDefined();
  306. expect(result.agent.contexts?.[0].path).toBe(
  307. ".opencode/context/core/standards.md"
  308. );
  309. });
  310. it("converts Claude skills to OAC skill references", () => {
  311. const source = {
  312. name: "Agent",
  313. skills: [".claude/skills/coding.md", ".claude/skills/testing.md"],
  314. };
  315. const result = engine.translateToOAC(source, "claude");
  316. expect(result.agent.frontmatter?.skills).toHaveLength(2);
  317. expect(result.agent.frontmatter?.skills?.[0]).toBe("coding");
  318. });
  319. it("includes system prompt when provided", () => {
  320. const source = {
  321. name: "Agent",
  322. systemPrompt: "Custom prompt",
  323. };
  324. const result = engine.translateToOAC(source, "claude");
  325. expect(result.agent.systemPrompt).toBe("Custom prompt");
  326. });
  327. });
  328. // ==========================================================================
  329. // translateBatch()
  330. // ==========================================================================
  331. describe("translateBatch()", () => {
  332. it("translates multiple agents", () => {
  333. const agents = [
  334. createMinimalAgent({ frontmatter: { name: "Agent 1", description: "First", mode: "primary" } }),
  335. createMinimalAgent({ frontmatter: { name: "Agent 2", description: "Second", mode: "primary" } }),
  336. ];
  337. const results = engine.translateBatch(agents, "claude");
  338. expect(results).toHaveLength(2);
  339. expect(results[0].frontmatter.name).toBe("Agent 1");
  340. expect(results[1].frontmatter.name).toBe("Agent 2");
  341. });
  342. it("handles empty array", () => {
  343. const results = engine.translateBatch([], "claude");
  344. expect(results).toHaveLength(0);
  345. });
  346. it("processes each agent independently", () => {
  347. const agents = [
  348. createTestAgent(),
  349. createMinimalAgent(),
  350. ];
  351. const results = engine.translateBatch(agents, "claude");
  352. expect(results[0].tools).toBeDefined();
  353. expect(results[1].tools).toBeUndefined();
  354. });
  355. });
  356. // ==========================================================================
  357. // preview()
  358. // ==========================================================================
  359. describe("preview()", () => {
  360. it("returns compatibility analysis without translation", () => {
  361. const agent = createTestAgent();
  362. const result = engine.preview(agent, "cursor");
  363. expect(result).toHaveProperty("compatible");
  364. expect(result).toHaveProperty("score");
  365. expect(result).toHaveProperty("warnings");
  366. expect(result).toHaveProperty("blockers");
  367. });
  368. it("identifies incompatible agents", () => {
  369. const agent = createTestAgent({
  370. frontmatter: {
  371. name: "Agent with hooks",
  372. description: "Agent",
  373. mode: "primary",
  374. hooks: [{ event: "PreToolUse", command: "echo test" }],
  375. },
  376. });
  377. const result = engine.preview(agent, "cursor");
  378. expect(result.compatible).toBe(false);
  379. expect(result.blockers.length).toBeGreaterThan(0);
  380. });
  381. });
  382. // ==========================================================================
  383. // isFullyCompatible()
  384. // ==========================================================================
  385. describe("isFullyCompatible()", () => {
  386. it("returns true for minimal agent to Claude", () => {
  387. const agent = createMinimalAgent();
  388. expect(engine.isFullyCompatible(agent, "claude")).toBe(true);
  389. });
  390. it("returns false when features will be lost", () => {
  391. const agent = createTestAgent(); // Has temperature, contexts with priority
  392. expect(engine.isFullyCompatible(agent, "cursor")).toBe(false);
  393. });
  394. });
  395. // ==========================================================================
  396. // Factory Functions
  397. // ==========================================================================
  398. describe("createTranslationEngine()", () => {
  399. it("creates engine with default options", () => {
  400. const engine = createTranslationEngine();
  401. expect(engine).toBeInstanceOf(TranslationEngine);
  402. expect(engine.getOptions().permissionStrategy).toBe("permissive");
  403. });
  404. it("creates engine with custom options", () => {
  405. const engine = createTranslationEngine({
  406. permissionStrategy: "restrictive",
  407. });
  408. expect(engine.getOptions().permissionStrategy).toBe("restrictive");
  409. });
  410. });
  411. describe("translate() function", () => {
  412. it("performs one-off translation", () => {
  413. const agent = createMinimalAgent();
  414. const result = translate(agent, "claude");
  415. expect(result.success).toBe(true);
  416. expect(result.frontmatter.name).toBe("Minimal Agent");
  417. });
  418. it("accepts options", () => {
  419. const agent = createMinimalAgent();
  420. const result = translate(agent, "claude", {
  421. analyzeCompatibility: false,
  422. });
  423. expect(result.compatibility).toBeUndefined();
  424. });
  425. });
  426. describe("previewTranslation()", () => {
  427. it("performs one-off compatibility check", () => {
  428. const agent = createTestAgent();
  429. const result = previewTranslation(agent, "cursor");
  430. expect(result).toHaveProperty("compatible");
  431. expect(result).toHaveProperty("score");
  432. });
  433. });
  434. // ==========================================================================
  435. // Edge Cases
  436. // ==========================================================================
  437. describe("edge cases", () => {
  438. it("handles agent with no tools", () => {
  439. const agent = createMinimalAgent();
  440. const result = engine.translate(agent, "claude");
  441. expect(result.success).toBe(true);
  442. expect(result.tools).toBeUndefined();
  443. });
  444. it("handles agent with no permissions", () => {
  445. const agent = createMinimalAgent();
  446. const result = engine.translate(agent, "claude");
  447. expect(result.success).toBe(true);
  448. expect(result.permissions).toBeUndefined();
  449. });
  450. it("handles agent with no contexts", () => {
  451. const agent = createMinimalAgent({ contexts: [] });
  452. const result = engine.translate(agent, "claude");
  453. expect(result.success).toBe(true);
  454. expect(result.contextPaths).toBeUndefined();
  455. });
  456. it("handles agent with no skills", () => {
  457. const agent = createMinimalAgent();
  458. const result = engine.translate(agent, "claude");
  459. expect(result.success).toBe(true);
  460. expect(result.skills).toBeUndefined();
  461. });
  462. it("handles Windsurf target", () => {
  463. const agent = createTestAgent();
  464. const result = engine.translate(agent, "windsurf");
  465. expect(result.success).toBe(true);
  466. expect(result.frontmatter.name).toBe("Test Agent");
  467. });
  468. });
  469. });