263 lines · 12.7 KB · TypeScript
RawDownload
// @effect-diagnostics nodeBuiltinImport:off -- Isolated config fixtures, never the user's desktop.import * as NodeFSP from "node:fs/promises";import * as NodePath from "node:path";import * as NodeOS from "node:os";import * as NodeChildProcess from "node:child_process";import * as NodeUtil from "node:util";import { afterEach, beforeEach, expect, it as test, vi } from "vite-plus/test";import { CaptureShortcutConfig, niriCaptureConfigPath } from "./CaptureShortcutConfig.ts";import { captureConfigBinding } from "./captureConfigEdit.ts"; // oxlint-disable-next-line t3code/no-global-process-runtime -- Test collection checks the host before starting these Linux-native filesystem tests.const it = test.runIf(process.platform === "linux");let directory: string;let path: string;let setup: CaptureShortcutConfig;const tools = {  validateNiri: vi.fn<(path: string) => Promise<void>>(),  hyprlandBindings:    vi.fn<() => Promise<{ modmask: number; key: string; dispatcher: string; arg: string }[]>>(),  reloadHyprland: vi.fn<() => Promise<void>>(),};const appId = "com.t3tools.T3Code";const install = { operation: "install", chooseFile: false } as const;const target = () => ({ desktop: "niri" as const, path, appId });beforeEach(async () => {  directory = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-capture-config-"));  path = NodePath.join(directory, "config.kdl");  await NodeFSP.writeFile(path, "binds {\n    Mod+Q { quit; }\n}\n", { mode: 0o640 });  tools.validateNiri.mockReset().mockResolvedValue(undefined);  tools.hyprlandBindings.mockReset().mockResolvedValue([]);  tools.reloadHyprland.mockReset().mockResolvedValue(undefined);  setup = new CaptureShortcutConfig(tools);});afterEach(async () => {  await NodeFSP.rm(directory, { recursive: true, force: true });}); it("reads without writing, then applies only the approved diff with a backup", async () => {  const before = await NodeFSP.readFile(path, "utf8");  const preview = await setup.preview(target(), install);  expect(await NodeFSP.readFile(path, "utf8")).toBe(before);  expect(await NodeFSP.readdir(directory)).toEqual(["config.kdl"]);  expect(tools.validateNiri).not.toHaveBeenCalled();  expect(preview.before).toBe(before);  expect(preview.after).toContain("Ctrl+Shift+2");  const applied = await setup.apply(preview.id, "niri");  expect(await NodeFSP.readFile(path, "utf8")).toBe(preview.after);  expect(await NodeFSP.readFile(applied.backupPath!, "utf8")).toBe(before);  expect((await NodeFSP.stat(path)).mode & 0o777).toBe(0o640);  expect(tools.validateNiri).toHaveBeenCalledOnce();  await expect(setup.apply(preview.id, "niri")).rejects.toThrow("expired");});it("refuses apply without a preview or for the wrong desktop", async () => {  await expect(setup.apply("invented", "niri")).rejects.toThrow("expired");  const preview = await setup.preview(target(), install);  await expect(setup.apply(preview.id, "hyprland")).rejects.toThrow("expired");  expect(await NodeFSP.readFile(path, "utf8")).toBe(preview.before);});it("rechecks Hyprland conflicts before writing", async () => {  const hypr = {    ...target(),    desktop: "hyprland" as const,    path: NodePath.join(directory, "hyprland.conf"),  };  await NodeFSP.writeFile(hypr.path, "# unchanged\n");  const preview = await setup.preview(hypr, install);  tools.hyprlandBindings.mockResolvedValue([    { modmask: 5, key: "2", dispatcher: "exec", arg: "kitty" },  ]);  await expect(setup.apply(preview.id, "hyprland")).rejects.toThrow("already used");  expect(await NodeFSP.readFile(hypr.path, "utf8")).toBe(preview.before);});it("notices an optional include created while the diff is open", async () => {  await NodeFSP.appendFile(path, 'include optional=true "later.kdl"\n');  const preview = await setup.preview(target(), install);  await NodeFSP.writeFile(    NodePath.join(directory, "later.kdl"),    "binds {\n Ctrl+Shift+2 { quit; }\n}\n",  );  await expect(setup.apply(preview.id, "niri")).rejects.toThrow("changed since");  expect(await NodeFSP.readFile(path, "utf8")).toBe(preview.before);});it("invalidates an earlier proposal when another config is opened", async () => {  const first = await setup.preview(target(), install);  await setup.preview(target(), { ...install, shortcut: "Ctrl+Alt+Y" });  await expect(setup.apply(first.id, "niri")).rejects.toThrow("expired");});it("does not overwrite edits made after preview", async () => {  const preview = await setup.preview(target(), install);  await NodeFSP.appendFile(path, "// edited elsewhere\n");  await expect(setup.apply(preview.id, "niri")).rejects.toThrow("changed since");  expect(await NodeFSP.readFile(path, "utf8")).toBe(preview.before + "// edited elsewhere\n");  expect(await NodeFSP.readdir(directory)).toEqual(["config.kdl"]);});it("rechecks after validation and cleans the staged file on concurrent edits", async () => {  const preview = await setup.preview(target(), install);  tools.validateNiri.mockImplementation(async () => {    await NodeFSP.appendFile(path, "// keep this\n");  });  await expect(setup.apply(preview.id, "niri")).rejects.toThrow("changed since");  expect(await NodeFSP.readdir(directory)).toEqual(["config.kdl"]);  expect(await NodeFSP.readFile(path, "utf8")).toContain("keep this");});it("preserves symlink-managed dotfiles and identifies their actual target", async () => {  const link = NodePath.join(directory, "linked.kdl");  await NodeFSP.symlink(path, link);  const preview = await setup.preview({ ...target(), path: link }, install);  expect(preview.path).toBe(link);  expect(preview.resolvedPath).toBe(path);  await setup.apply(preview.id, "niri");  expect((await NodeFSP.lstat(link)).isSymbolicLink()).toBe(true);  expect(await NodeFSP.readFile(link, "utf8")).toBe(preview.after);});it("refuses a symlink retargeted since preview", async () => {  const link = NodePath.join(directory, "linked.kdl");  const other = NodePath.join(directory, "other.kdl");  await NodeFSP.writeFile(other, "binds {}\n");  await NodeFSP.symlink(path, link);  const preview = await setup.preview({ ...target(), path: link }, install);  await NodeFSP.unlink(link);  await NodeFSP.symlink(other, link);  await expect(setup.apply(preview.id, "niri")).rejects.toThrow("changed since");  expect(await NodeFSP.readFile(other, "utf8")).toBe("binds {}\n");});it("validates symlinked configs from their original directory so relative includes stay valid", async () => {  const active = NodePath.join(directory, "active");  await NodeFSP.mkdir(active);  const link = NodePath.join(active, "config.kdl");  await NodeFSP.symlink(path, link);  await NodeFSP.appendFile(path, 'include "keys.kdl"\n');  await NodeFSP.writeFile(NodePath.join(active, "keys.kdl"), "binds {}\n");  tools.validateNiri.mockImplementation(async (file) => {    expect(NodePath.dirname(file)).toBe(active);    expect(await NodeFSP.readFile(file, "utf8")).toContain("Ctrl+Shift+2");    expect(await NodeFSP.readFile(NodePath.join(NodePath.dirname(file), "keys.kdl"), "utf8")).toBe(      "binds {}\n",    );  });  const preview = await setup.preview({ ...target(), path: link }, install);  await setup.apply(preview.id, "niri");  expect((await NodeFSP.readdir(active)).sort()).toEqual(["config.kdl", "keys.kdl"]);  expect((await NodeFSP.lstat(link)).isSymbolicLink()).toBe(true);});it("does not apply invalid Niri configs", async () => {  const preview = await setup.preview(target(), install);  tools.validateNiri.mockRejectedValue(new Error("Invalid config"));  await expect(setup.apply(preview.id, "niri")).rejects.toThrow("Invalid config");  expect(await NodeFSP.readFile(path, "utf8")).toBe(preview.before);  expect(await NodeFSP.readdir(directory)).toEqual(["config.kdl"]);});it("leaves already configured files unchanged, without creating a backup", async () => {  await NodeFSP.writeFile(path, `binds { ${captureConfigBinding("niri", appId, "Ctrl+Alt+Y")} }`);  const preview = await setup.preview(target(), install);  expect(preview.after).toBe(preview.before);  expect(preview.shortcut).toBe("Ctrl+Alt+Y");  expect(await setup.apply(preview.id, "niri")).toEqual({ backupPath: null, warning: null });  expect(await NodeFSP.readdir(directory)).toEqual(["config.kdl"]);});it("detects conflicts in included Niri configs without changing either file", async () => {  await NodeFSP.appendFile(path, 'include "keys.kdl"\n');  await NodeFSP.writeFile(NodePath.join(directory, "keys.kdl"), "binds { Shift+Ctrl+2 { quit; } }");  await expect(setup.preview(target(), install)).rejects.toThrow("already used");  expect((await NodeFSP.readdir(directory)).sort()).toEqual(["config.kdl", "keys.kdl"]);});it("detects included config changes before applying", async () => {  await NodeFSP.appendFile(path, 'include "keys.kdl"\n');  const included = NodePath.join(directory, "keys.kdl");  await NodeFSP.writeFile(included, "binds { Ctrl+Alt+Q { quit; } }");  const preview = await setup.preview(target(), install);  await NodeFSP.writeFile(included, "binds { Ctrl+Shift+2 { quit; } }");  await expect(setup.apply(preview.id, "niri")).rejects.toThrow("changed since");});it("rejects a conflict from another Hyprland config or dynamic Lua bind", async () => {  const hypr = {    ...target(),    desktop: "hyprland" as const,    path: NodePath.join(directory, "bindings.lua"),  };  await NodeFSP.writeFile(hypr.path, "-- user bindings\n");  tools.hyprlandBindings.mockResolvedValue([    { modmask: 5, key: "2", dispatcher: "exec", arg: "kitty" },  ]);  await expect(setup.preview(hypr, install)).rejects.toThrow("already used");  expect(await NodeFSP.readFile(hypr.path, "utf8")).toBe("-- user bindings\n");});it("reloads Hyprland after apply, but not after read", async () => {  const hypr = {    ...target(),    desktop: "hyprland" as const,    path: NodePath.join(directory, "hyprland.conf"),  };  await NodeFSP.writeFile(hypr.path, "# preferences\n");  const preview = await setup.preview(hypr, install);  expect(tools.reloadHyprland).not.toHaveBeenCalled();  await setup.apply(preview.id, "hyprland");  expect(tools.reloadHyprland).toHaveBeenCalledOnce();});it("reports reload failure separately from a successfully saved config", async () => {  const hypr = {    ...target(),    desktop: "hyprland" as const,    path: NodePath.join(directory, "hyprland.conf"),  };  await NodeFSP.writeFile(hypr.path, "# preferences\n");  const preview = await setup.preview(hypr, install);  tools.reloadHyprland.mockRejectedValue(new Error("Compositor stopped"));  const result = await setup.apply(preview.id, "hyprland");  expect(result.warning).toContain("Config saved");  expect(await NodeFSP.readFile(hypr.path, "utf8")).toBe(preview.after);});it("does not edit Omarchy's shipped defaults", async () => {  const defaults = NodePath.join(directory, "omarchy", "default", "hypr");  await NodeFSP.mkdir(defaults, { recursive: true });  const file = NodePath.join(defaults, "bindings.conf");  await NodeFSP.writeFile(file, "# defaults\n");  await expect(    setup.preview({ ...target(), desktop: "hyprland", path: file }, install),  ).rejects.toThrow("not system or Omarchy");});it("honors NIRI_CONFIG and XDG_CONFIG_HOME", () => {  expect(    niriCaptureConfigPath(      { NIRI_CONFIG: "/custom/test.kdl", XDG_CONFIG_HOME: "/config" },      "/home/test",    ),  ).toBe("/custom/test.kdl");  expect(niriCaptureConfigPath({ XDG_CONFIG_HOME: "/config" }, "/home/test")).toBe(    "/config/niri/config.kdl",  );  expect(niriCaptureConfigPath({}, "/home/test")).toBe("/home/test/.config/niri/config.kdl");});it("preserves a UTF-8 BOM through preview and apply", async () => {  await NodeFSP.writeFile(path, "\uFEFFbinds {}\r\n");  const preview = await setup.preview(target(), install);  await setup.apply(preview.id, "niri");  expect((await NodeFSP.readFile(path)).subarray(0, 3)).toEqual(Buffer.from([0xef, 0xbb, 0xbf]));});it("the installed Niri parser accepts the generated edit and its relative includes", async (context) => {  try {    await NodeUtil.promisify(NodeChildProcess.execFile)("niri", ["--version"]);  } catch (error) {    if ((error as NodeJS.ErrnoException).code === "ENOENT") {      context.skip();      return;    }    throw error;  }  // No compositor is launched: validate only parses the temporary fixture.  await NodeFSP.appendFile(path, 'include "keys.kdl"\n');  await NodeFSP.writeFile(    NodePath.join(directory, "keys.kdl"),    "binds {\n    Ctrl+Alt+Q { quit; }\n}\n",  );  const checked = new CaptureShortcutConfig({    ...tools,    validateNiri: async (file) => {      await NodeUtil.promisify(NodeChildProcess.execFile)("niri", ["validate", "--config", file]);    },  });  const preview = await checked.preview(target(), install);  await checked.apply(preview.id, "niri");  expect(await NodeFSP.readFile(path, "utf8")).toBe(preview.after);});