/** * Unit tests for ContextMapper * * Tests context file path mapping between OAC and other platforms. */ import { describe, it, expect } from "vitest"; import { mapContextPathFromOAC, mapContextPathToOAC, mapContextReferenceFromOAC, mapContextReferencesFromOAC, mapSkillsToClaudeFormat, mapSkillsFromClaudeFormat, getContextBaseDir, supportsExternalContext, supportsContextSubdirs, supportsContextPriority, createContextReference, normalizeContextPath, getRelativeContextPath, type ContextPlatform, } from "../../../src/mappers/ContextMapper"; import type { ContextReference, SkillReference } from "../../../src/types"; describe("ContextMapper", () => { // ========================================================================== // mapContextPathFromOAC - OAC to Platform Path Mapping // ========================================================================== describe("mapContextPathFromOAC()", () => { describe("Claude platform", () => { it("maps OAC context path to Claude skills path", () => { const result = mapContextPathFromOAC( ".opencode/context/core/standards.md", "claude" ); expect(result.path).toBe(".claude/skills/core/standards.md"); expect(result.exact).toBe(true); expect(result.warning).toBeUndefined(); }); it("preserves subdirectory structure for Claude", () => { const result = mapContextPathFromOAC( ".opencode/context/domain/auth/models.md", "claude" ); expect(result.path).toBe(".claude/skills/domain/auth/models.md"); expect(result.exact).toBe(true); }); it("handles paths without base dir prefix", () => { const result = mapContextPathFromOAC("core/standards.md", "claude"); expect(result.path).toBe(".claude/skills/core/standards.md"); expect(result.exact).toBe(true); }); }); describe("Cursor platform (inline-only)", () => { it("returns empty path with warning for Cursor", () => { const result = mapContextPathFromOAC( ".opencode/context/core/standards.md", "cursor" ); expect(result.path).toBe(""); expect(result.exact).toBe(false); expect(result.warning).toContain("does not support external context files"); }); }); describe("Windsurf platform", () => { it("maps OAC context path to Windsurf context path", () => { const result = mapContextPathFromOAC( ".opencode/context/core/standards.md", "windsurf" ); expect(result.path).toBe(".windsurf/context/core/standards.md"); expect(result.exact).toBe(true); }); it("preserves subdirectory structure for Windsurf", () => { const result = mapContextPathFromOAC( ".opencode/context/guides/development.md", "windsurf" ); expect(result.path).toBe(".windsurf/context/guides/development.md"); expect(result.exact).toBe(true); }); }); }); // ========================================================================== // mapContextPathToOAC - Platform to OAC Path Mapping // ========================================================================== describe("mapContextPathToOAC()", () => { describe("from Claude", () => { it("maps Claude skills path to OAC context path", () => { const result = mapContextPathToOAC( ".claude/skills/core/standards.md", "claude" ); expect(result.path).toBe(".opencode/context/core/standards.md"); expect(result.exact).toBe(true); }); it("handles paths without base dir prefix", () => { const result = mapContextPathToOAC("core/standards.md", "claude"); expect(result.path).toBe(".opencode/context/core/standards.md"); expect(result.exact).toBe(true); }); }); describe("from Cursor (inline-only)", () => { it("returns empty path with warning for Cursor", () => { const result = mapContextPathToOAC("some-path.md", "cursor"); expect(result.path).toBe(""); expect(result.exact).toBe(false); expect(result.warning).toContain("uses inline context"); }); }); describe("from Windsurf", () => { it("maps Windsurf context path to OAC context path", () => { const result = mapContextPathToOAC( ".windsurf/context/core/standards.md", "windsurf" ); expect(result.path).toBe(".opencode/context/core/standards.md"); expect(result.exact).toBe(true); }); }); }); // ========================================================================== // mapContextReferenceFromOAC - Context Reference Mapping // ========================================================================== describe("mapContextReferenceFromOAC()", () => { it("maps context reference without priority", () => { const context: ContextReference = { path: ".opencode/context/core/standards.md", }; const result = mapContextReferenceFromOAC(context, "claude"); expect(result.path).toBe(".claude/skills/core/standards.md"); expect(result.exact).toBe(true); expect(result.priority).toBeUndefined(); }); it("includes priority in result but warns if unsupported", () => { const context: ContextReference = { path: ".opencode/context/core/standards.md", priority: "critical", }; const result = mapContextReferenceFromOAC(context, "claude"); expect(result.path).toBe(".claude/skills/core/standards.md"); expect(result.priority).toBe("critical"); expect(result.exact).toBe(false); expect(result.warning).toContain("does not support priority metadata"); }); it("returns inline warning for Cursor", () => { const context: ContextReference = { path: ".opencode/context/core/standards.md", priority: "high", }; const result = mapContextReferenceFromOAC(context, "cursor"); expect(result.path).toBe(""); expect(result.exact).toBe(false); expect(result.warning).toBeDefined(); }); }); // ========================================================================== // mapContextReferencesFromOAC - Batch Context Reference Mapping // ========================================================================== describe("mapContextReferencesFromOAC()", () => { it("maps multiple context references", () => { const contexts: ContextReference[] = [ { path: ".opencode/context/core/standards.md" }, { path: ".opencode/context/guides/workflow.md" }, ]; const result = mapContextReferencesFromOAC(contexts, "claude"); expect(result.paths).toHaveLength(2); expect(result.paths[0]).toBe(".claude/skills/core/standards.md"); expect(result.paths[1]).toBe(".claude/skills/guides/workflow.md"); expect(result.warnings).toHaveLength(0); }); it("collects warnings from all references", () => { const contexts: ContextReference[] = [ { path: ".opencode/context/core/standards.md", priority: "critical" }, { path: ".opencode/context/guides/workflow.md", priority: "high" }, ]; const result = mapContextReferencesFromOAC(contexts, "claude"); expect(result.paths).toHaveLength(2); expect(result.warnings.length).toBeGreaterThan(0); }); it("filters out empty paths for Cursor", () => { const contexts: ContextReference[] = [ { path: ".opencode/context/core/standards.md" }, { path: ".opencode/context/guides/workflow.md" }, ]; const result = mapContextReferencesFromOAC(contexts, "cursor"); expect(result.paths).toHaveLength(0); expect(result.warnings.length).toBeGreaterThan(0); }); it("handles empty array", () => { const result = mapContextReferencesFromOAC([], "claude"); expect(result.paths).toHaveLength(0); expect(result.warnings).toHaveLength(0); }); }); // ========================================================================== // mapSkillsToClaudeFormat - OAC Skills to Claude Format // ========================================================================== describe("mapSkillsToClaudeFormat()", () => { it("maps string skill references", () => { const skills: SkillReference[] = ["coding", "testing"]; const result = mapSkillsToClaudeFormat(skills); expect(result.skills).toHaveLength(2); expect(result.skills[0]).toBe(".claude/skills/coding.md"); expect(result.skills[1]).toBe(".claude/skills/testing.md"); expect(result.warnings).toHaveLength(0); }); it("maps object skill references", () => { const skills: SkillReference[] = [ { name: "coding" }, { name: "testing" }, ]; const result = mapSkillsToClaudeFormat(skills); expect(result.skills).toHaveLength(2); expect(result.skills[0]).toBe(".claude/skills/coding.md"); expect(result.skills[1]).toBe(".claude/skills/testing.md"); }); it("warns when skill config will be ignored", () => { const skills: SkillReference[] = [ { name: "coding", config: { theme: "dark" } }, ]; const result = mapSkillsToClaudeFormat(skills); expect(result.skills).toHaveLength(1); expect(result.warnings).toHaveLength(1); expect(result.warnings[0]).toContain("config for 'coding' will be ignored"); }); it("handles mixed string and object skills", () => { const skills: SkillReference[] = [ "simple-skill", { name: "configured-skill", config: { option: true } }, ]; const result = mapSkillsToClaudeFormat(skills); expect(result.skills).toHaveLength(2); expect(result.skills[0]).toBe(".claude/skills/simple-skill.md"); expect(result.skills[1]).toBe(".claude/skills/configured-skill.md"); expect(result.warnings).toHaveLength(1); }); it("handles empty skill config without warning", () => { const skills: SkillReference[] = [{ name: "coding", config: {} }]; const result = mapSkillsToClaudeFormat(skills); expect(result.warnings).toHaveLength(0); }); }); // ========================================================================== // mapSkillsFromClaudeFormat - Claude Skills to OAC Format // ========================================================================== describe("mapSkillsFromClaudeFormat()", () => { it("extracts skill names from Claude paths", () => { const skillPaths = [".claude/skills/coding.md", ".claude/skills/testing.md"]; const result = mapSkillsFromClaudeFormat(skillPaths); expect(result).toHaveLength(2); expect(result[0]).toBe("coding"); expect(result[1]).toBe("testing"); }); it("handles nested skill paths", () => { const skillPaths = [".claude/skills/advanced/coding.md"]; const result = mapSkillsFromClaudeFormat(skillPaths); expect(result).toHaveLength(1); expect(result[0]).toBe("coding"); }); it("handles paths without expected format", () => { const skillPaths = ["some/weird/path"]; const result = mapSkillsFromClaudeFormat(skillPaths); expect(result).toHaveLength(1); // Falls back to full path when no match expect(result[0]).toBe("some/weird/path"); }); it("handles empty array", () => { const result = mapSkillsFromClaudeFormat([]); expect(result).toHaveLength(0); }); }); // ========================================================================== // Utility Functions // ========================================================================== describe("getContextBaseDir()", () => { it("returns correct base dir for OAC", () => { expect(getContextBaseDir("oac")).toBe(".opencode/context"); }); it("returns correct base dir for Claude", () => { expect(getContextBaseDir("claude")).toBe(".claude/skills"); }); it("returns empty string for Cursor (inline-only)", () => { expect(getContextBaseDir("cursor")).toBe(""); }); it("returns correct base dir for Windsurf", () => { expect(getContextBaseDir("windsurf")).toBe(".windsurf/context"); }); }); describe("supportsExternalContext()", () => { it("returns true for OAC", () => { expect(supportsExternalContext("oac")).toBe(true); }); it("returns true for Claude", () => { expect(supportsExternalContext("claude")).toBe(true); }); it("returns false for Cursor", () => { expect(supportsExternalContext("cursor")).toBe(false); }); it("returns true for Windsurf", () => { expect(supportsExternalContext("windsurf")).toBe(true); }); }); describe("supportsContextSubdirs()", () => { it("returns true for OAC", () => { expect(supportsContextSubdirs("oac")).toBe(true); }); it("returns true for Claude", () => { expect(supportsContextSubdirs("claude")).toBe(true); }); it("returns false for Cursor", () => { expect(supportsContextSubdirs("cursor")).toBe(false); }); it("returns true for Windsurf", () => { expect(supportsContextSubdirs("windsurf")).toBe(true); }); }); describe("supportsContextPriority()", () => { it("returns true only for OAC", () => { expect(supportsContextPriority("oac")).toBe(true); expect(supportsContextPriority("claude")).toBe(false); expect(supportsContextPriority("cursor")).toBe(false); expect(supportsContextPriority("windsurf")).toBe(false); }); }); describe("createContextReference()", () => { it("creates reference with path only", () => { const ref = createContextReference(".opencode/context/core/standards.md"); expect(ref.path).toBe(".opencode/context/core/standards.md"); expect(ref.priority).toBeUndefined(); expect(ref.description).toBeUndefined(); }); it("creates reference with priority", () => { const ref = createContextReference( ".opencode/context/core/standards.md", "critical" ); expect(ref.path).toBe(".opencode/context/core/standards.md"); expect(ref.priority).toBe("critical"); }); it("creates reference with all fields", () => { const ref = createContextReference( ".opencode/context/core/standards.md", "high", "Code quality standards" ); expect(ref.path).toBe(".opencode/context/core/standards.md"); expect(ref.priority).toBe("high"); expect(ref.description).toBe("Code quality standards"); }); }); describe("normalizeContextPath()", () => { it("removes duplicate slashes", () => { expect(normalizeContextPath("path//to///file.md")).toBe("path/to/file.md"); }); it("removes leading slash", () => { expect(normalizeContextPath("/path/to/file.md")).toBe("path/to/file.md"); }); it("removes trailing slash", () => { expect(normalizeContextPath("path/to/directory/")).toBe("path/to/directory"); }); it("handles combined issues", () => { expect(normalizeContextPath("//path//to///file/")).toBe("path/to/file"); }); it("returns unchanged normal path", () => { expect(normalizeContextPath("path/to/file.md")).toBe("path/to/file.md"); }); }); describe("getRelativeContextPath()", () => { it("extracts relative path for OAC", () => { const result = getRelativeContextPath( ".opencode/context/core/standards.md", "oac" ); expect(result).toBe("core/standards.md"); }); it("extracts relative path for Claude", () => { const result = getRelativeContextPath( ".claude/skills/coding/standards.md", "claude" ); expect(result).toBe("coding/standards.md"); }); it("returns full path if base dir not found", () => { const result = getRelativeContextPath("some/other/path.md", "oac"); expect(result).toBe("some/other/path.md"); }); it("handles Cursor (empty base dir)", () => { const result = getRelativeContextPath("any/path.md", "cursor"); expect(result).toBe("any/path.md"); }); }); });