ContextMapper.test.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. /**
  2. * Unit tests for ContextMapper
  3. *
  4. * Tests context file path mapping between OAC and other platforms.
  5. */
  6. import { describe, it, expect } from "vitest";
  7. import {
  8. mapContextPathFromOAC,
  9. mapContextPathToOAC,
  10. mapContextReferenceFromOAC,
  11. mapContextReferencesFromOAC,
  12. mapSkillsToClaudeFormat,
  13. mapSkillsFromClaudeFormat,
  14. getContextBaseDir,
  15. supportsExternalContext,
  16. supportsContextSubdirs,
  17. supportsContextPriority,
  18. createContextReference,
  19. normalizeContextPath,
  20. getRelativeContextPath,
  21. type ContextPlatform,
  22. } from "../../../src/mappers/ContextMapper";
  23. import type { ContextReference, SkillReference } from "../../../src/types";
  24. describe("ContextMapper", () => {
  25. // ==========================================================================
  26. // mapContextPathFromOAC - OAC to Platform Path Mapping
  27. // ==========================================================================
  28. describe("mapContextPathFromOAC()", () => {
  29. describe("Claude platform", () => {
  30. it("maps OAC context path to Claude skills path", () => {
  31. const result = mapContextPathFromOAC(
  32. ".opencode/context/core/standards.md",
  33. "claude"
  34. );
  35. expect(result.path).toBe(".claude/skills/core/standards.md");
  36. expect(result.exact).toBe(true);
  37. expect(result.warning).toBeUndefined();
  38. });
  39. it("preserves subdirectory structure for Claude", () => {
  40. const result = mapContextPathFromOAC(
  41. ".opencode/context/domain/auth/models.md",
  42. "claude"
  43. );
  44. expect(result.path).toBe(".claude/skills/domain/auth/models.md");
  45. expect(result.exact).toBe(true);
  46. });
  47. it("handles paths without base dir prefix", () => {
  48. const result = mapContextPathFromOAC("core/standards.md", "claude");
  49. expect(result.path).toBe(".claude/skills/core/standards.md");
  50. expect(result.exact).toBe(true);
  51. });
  52. });
  53. describe("Cursor platform (inline-only)", () => {
  54. it("returns empty path with warning for Cursor", () => {
  55. const result = mapContextPathFromOAC(
  56. ".opencode/context/core/standards.md",
  57. "cursor"
  58. );
  59. expect(result.path).toBe("");
  60. expect(result.exact).toBe(false);
  61. expect(result.warning).toContain("does not support external context files");
  62. });
  63. });
  64. describe("Windsurf platform", () => {
  65. it("maps OAC context path to Windsurf context path", () => {
  66. const result = mapContextPathFromOAC(
  67. ".opencode/context/core/standards.md",
  68. "windsurf"
  69. );
  70. expect(result.path).toBe(".windsurf/context/core/standards.md");
  71. expect(result.exact).toBe(true);
  72. });
  73. it("preserves subdirectory structure for Windsurf", () => {
  74. const result = mapContextPathFromOAC(
  75. ".opencode/context/guides/development.md",
  76. "windsurf"
  77. );
  78. expect(result.path).toBe(".windsurf/context/guides/development.md");
  79. expect(result.exact).toBe(true);
  80. });
  81. });
  82. });
  83. // ==========================================================================
  84. // mapContextPathToOAC - Platform to OAC Path Mapping
  85. // ==========================================================================
  86. describe("mapContextPathToOAC()", () => {
  87. describe("from Claude", () => {
  88. it("maps Claude skills path to OAC context path", () => {
  89. const result = mapContextPathToOAC(
  90. ".claude/skills/core/standards.md",
  91. "claude"
  92. );
  93. expect(result.path).toBe(".opencode/context/core/standards.md");
  94. expect(result.exact).toBe(true);
  95. });
  96. it("handles paths without base dir prefix", () => {
  97. const result = mapContextPathToOAC("core/standards.md", "claude");
  98. expect(result.path).toBe(".opencode/context/core/standards.md");
  99. expect(result.exact).toBe(true);
  100. });
  101. });
  102. describe("from Cursor (inline-only)", () => {
  103. it("returns empty path with warning for Cursor", () => {
  104. const result = mapContextPathToOAC("some-path.md", "cursor");
  105. expect(result.path).toBe("");
  106. expect(result.exact).toBe(false);
  107. expect(result.warning).toContain("uses inline context");
  108. });
  109. });
  110. describe("from Windsurf", () => {
  111. it("maps Windsurf context path to OAC context path", () => {
  112. const result = mapContextPathToOAC(
  113. ".windsurf/context/core/standards.md",
  114. "windsurf"
  115. );
  116. expect(result.path).toBe(".opencode/context/core/standards.md");
  117. expect(result.exact).toBe(true);
  118. });
  119. });
  120. });
  121. // ==========================================================================
  122. // mapContextReferenceFromOAC - Context Reference Mapping
  123. // ==========================================================================
  124. describe("mapContextReferenceFromOAC()", () => {
  125. it("maps context reference without priority", () => {
  126. const context: ContextReference = {
  127. path: ".opencode/context/core/standards.md",
  128. };
  129. const result = mapContextReferenceFromOAC(context, "claude");
  130. expect(result.path).toBe(".claude/skills/core/standards.md");
  131. expect(result.exact).toBe(true);
  132. expect(result.priority).toBeUndefined();
  133. });
  134. it("includes priority in result but warns if unsupported", () => {
  135. const context: ContextReference = {
  136. path: ".opencode/context/core/standards.md",
  137. priority: "critical",
  138. };
  139. const result = mapContextReferenceFromOAC(context, "claude");
  140. expect(result.path).toBe(".claude/skills/core/standards.md");
  141. expect(result.priority).toBe("critical");
  142. expect(result.exact).toBe(false);
  143. expect(result.warning).toContain("does not support priority metadata");
  144. });
  145. it("returns inline warning for Cursor", () => {
  146. const context: ContextReference = {
  147. path: ".opencode/context/core/standards.md",
  148. priority: "high",
  149. };
  150. const result = mapContextReferenceFromOAC(context, "cursor");
  151. expect(result.path).toBe("");
  152. expect(result.exact).toBe(false);
  153. expect(result.warning).toBeDefined();
  154. });
  155. });
  156. // ==========================================================================
  157. // mapContextReferencesFromOAC - Batch Context Reference Mapping
  158. // ==========================================================================
  159. describe("mapContextReferencesFromOAC()", () => {
  160. it("maps multiple context references", () => {
  161. const contexts: ContextReference[] = [
  162. { path: ".opencode/context/core/standards.md" },
  163. { path: ".opencode/context/guides/workflow.md" },
  164. ];
  165. const result = mapContextReferencesFromOAC(contexts, "claude");
  166. expect(result.paths).toHaveLength(2);
  167. expect(result.paths[0]).toBe(".claude/skills/core/standards.md");
  168. expect(result.paths[1]).toBe(".claude/skills/guides/workflow.md");
  169. expect(result.warnings).toHaveLength(0);
  170. });
  171. it("collects warnings from all references", () => {
  172. const contexts: ContextReference[] = [
  173. { path: ".opencode/context/core/standards.md", priority: "critical" },
  174. { path: ".opencode/context/guides/workflow.md", priority: "high" },
  175. ];
  176. const result = mapContextReferencesFromOAC(contexts, "claude");
  177. expect(result.paths).toHaveLength(2);
  178. expect(result.warnings.length).toBeGreaterThan(0);
  179. });
  180. it("filters out empty paths for Cursor", () => {
  181. const contexts: ContextReference[] = [
  182. { path: ".opencode/context/core/standards.md" },
  183. { path: ".opencode/context/guides/workflow.md" },
  184. ];
  185. const result = mapContextReferencesFromOAC(contexts, "cursor");
  186. expect(result.paths).toHaveLength(0);
  187. expect(result.warnings.length).toBeGreaterThan(0);
  188. });
  189. it("handles empty array", () => {
  190. const result = mapContextReferencesFromOAC([], "claude");
  191. expect(result.paths).toHaveLength(0);
  192. expect(result.warnings).toHaveLength(0);
  193. });
  194. });
  195. // ==========================================================================
  196. // mapSkillsToClaudeFormat - OAC Skills to Claude Format
  197. // ==========================================================================
  198. describe("mapSkillsToClaudeFormat()", () => {
  199. it("maps string skill references", () => {
  200. const skills: SkillReference[] = ["coding", "testing"];
  201. const result = mapSkillsToClaudeFormat(skills);
  202. expect(result.skills).toHaveLength(2);
  203. expect(result.skills[0]).toBe(".claude/skills/coding.md");
  204. expect(result.skills[1]).toBe(".claude/skills/testing.md");
  205. expect(result.warnings).toHaveLength(0);
  206. });
  207. it("maps object skill references", () => {
  208. const skills: SkillReference[] = [
  209. { name: "coding" },
  210. { name: "testing" },
  211. ];
  212. const result = mapSkillsToClaudeFormat(skills);
  213. expect(result.skills).toHaveLength(2);
  214. expect(result.skills[0]).toBe(".claude/skills/coding.md");
  215. expect(result.skills[1]).toBe(".claude/skills/testing.md");
  216. });
  217. it("warns when skill config will be ignored", () => {
  218. const skills: SkillReference[] = [
  219. { name: "coding", config: { theme: "dark" } },
  220. ];
  221. const result = mapSkillsToClaudeFormat(skills);
  222. expect(result.skills).toHaveLength(1);
  223. expect(result.warnings).toHaveLength(1);
  224. expect(result.warnings[0]).toContain("config for 'coding' will be ignored");
  225. });
  226. it("handles mixed string and object skills", () => {
  227. const skills: SkillReference[] = [
  228. "simple-skill",
  229. { name: "configured-skill", config: { option: true } },
  230. ];
  231. const result = mapSkillsToClaudeFormat(skills);
  232. expect(result.skills).toHaveLength(2);
  233. expect(result.skills[0]).toBe(".claude/skills/simple-skill.md");
  234. expect(result.skills[1]).toBe(".claude/skills/configured-skill.md");
  235. expect(result.warnings).toHaveLength(1);
  236. });
  237. it("handles empty skill config without warning", () => {
  238. const skills: SkillReference[] = [{ name: "coding", config: {} }];
  239. const result = mapSkillsToClaudeFormat(skills);
  240. expect(result.warnings).toHaveLength(0);
  241. });
  242. });
  243. // ==========================================================================
  244. // mapSkillsFromClaudeFormat - Claude Skills to OAC Format
  245. // ==========================================================================
  246. describe("mapSkillsFromClaudeFormat()", () => {
  247. it("extracts skill names from Claude paths", () => {
  248. const skillPaths = [".claude/skills/coding.md", ".claude/skills/testing.md"];
  249. const result = mapSkillsFromClaudeFormat(skillPaths);
  250. expect(result).toHaveLength(2);
  251. expect(result[0]).toBe("coding");
  252. expect(result[1]).toBe("testing");
  253. });
  254. it("handles nested skill paths", () => {
  255. const skillPaths = [".claude/skills/advanced/coding.md"];
  256. const result = mapSkillsFromClaudeFormat(skillPaths);
  257. expect(result).toHaveLength(1);
  258. expect(result[0]).toBe("coding");
  259. });
  260. it("handles paths without expected format", () => {
  261. const skillPaths = ["some/weird/path"];
  262. const result = mapSkillsFromClaudeFormat(skillPaths);
  263. expect(result).toHaveLength(1);
  264. // Falls back to full path when no match
  265. expect(result[0]).toBe("some/weird/path");
  266. });
  267. it("handles empty array", () => {
  268. const result = mapSkillsFromClaudeFormat([]);
  269. expect(result).toHaveLength(0);
  270. });
  271. });
  272. // ==========================================================================
  273. // Utility Functions
  274. // ==========================================================================
  275. describe("getContextBaseDir()", () => {
  276. it("returns correct base dir for OAC", () => {
  277. expect(getContextBaseDir("oac")).toBe(".opencode/context");
  278. });
  279. it("returns correct base dir for Claude", () => {
  280. expect(getContextBaseDir("claude")).toBe(".claude/skills");
  281. });
  282. it("returns empty string for Cursor (inline-only)", () => {
  283. expect(getContextBaseDir("cursor")).toBe("");
  284. });
  285. it("returns correct base dir for Windsurf", () => {
  286. expect(getContextBaseDir("windsurf")).toBe(".windsurf/context");
  287. });
  288. });
  289. describe("supportsExternalContext()", () => {
  290. it("returns true for OAC", () => {
  291. expect(supportsExternalContext("oac")).toBe(true);
  292. });
  293. it("returns true for Claude", () => {
  294. expect(supportsExternalContext("claude")).toBe(true);
  295. });
  296. it("returns false for Cursor", () => {
  297. expect(supportsExternalContext("cursor")).toBe(false);
  298. });
  299. it("returns true for Windsurf", () => {
  300. expect(supportsExternalContext("windsurf")).toBe(true);
  301. });
  302. });
  303. describe("supportsContextSubdirs()", () => {
  304. it("returns true for OAC", () => {
  305. expect(supportsContextSubdirs("oac")).toBe(true);
  306. });
  307. it("returns true for Claude", () => {
  308. expect(supportsContextSubdirs("claude")).toBe(true);
  309. });
  310. it("returns false for Cursor", () => {
  311. expect(supportsContextSubdirs("cursor")).toBe(false);
  312. });
  313. it("returns true for Windsurf", () => {
  314. expect(supportsContextSubdirs("windsurf")).toBe(true);
  315. });
  316. });
  317. describe("supportsContextPriority()", () => {
  318. it("returns true only for OAC", () => {
  319. expect(supportsContextPriority("oac")).toBe(true);
  320. expect(supportsContextPriority("claude")).toBe(false);
  321. expect(supportsContextPriority("cursor")).toBe(false);
  322. expect(supportsContextPriority("windsurf")).toBe(false);
  323. });
  324. });
  325. describe("createContextReference()", () => {
  326. it("creates reference with path only", () => {
  327. const ref = createContextReference(".opencode/context/core/standards.md");
  328. expect(ref.path).toBe(".opencode/context/core/standards.md");
  329. expect(ref.priority).toBeUndefined();
  330. expect(ref.description).toBeUndefined();
  331. });
  332. it("creates reference with priority", () => {
  333. const ref = createContextReference(
  334. ".opencode/context/core/standards.md",
  335. "critical"
  336. );
  337. expect(ref.path).toBe(".opencode/context/core/standards.md");
  338. expect(ref.priority).toBe("critical");
  339. });
  340. it("creates reference with all fields", () => {
  341. const ref = createContextReference(
  342. ".opencode/context/core/standards.md",
  343. "high",
  344. "Code quality standards"
  345. );
  346. expect(ref.path).toBe(".opencode/context/core/standards.md");
  347. expect(ref.priority).toBe("high");
  348. expect(ref.description).toBe("Code quality standards");
  349. });
  350. });
  351. describe("normalizeContextPath()", () => {
  352. it("removes duplicate slashes", () => {
  353. expect(normalizeContextPath("path//to///file.md")).toBe("path/to/file.md");
  354. });
  355. it("removes leading slash", () => {
  356. expect(normalizeContextPath("/path/to/file.md")).toBe("path/to/file.md");
  357. });
  358. it("removes trailing slash", () => {
  359. expect(normalizeContextPath("path/to/directory/")).toBe("path/to/directory");
  360. });
  361. it("handles combined issues", () => {
  362. expect(normalizeContextPath("//path//to///file/")).toBe("path/to/file");
  363. });
  364. it("returns unchanged normal path", () => {
  365. expect(normalizeContextPath("path/to/file.md")).toBe("path/to/file.md");
  366. });
  367. });
  368. describe("getRelativeContextPath()", () => {
  369. it("extracts relative path for OAC", () => {
  370. const result = getRelativeContextPath(
  371. ".opencode/context/core/standards.md",
  372. "oac"
  373. );
  374. expect(result).toBe("core/standards.md");
  375. });
  376. it("extracts relative path for Claude", () => {
  377. const result = getRelativeContextPath(
  378. ".claude/skills/coding/standards.md",
  379. "claude"
  380. );
  381. expect(result).toBe("coding/standards.md");
  382. });
  383. it("returns full path if base dir not found", () => {
  384. const result = getRelativeContextPath("some/other/path.md", "oac");
  385. expect(result).toBe("some/other/path.md");
  386. });
  387. it("handles Cursor (empty base dir)", () => {
  388. const result = getRelativeContextPath("any/path.md", "cursor");
  389. expect(result).toBe("any/path.md");
  390. });
  391. });
  392. });