import { describe, test, expect } from "vitest"; import { checkConflicts } from "./conflict-check"; import type { LexiconPlugin, CommandGroup } from "../lexicon"; // eslint-disable-next-line @typescript-eslint/no-explicit-any const mockSerializer = { name: "test", serialize: () => ({}) } as any; const noopAsync = async () => {}; function makePlugin( name: string, opts: { rules?: { id: string }[]; skills?: { name: string }[]; mcpTools?: { name: string }[]; mcpResources?: { uri: string }[]; commandGroup?: CommandGroup; } = {}, ): LexiconPlugin { const plugin: LexiconPlugin = { name, serializer: { ...mockSerializer, name }, generate: noopAsync, validate: noopAsync, coverage: noopAsync, package: noopAsync, }; if (opts.rules) { const rules = opts.rules.map((r) => ({ id: r.id, severity: "error" as const, category: "correctness" as const, check: () => [], })); // eslint-disable-next-line @typescript-eslint/no-explicit-any (plugin as any).lintRules = () => rules; } if (opts.skills) { const skills = opts.skills.map((s) => ({ name: s.name, description: "", content: "", })); // eslint-disable-next-line @typescript-eslint/no-explicit-any (plugin as any).skills = () => skills; } if (opts.mcpTools) { const tools = opts.mcpTools.map((t) => ({ name: t.name, description: "", inputSchema: { type: "object" as const, properties: {} }, handler: async () => "", })); // eslint-disable-next-line @typescript-eslint/no-explicit-any (plugin as any).mcpTools = () => tools; } if (opts.commandGroup) { const group = opts.commandGroup; plugin.commands = () => group; } if (opts.mcpResources) { const resources = opts.mcpResources.map((r) => ({ uri: r.uri, name: "", description: "", handler: async () => "", })); // eslint-disable-next-line @typescript-eslint/no-explicit-any (plugin as any).mcpResources = () => resources; } return plugin; } describe("checkConflicts", () => { // ----------------------------------------------------------------------- // No conflicts // ----------------------------------------------------------------------- test("returns clean report when no conflicts exist", () => { const plugins = [ makePlugin("aws", { rules: [{ id: "AWS001" }] }), makePlugin("gcp", { rules: [{ id: "GCP001" }] }), ]; const report = checkConflicts(plugins); expect(report.conflicts).toHaveLength(0); expect(report.warnings).toHaveLength(0); }); test("handles plugins with no optional methods", () => { const plugins = [ makePlugin("aws"), makePlugin("gcp"), ]; const report = checkConflicts(plugins); expect(report.conflicts).toHaveLength(0); expect(report.warnings).toHaveLength(0); }); // ----------------------------------------------------------------------- // Rule ID conflicts (hard) // ----------------------------------------------------------------------- test("detects duplicate rule IDs as hard conflicts", () => { const plugins = [ makePlugin("aws", { rules: [{ id: "SHARED001" }, { id: "AWS001" }] }), makePlugin("gcp", { rules: [{ id: "SHARED001" }, { id: "GCP001" }] }), ]; const report = checkConflicts(plugins); expect(report.conflicts).toHaveLength(1); expect(report.conflicts[0]).toEqual({ type: "rule-id", key: "SHARED001", plugins: ["aws", "gcp"], }); expect(report.warnings).toHaveLength(0); }); // ----------------------------------------------------------------------- // Skill conflicts (warnings) // ----------------------------------------------------------------------- test("detects duplicate skill names as warnings", () => { const plugins = [ makePlugin("aws", { skills: [{ name: "scaffold" }] }), makePlugin("gcp", { skills: [{ name: "scaffold" }] }), ]; const report = checkConflicts(plugins); expect(report.conflicts).toHaveLength(0); expect(report.warnings).toHaveLength(1); expect(report.warnings[0]).toEqual({ type: "skill-name", key: "scaffold", plugins: ["aws", "gcp"], }); }); // ----------------------------------------------------------------------- // MCP tool conflicts (warnings) // ----------------------------------------------------------------------- test("detects duplicate MCP tool names as warnings", () => { const plugins = [ makePlugin("aws", { mcpTools: [{ name: "diff" }] }), makePlugin("gcp", { mcpTools: [{ name: "diff" }] }), ]; const report = checkConflicts(plugins); expect(report.conflicts).toHaveLength(0); expect(report.warnings).toHaveLength(1); expect(report.warnings[0]).toEqual({ type: "mcp-tool", key: "diff", plugins: ["aws", "gcp"], }); }); test("no MCP tool conflict when names differ", () => { const plugins = [ makePlugin("aws", { mcpTools: [{ name: "diff" }] }), makePlugin("gcp", { mcpTools: [{ name: "deploy" }] }), ]; const report = checkConflicts(plugins); expect(report.warnings.filter((w) => w.type === "mcp-tool")).toHaveLength(0); }); test("MCP tool conflict across three plugins", () => { const plugins = [ makePlugin("aws", { mcpTools: [{ name: "scan" }] }), makePlugin("gcp", { mcpTools: [{ name: "scan" }] }), makePlugin("azure", { mcpTools: [{ name: "scan" }] }), ]; const report = checkConflicts(plugins); const toolWarnings = report.warnings.filter((w) => w.type === "mcp-tool"); expect(toolWarnings).toHaveLength(1); expect(toolWarnings[0].plugins).toEqual(["aws", "gcp", "azure"]); }); // ----------------------------------------------------------------------- // MCP resource conflicts (warnings) // ----------------------------------------------------------------------- test("detects duplicate MCP resource URIs as warnings", () => { const plugins = [ makePlugin("aws", { mcpResources: [{ uri: "catalog" }] }), makePlugin("gcp", { mcpResources: [{ uri: "catalog" }] }), ]; const report = checkConflicts(plugins); expect(report.conflicts).toHaveLength(0); expect(report.warnings).toHaveLength(1); expect(report.warnings[0]).toEqual({ type: "mcp-resource", key: "catalog", plugins: ["aws", "gcp"], }); }); test("no MCP resource conflict when URIs differ", () => { const plugins = [ makePlugin("aws", { mcpResources: [{ uri: "aws-catalog" }] }), makePlugin("gcp", { mcpResources: [{ uri: "gcp-catalog" }] }), ]; const report = checkConflicts(plugins); expect(report.warnings.filter((w) => w.type === "mcp-resource")).toHaveLength(0); }); // ----------------------------------------------------------------------- // Command-group name conflicts (hard, chant #1078) // ----------------------------------------------------------------------- test("detects two lexicons claiming the same command-group name as a hard conflict", () => { const plugins = [ makePlugin("k8s", { commandGroup: { name: "kube", description: "d", commands: [] } }), makePlugin("fly", { commandGroup: { name: "kube", description: "d2", commands: [] } }), ]; const report = checkConflicts(plugins); expect(report.conflicts).toEqual([{ type: "command-group-name", key: "kube", plugins: ["k8s", "fly"] }]); expect(report.warnings).toHaveLength(0); }); test("detects a command-group name colliding with a reserved core command word", () => { const plugins = [makePlugin("rogue", { commandGroup: { name: "build", description: "d", commands: [] } })]; const report = checkConflicts(plugins); expect(report.conflicts).toEqual([{ type: "command-group-name", key: "build", plugins: ["rogue"] }]); }); test("no conflict for a single lexicon's own, non-reserved command-group name", () => { const plugins = [ makePlugin("k8s", { commandGroup: { name: "kube", description: "d", commands: [] } }), makePlugin("aws"), ]; const report = checkConflicts(plugins); expect(report.conflicts.filter((c) => c.type === "command-group-name")).toHaveLength(0); }); // ----------------------------------------------------------------------- // Combined // ----------------------------------------------------------------------- test("detects multiple conflict types simultaneously", () => { const plugins = [ makePlugin("aws", { rules: [{ id: "DUPE_RULE" }], skills: [{ name: "scaffold" }], }), makePlugin("gcp", { rules: [{ id: "DUPE_RULE" }], skills: [{ name: "scaffold" }], }), ]; const report = checkConflicts(plugins); expect(report.conflicts).toHaveLength(1); expect(report.conflicts[0].type).toBe("rule-id"); expect(report.warnings).toHaveLength(1); expect(report.warnings.map((w) => w.type)).toContain("skill-name"); }); test("detects all four conflict types at once", () => { const plugins = [ makePlugin("aws", { rules: [{ id: "SHARED" }], skills: [{ name: "scaffold" }], mcpTools: [{ name: "scan" }], mcpResources: [{ uri: "catalog" }], }), makePlugin("gcp", { rules: [{ id: "SHARED" }], skills: [{ name: "scaffold" }], mcpTools: [{ name: "scan" }], mcpResources: [{ uri: "catalog" }], }), ]; const report = checkConflicts(plugins); expect(report.conflicts).toHaveLength(1); expect(report.warnings).toHaveLength(3); const warningTypes = report.warnings.map((w) => w.type).sort(); expect(warningTypes).toEqual(["mcp-resource", "mcp-tool", "skill-name"]); }); test("detects conflict across three plugins", () => { const plugins = [ makePlugin("aws", { rules: [{ id: "SHARED" }] }), makePlugin("gcp", { rules: [{ id: "SHARED" }] }), makePlugin("azure", { rules: [{ id: "SHARED" }] }), ]; const report = checkConflicts(plugins); expect(report.conflicts).toHaveLength(1); expect(report.conflicts[0].plugins).toEqual(["aws", "gcp", "azure"]); }); }); // --------------------------------------------------------------------------- // Cross-lexicon skill naming consistency // --------------------------------------------------------------------------- describe("skill naming consistency", () => { test("all skill names match chant-{lexicon}(-{topic})* pattern", async () => { const { readdirSync, readFileSync } = await import("fs"); const { join } = await import("path"); const lexiconsDir = join(import.meta.dirname, "../../../../lexicons"); const lexiconNames = readdirSync(lexiconsDir, { withFileTypes: true }) .filter((d) => d.isDirectory()) .map((d) => d.name); const violations: string[] = []; const namePattern = /^chant-[a-z0-9]+(-[a-z0-9]+)*$/; for (const lex of lexiconNames) { const skillsDir = join(lexiconsDir, lex, "src", "skills"); let files: string[]; try { files = readdirSync(skillsDir).filter((f: string) => f.endsWith(".md")); } catch { continue; // no skills dir } for (const file of files) { const skillName = file.replace(/\.md$/, ""); if (!namePattern.test(skillName)) { violations.push(`${lex}: ${file} → name "${skillName}" does not match pattern`); } // Verify file name starts with chant-{lexicon} if (!skillName.startsWith(`chant-${lex}`)) { // Allow k8s cross-provider skills (chant-k8s-eks, chant-k8s-aks, chant-k8s-gke) // which are valid as they belong to the k8s lexicon violations.push(`${lex}: ${file} → name should start with "chant-${lex}"`); } } } expect(violations).toEqual([]); }); });