import { describe, expect, it } from "vitest"; import { activeAttachments, applyCompletion, detectTrigger, fileItem, matchesQuery, skillItems, } from "./mentions"; describe("detectTrigger", () => { it("opens the file selector on @ at the start or after a space", () => { expect(detectTrigger("@", 1)).toEqual({ kind: "file", query: "", start: 0, }); expect(detectTrigger("look at @src/ma", 15)).toEqual({ kind: "file", query: "src/ma", start: 8, }); }); it("ignores @ glued to a word or followed by whitespace", () => { expect(detectTrigger("mail me@example", 15)).toBeNull(); expect(detectTrigger("@src done", 9)).toBeNull(); }); it("only looks before the caret", () => { expect(detectTrigger("@a @b", 2)).toEqual({ kind: "file", query: "a", start: 0, }); }); it("opens the skill selector on a leading slash only", () => { expect(detectTrigger("/", 1)).toEqual({ kind: "skill", query: "", start: 0, }); expect(detectTrigger("/qua", 4)).toEqual({ kind: "skill", query: "qua", start: 0, }); expect(detectTrigger("/quality now", 12)).toBeNull(); expect(detectTrigger("see /path", 9)).toBeNull(); }); }); describe("applyCompletion", () => { it("replaces the trigger and query, adds a space and moves the caret", () => { const trigger = detectTrigger("look at @src/ma please", 15); if (!trigger) { throw new Error("expected a trigger"); } expect( applyCompletion("look at @src/ma please", trigger, "@src/main.go"), ).toEqual({ text: "look at @src/main.go please", caret: "look at @src/main.go ".length, }); }); it("handles the slash trigger at the start", () => { expect( applyCompletion( "/qu", { kind: "skill", query: "qu", start: 0 }, "/quality", ), ).toEqual({ text: "/quality ", caret: 9, }); }); }); describe("skillItems", () => { it("merges skills and commands, skills winning, sorted by name", () => { const items = skillItems( [ { name: "quality", description: "Measure quality", path: "/p", source: "project", }, { name: "review", description: "Skill review", path: "/p2", source: "user", }, ], [ { name: "review", description: "Command review" }, { name: "compact", description: "Summarise" }, ], ); expect(items.map((i) => [i.label, i.source, i.insert])).toEqual([ ["compact", "command", "/compact"], ["quality", "skill", "/quality"], ["review", "skill", "/review"], ]); }); it("filters on label or description, case-insensitively", () => { const [item] = skillItems( [], [{ name: "compact", description: "Summarise the thread" }], ); expect(matchesQuery(item, "COMP")).toBe(true); expect(matchesQuery(item, "thread")).toBe(true); expect(matchesQuery(item, "zzz")).toBe(false); expect(matchesQuery(item, "")).toBe(true); }); }); describe("fileItem / activeAttachments", () => { it("builds an @ insertion carrying the attachment", () => { expect( fileItem({ path: "/w/src/main.go", relPath: "src/main.go" }), ).toMatchObject({ insert: "@src/main.go", attachment: { path: "/w/src/main.go", name: "src/main.go" }, }); }); it("drops attachments no longer mentioned and duplicates", () => { const attachments = [ { path: "/w/a.go", name: "a.go" }, { path: "/w/b.go", name: "b.go" }, { path: "/w/a.go", name: "a.go" }, ]; expect(activeAttachments("see @a.go", attachments)).toEqual([ { path: "/w/a.go", name: "a.go" }, ]); }); });