// @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>(), hyprlandBindings: vi.fn<() => Promise<{ modmask: number; key: string; dispatcher: string; arg: string }[]>>(), reloadHyprland: vi.fn<() => Promise>(), }; 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); });