CapabilityMatrix.test.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. /**
  2. * Unit tests for CapabilityMatrix
  3. *
  4. * Tests feature capability matrix and compatibility analysis.
  5. */
  6. import { describe, it, expect } from "vitest";
  7. import {
  8. getCapabilityMatrix,
  9. getFeaturesByCategory,
  10. getFeatureSupport,
  11. isFeatureSupported,
  12. analyzeCompatibility,
  13. getToolCapabilities,
  14. comparePlatforms,
  15. getConversionSummary,
  16. type Platform,
  17. type FeatureCategory,
  18. type SupportLevel,
  19. } from "../../../src/core/CapabilityMatrix";
  20. import type { OpenAgent } from "../../../src/types";
  21. // =============================================================================
  22. // Test Fixtures
  23. // =============================================================================
  24. const createMinimalAgent = (overrides: Partial<OpenAgent> = {}): OpenAgent => ({
  25. frontmatter: {
  26. name: "Test Agent",
  27. description: "A test agent",
  28. mode: "primary",
  29. ...overrides.frontmatter,
  30. },
  31. systemPrompt: "You are a test agent.",
  32. contexts: [],
  33. ...overrides,
  34. });
  35. describe("CapabilityMatrix", () => {
  36. // ==========================================================================
  37. // getCapabilityMatrix
  38. // ==========================================================================
  39. describe("getCapabilityMatrix()", () => {
  40. it("returns array of feature definitions", () => {
  41. const matrix = getCapabilityMatrix();
  42. expect(Array.isArray(matrix)).toBe(true);
  43. expect(matrix.length).toBeGreaterThan(0);
  44. });
  45. it("each feature has required properties", () => {
  46. const matrix = getCapabilityMatrix();
  47. for (const feature of matrix) {
  48. expect(feature).toHaveProperty("name");
  49. expect(feature).toHaveProperty("category");
  50. expect(feature).toHaveProperty("description");
  51. expect(feature).toHaveProperty("support");
  52. expect(typeof feature.name).toBe("string");
  53. expect(typeof feature.description).toBe("string");
  54. }
  55. });
  56. it("each feature has support for all platforms", () => {
  57. const matrix = getCapabilityMatrix();
  58. const platforms: Platform[] = ["oac", "claude", "cursor", "windsurf"];
  59. for (const feature of matrix) {
  60. for (const platform of platforms) {
  61. expect(feature.support).toHaveProperty(platform);
  62. expect(["full", "partial", "none"]).toContain(feature.support[platform]);
  63. }
  64. }
  65. });
  66. it("returns a copy (immutable)", () => {
  67. const matrix1 = getCapabilityMatrix();
  68. const matrix2 = getCapabilityMatrix();
  69. expect(matrix1).not.toBe(matrix2);
  70. expect(matrix1).toEqual(matrix2);
  71. });
  72. });
  73. // ==========================================================================
  74. // getFeaturesByCategory
  75. // ==========================================================================
  76. describe("getFeaturesByCategory()", () => {
  77. it("returns features for agents category", () => {
  78. const features = getFeaturesByCategory("agents");
  79. expect(features.length).toBeGreaterThan(0);
  80. for (const feature of features) {
  81. expect(feature.category).toBe("agents");
  82. }
  83. });
  84. it("returns features for permissions category", () => {
  85. const features = getFeaturesByCategory("permissions");
  86. expect(features.length).toBeGreaterThan(0);
  87. for (const feature of features) {
  88. expect(feature.category).toBe("permissions");
  89. }
  90. });
  91. it("returns features for tools category", () => {
  92. const features = getFeaturesByCategory("tools");
  93. expect(features.length).toBeGreaterThan(0);
  94. for (const feature of features) {
  95. expect(feature.category).toBe("tools");
  96. }
  97. });
  98. it("returns features for context category", () => {
  99. const features = getFeaturesByCategory("context");
  100. expect(features.length).toBeGreaterThan(0);
  101. for (const feature of features) {
  102. expect(feature.category).toBe("context");
  103. }
  104. });
  105. it("returns features for model category", () => {
  106. const features = getFeaturesByCategory("model");
  107. expect(features.length).toBeGreaterThan(0);
  108. for (const feature of features) {
  109. expect(feature.category).toBe("model");
  110. }
  111. });
  112. it("returns features for advanced category", () => {
  113. const features = getFeaturesByCategory("advanced");
  114. expect(features.length).toBeGreaterThan(0);
  115. for (const feature of features) {
  116. expect(feature.category).toBe("advanced");
  117. }
  118. });
  119. });
  120. // ==========================================================================
  121. // getFeatureSupport
  122. // ==========================================================================
  123. describe("getFeatureSupport()", () => {
  124. it("returns support level for known feature", () => {
  125. const support = getFeatureSupport("multipleAgents", "cursor");
  126. expect(support).toBe("none");
  127. });
  128. it("returns full for OAC on most features", () => {
  129. const support = getFeatureSupport("granularPermissions", "oac");
  130. expect(support).toBe("full");
  131. });
  132. it("returns undefined for unknown feature", () => {
  133. const support = getFeatureSupport("unknownFeature", "claude");
  134. expect(support).toBeUndefined();
  135. });
  136. it("returns correct support for bashExecution (universally supported)", () => {
  137. expect(getFeatureSupport("bashExecution", "oac")).toBe("full");
  138. expect(getFeatureSupport("bashExecution", "claude")).toBe("full");
  139. expect(getFeatureSupport("bashExecution", "cursor")).toBe("full");
  140. expect(getFeatureSupport("bashExecution", "windsurf")).toBe("full");
  141. });
  142. });
  143. // ==========================================================================
  144. // isFeatureSupported
  145. // ==========================================================================
  146. describe("isFeatureSupported()", () => {
  147. it("returns true for fully supported features", () => {
  148. expect(isFeatureSupported("bashExecution", "claude")).toBe(true);
  149. });
  150. it("returns false for partially supported features", () => {
  151. expect(isFeatureSupported("temperatureControl", "cursor")).toBe(false);
  152. });
  153. it("returns false for unsupported features", () => {
  154. expect(isFeatureSupported("multipleAgents", "cursor")).toBe(false);
  155. });
  156. it("returns false for unknown features", () => {
  157. expect(isFeatureSupported("unknownFeature", "claude")).toBe(false);
  158. });
  159. });
  160. // ==========================================================================
  161. // analyzeCompatibility
  162. // ==========================================================================
  163. describe("analyzeCompatibility()", () => {
  164. describe("minimal agent analysis", () => {
  165. it("returns compatible result for minimal agent", () => {
  166. const agent = createMinimalAgent();
  167. const result = analyzeCompatibility(agent, "claude");
  168. expect(result.compatible).toBe(true);
  169. expect(result.blockers).toHaveLength(0);
  170. });
  171. it("calculates score between 0 and 100", () => {
  172. const agent = createMinimalAgent();
  173. const result = analyzeCompatibility(agent, "claude");
  174. expect(result.score).toBeGreaterThanOrEqual(0);
  175. expect(result.score).toBeLessThanOrEqual(100);
  176. });
  177. });
  178. describe("subagent mode analysis", () => {
  179. it("warns about subagent mode on unsupported platforms", () => {
  180. const agent = createMinimalAgent({
  181. frontmatter: {
  182. name: "Subagent",
  183. description: "A subagent",
  184. mode: "subagent",
  185. },
  186. });
  187. const result = analyzeCompatibility(agent, "cursor");
  188. expect(result.warnings.some((w) => w.includes("mode"))).toBe(true);
  189. expect(result.degraded).toContain("agentModes");
  190. });
  191. it("degrades subagent mode on partially supported platforms", () => {
  192. const agent = createMinimalAgent({
  193. frontmatter: {
  194. name: "Subagent",
  195. description: "A subagent",
  196. mode: "subagent",
  197. },
  198. });
  199. const result = analyzeCompatibility(agent, "windsurf");
  200. expect(result.degraded).toContain("agentModes");
  201. });
  202. });
  203. describe("temperature analysis", () => {
  204. it("warns when temperature will be ignored", () => {
  205. const agent = createMinimalAgent({
  206. frontmatter: {
  207. name: "Agent",
  208. description: "Agent with temp",
  209. mode: "primary",
  210. temperature: 0.7,
  211. },
  212. });
  213. const result = analyzeCompatibility(agent, "claude");
  214. expect(result.warnings.some((w) => w.includes("Temperature"))).toBe(true);
  215. expect(result.lost).toContain("temperatureControl");
  216. });
  217. it("degrades temperature on partial support platforms", () => {
  218. const agent = createMinimalAgent({
  219. frontmatter: {
  220. name: "Agent",
  221. description: "Agent with temp",
  222. mode: "primary",
  223. temperature: 0.7,
  224. },
  225. });
  226. const result = analyzeCompatibility(agent, "cursor");
  227. expect(result.degraded).toContain("temperatureControl");
  228. });
  229. });
  230. describe("hooks analysis", () => {
  231. it("blocks when hooks are not supported", () => {
  232. const agent = createMinimalAgent({
  233. frontmatter: {
  234. name: "Agent",
  235. description: "Agent with hooks",
  236. mode: "primary",
  237. hooks: [{ event: "PreToolUse", command: "echo hello" }],
  238. },
  239. });
  240. const result = analyzeCompatibility(agent, "cursor");
  241. expect(result.compatible).toBe(false);
  242. expect(result.blockers.some((b) => b.includes("Hooks"))).toBe(true);
  243. expect(result.lost).toContain("hooks");
  244. });
  245. it("preserves hooks on supported platforms", () => {
  246. const agent = createMinimalAgent({
  247. frontmatter: {
  248. name: "Agent",
  249. description: "Agent with hooks",
  250. mode: "primary",
  251. hooks: [{ event: "PreToolUse", command: "echo hello" }],
  252. },
  253. });
  254. const result = analyzeCompatibility(agent, "claude");
  255. expect(result.preserved).toContain("hooks");
  256. });
  257. });
  258. describe("skills analysis", () => {
  259. it("degrades skills on unsupported platforms", () => {
  260. const agent = createMinimalAgent({
  261. frontmatter: {
  262. name: "Agent",
  263. description: "Agent with skills",
  264. mode: "primary",
  265. skills: ["coding", "testing"],
  266. },
  267. });
  268. const result = analyzeCompatibility(agent, "cursor");
  269. expect(result.warnings.some((w) => w.includes("Skills"))).toBe(true);
  270. expect(result.degraded).toContain("skillsSystem");
  271. });
  272. it("preserves skills on supported platforms", () => {
  273. const agent = createMinimalAgent({
  274. frontmatter: {
  275. name: "Agent",
  276. description: "Agent with skills",
  277. mode: "primary",
  278. skills: ["coding"],
  279. },
  280. });
  281. const result = analyzeCompatibility(agent, "claude");
  282. expect(result.preserved).toContain("skillsSystem");
  283. });
  284. });
  285. describe("granular permissions analysis", () => {
  286. it("degrades granular permissions", () => {
  287. const agent = createMinimalAgent({
  288. frontmatter: {
  289. name: "Agent",
  290. description: "Agent with permissions",
  291. mode: "primary",
  292. permission: {
  293. bash: { allow: ["npm *"], deny: ["rm -rf *"] },
  294. },
  295. },
  296. });
  297. const result = analyzeCompatibility(agent, "cursor");
  298. expect(
  299. result.warnings.some((w) => w.includes("Granular permissions"))
  300. ).toBe(true);
  301. expect(result.degraded).toContain("granularPermissions");
  302. });
  303. });
  304. describe("context analysis", () => {
  305. it("degrades external context for inline-only platforms", () => {
  306. const agent = createMinimalAgent({
  307. contexts: [{ path: ".opencode/context/core/standards.md" }],
  308. });
  309. const result = analyzeCompatibility(agent, "cursor");
  310. expect(
  311. result.warnings.some((w) => w.includes("External context"))
  312. ).toBe(true);
  313. expect(result.degraded).toContain("externalContext");
  314. });
  315. it("warns about context priority loss", () => {
  316. const agent = createMinimalAgent({
  317. contexts: [
  318. { path: ".opencode/context/core/standards.md", priority: "critical" },
  319. ],
  320. });
  321. const result = analyzeCompatibility(agent, "claude");
  322. expect(
  323. result.warnings.some((w) => w.includes("priority"))
  324. ).toBe(true);
  325. expect(result.lost).toContain("contextPriority");
  326. });
  327. });
  328. describe("maxSteps analysis", () => {
  329. it("warns when maxSteps will be ignored", () => {
  330. const agent = createMinimalAgent({
  331. frontmatter: {
  332. name: "Agent",
  333. description: "Agent with maxSteps",
  334. mode: "primary",
  335. maxSteps: 100,
  336. },
  337. });
  338. const result = analyzeCompatibility(agent, "claude");
  339. expect(result.warnings.some((w) => w.includes("maxSteps"))).toBe(true);
  340. expect(result.lost).toContain("maxSteps");
  341. });
  342. });
  343. });
  344. // ==========================================================================
  345. // getToolCapabilities
  346. // ==========================================================================
  347. describe("getToolCapabilities()", () => {
  348. describe("Claude capabilities", () => {
  349. it("returns correct capabilities for Claude", () => {
  350. const caps = getToolCapabilities("claude");
  351. expect(caps.name).toBe("claude");
  352. expect(caps.displayName).toBe("Claude Code");
  353. expect(caps.supportsMultipleAgents).toBe(true);
  354. expect(caps.supportsSkills).toBe(true);
  355. expect(caps.supportsHooks).toBe(true);
  356. expect(caps.supportsGranularPermissions).toBe(false);
  357. expect(caps.configFormat).toBe("json");
  358. expect(caps.outputStructure).toBe("directory");
  359. });
  360. });
  361. describe("Cursor capabilities", () => {
  362. it("returns correct capabilities for Cursor", () => {
  363. const caps = getToolCapabilities("cursor");
  364. expect(caps.name).toBe("cursor");
  365. expect(caps.displayName).toBe("Cursor IDE");
  366. expect(caps.supportsMultipleAgents).toBe(false);
  367. expect(caps.supportsSkills).toBe(false);
  368. expect(caps.supportsHooks).toBe(false);
  369. expect(caps.configFormat).toBe("plain");
  370. expect(caps.outputStructure).toBe("single-file");
  371. });
  372. });
  373. describe("Windsurf capabilities", () => {
  374. it("returns correct capabilities for Windsurf", () => {
  375. const caps = getToolCapabilities("windsurf");
  376. expect(caps.name).toBe("windsurf");
  377. expect(caps.displayName).toBe("Windsurf");
  378. expect(caps.supportsMultipleAgents).toBe(true);
  379. expect(caps.configFormat).toBe("json");
  380. expect(caps.outputStructure).toBe("directory");
  381. });
  382. });
  383. });
  384. // ==========================================================================
  385. // comparePlatforms
  386. // ==========================================================================
  387. describe("comparePlatforms()", () => {
  388. it("compares OAC and Claude", () => {
  389. const comparison = comparePlatforms("oac", "claude");
  390. expect(comparison).toHaveProperty("identical");
  391. expect(comparison).toHaveProperty("betterInA");
  392. expect(comparison).toHaveProperty("betterInB");
  393. expect(comparison).toHaveProperty("different");
  394. expect(Array.isArray(comparison.identical)).toBe(true);
  395. });
  396. it("OAC has more features than Cursor", () => {
  397. const comparison = comparePlatforms("oac", "cursor");
  398. expect(comparison.betterInA.length).toBeGreaterThan(0);
  399. });
  400. it("identical platforms have no differences", () => {
  401. const comparison = comparePlatforms("oac", "oac");
  402. expect(comparison.betterInA).toHaveLength(0);
  403. expect(comparison.betterInB).toHaveLength(0);
  404. expect(comparison.different).toHaveLength(0);
  405. });
  406. it("comparison is symmetric", () => {
  407. const ab = comparePlatforms("claude", "cursor");
  408. const ba = comparePlatforms("cursor", "claude");
  409. expect(ab.betterInA).toEqual(ba.betterInB);
  410. expect(ab.betterInB).toEqual(ba.betterInA);
  411. });
  412. });
  413. // ==========================================================================
  414. // getConversionSummary
  415. // ==========================================================================
  416. describe("getConversionSummary()", () => {
  417. it("returns array of summary strings", () => {
  418. const summary = getConversionSummary("oac", "cursor");
  419. expect(Array.isArray(summary)).toBe(true);
  420. expect(summary.length).toBeGreaterThan(0);
  421. });
  422. it("mentions degraded features when converting to simpler platform", () => {
  423. const summary = getConversionSummary("oac", "cursor");
  424. expect(summary.some((s) => s.includes("degraded"))).toBe(true);
  425. });
  426. it("handles identical platforms", () => {
  427. const summary = getConversionSummary("oac", "oac");
  428. expect(summary.some((s) => s.includes("Full feature parity"))).toBe(true);
  429. });
  430. it("mentions enhanced features when converting to richer platform", () => {
  431. const summary = getConversionSummary("cursor", "claude");
  432. expect(
  433. summary.some((s) => s.includes("enhanced") || s.includes("parity"))
  434. ).toBe(true);
  435. });
  436. });
  437. });