import { describe, it, expect } from "vitest"; import { isAuthorable, isUnderRoot, routeEdit, selectionRejection } from "../src/workspacePath"; describe("isUnderRoot", () => { const root = "/a/vscode-cowriting-plugin/sandbox"; it("accepts the root itself and files inside it", () => { expect(isUnderRoot(root, root)).toBe(true); expect(isUnderRoot(`${root}/docs/x.md`, root)).toBe(true); }); it("rejects files outside the root", () => { expect(isUnderRoot("/a/other/x.md", root)).toBe(false); }); it("rejects a SIBLING whose path is a string-prefix of the root's parent (the prefix-collision bug)", () => { // The reported case: EDH root is .../vscode-cowriting-plugin/sandbox, the file // is in the sibling content repo. Plain startsWith on the plugin-repo prefix // would falsely match; the separator boundary must reject it. const pluginRoot = "/a/vscode-cowriting-plugin"; const contentFile = "/a/vscode-cowriting-plugin-content/issues/x.md"; expect(contentFile.startsWith(pluginRoot)).toBe(true); // the latent bug expect(isUnderRoot(contentFile, pluginRoot)).toBe(false); // fixed }); }); describe("isAuthorable", () => { it("accepts file: and untitled: schemes", () => { expect(isAuthorable("file")).toBe(true); expect(isAuthorable("untitled")).toBe(true); }); it("rejects read-only / virtual schemes", () => { expect(isAuthorable("git")).toBe(false); expect(isAuthorable("output")).toBe(false); expect(isAuthorable("cowriting-baseline")).toBe(false); expect(isAuthorable("")).toBe(false); }); }); describe("selectionRejection — F8 widened (accepts out-of-folder + untitled)", () => { const ok = { hasEditor: true, selectionEmpty: false, scheme: "file" }; it("accepts an in-folder / out-of-folder file (file: scheme)", () => { expect(selectionRejection({ ...ok, scheme: "file" })).toBeNull(); }); it("accepts an untitled buffer (no longer rejected)", () => { expect(selectionRejection({ ...ok, scheme: "untitled" })).toBeNull(); }); it("names the missing editor", () => { expect(selectionRejection({ ...ok, hasEditor: false })).toMatch(/focus a text editor/i); }); it("names the empty selection (only when there IS an editor)", () => { expect(selectionRejection({ ...ok, selectionEmpty: true })).toMatch(/select some text/i); }); it("rejects a non-{file,untitled} scheme with its own message (not 'select some text')", () => { const msg = selectionRejection({ ...ok, scheme: "git" }); expect(msg).toMatch(/can.?t be edited|read-only|not a file/i); expect(msg).not.toMatch(/select some text/i); }); }); describe("routeEdit (single Ask-Claude-to-Edit gesture)", () => { const focused = { hasUri: false, uriMatchesActiveEditor: false, hasActiveEditor: true, selectionEmpty: false }; it("non-empty selection in the focused editor → selection", () => { expect(routeEdit(focused)).toBe("selection"); }); it("empty selection in the focused editor → document", () => { expect(routeEdit({ ...focused, selectionEmpty: true })).toBe("document"); }); it("no active editor → document", () => { expect(routeEdit({ ...focused, hasActiveEditor: false, selectionEmpty: true })).toBe("document"); }); it("tab right-click on a doc that ISN'T the focused editor → document (no selection to act on)", () => { expect(routeEdit({ hasUri: true, uriMatchesActiveEditor: false, hasActiveEditor: true, selectionEmpty: false })).toBe( "document", ); }); it("tab right-click on the focused editor with a selection → selection", () => { expect(routeEdit({ hasUri: true, uriMatchesActiveEditor: true, hasActiveEditor: true, selectionEmpty: false })).toBe( "selection", ); }); });