import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { assert, afterEach, describe, expect, it, vi } from "vitest"; import { compileResolvedKeybindingRule, parseKeybindingShortcut, upsertKeybindingRule, } from "./keybindings"; describe("server keybindings", () => { const tempDirs: string[] = []; const logger = { warn: vi.fn(), }; function makeTempDir(prefix: string): string { const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); tempDirs.push(dir); return dir; } afterEach(() => { vi.restoreAllMocks(); logger.warn.mockReset(); for (const dir of tempDirs.splice(0, tempDirs.length)) { fs.rmSync(dir, { recursive: true, force: true }); } }); it("parses shortcuts including plus key", () => { assert.deepEqual(parseKeybindingShortcut("mod+j"), { key: "j", metaKey: false, ctrlKey: false, shiftKey: false, altKey: false, modKey: true, }); assert.deepEqual(parseKeybindingShortcut("mod++"), { key: "+", metaKey: false, ctrlKey: false, shiftKey: false, altKey: false, modKey: true, }); }); it("compiles valid rule with parsed when AST", () => { const compiled = compileResolvedKeybindingRule({ key: "mod+d", command: "terminal.split", when: "terminalOpen && !terminalFocus", }); assert.deepEqual(compiled, { command: "terminal.split", shortcut: { key: "d", metaKey: false, ctrlKey: false, shiftKey: false, altKey: false, modKey: true, }, whenAst: { type: "and", left: { type: "identifier", name: "terminalOpen" }, right: { type: "not", node: { type: "identifier", name: "terminalFocus" }, }, }, }); }); it("rejects invalid rules", () => { assert.isNull( compileResolvedKeybindingRule({ key: "mod+shift+d+o", command: "terminal.new", }), ); assert.isNull( compileResolvedKeybindingRule({ key: "mod+d", command: "terminal.split", when: "terminalFocus && (", }), ); }); it("upserts custom keybindings to ~/.t3/keybindings.json", () => { const fakeHome = makeTempDir("t3code-keybindings-upsert-"); const configDir = path.join(fakeHome, ".t3"); fs.mkdirSync(configDir, { recursive: true }); const configPath = path.join(configDir, "keybindings.json"); fs.writeFileSync( configPath, JSON.stringify([{ key: "mod+j", command: "terminal.toggle" }]), "utf8", ); vi.spyOn(os, "homedir").mockReturnValue(fakeHome); const resolved = upsertKeybindingRule(logger, { key: "mod+shift+r", command: "script.run-tests.run", }); const persisted = JSON.parse(fs.readFileSync(configPath, "utf8")) as Array<{ key: string; command: string; }>; expect(persisted).toEqual([ { key: "mod+j", command: "terminal.toggle" }, { key: "mod+shift+r", command: "script.run-tests.run" }, ]); expect(resolved.some((entry) => entry.command === "script.run-tests.run")).toBe(true); }); it("replaces existing custom keybinding for the same command", () => { const fakeHome = makeTempDir("t3code-keybindings-upsert-replace-"); const configDir = path.join(fakeHome, ".t3"); fs.mkdirSync(configDir, { recursive: true }); const configPath = path.join(configDir, "keybindings.json"); fs.writeFileSync( configPath, JSON.stringify([{ key: "mod+r", command: "script.run-tests.run" }]), "utf8", ); vi.spyOn(os, "homedir").mockReturnValue(fakeHome); upsertKeybindingRule(logger, { key: "mod+shift+r", command: "script.run-tests.run", }); const persisted = JSON.parse(fs.readFileSync(configPath, "utf8")) as Array<{ key: string; command: string; }>; expect(persisted).toEqual([{ key: "mod+shift+r", command: "script.run-tests.run" }]); }); it("refuses to overwrite malformed keybindings config", () => { const fakeHome = makeTempDir("t3code-keybindings-malformed-"); const configDir = path.join(fakeHome, ".t3"); fs.mkdirSync(configDir, { recursive: true }); const configPath = path.join(configDir, "keybindings.json"); fs.writeFileSync(configPath, "{ not-json", "utf8"); vi.spyOn(os, "homedir").mockReturnValue(fakeHome); expect(() => upsertKeybindingRule(logger, { key: "mod+shift+r", command: "script.run-tests.run", }), ).toThrow(/Unable to parse keybindings config/); expect(fs.readFileSync(configPath, "utf8")).toBe("{ not-json"); }); it("cleans up temp files when atomic keybinding write fails", () => { const fakeHome = makeTempDir("t3code-keybindings-atomic-"); const configDir = path.join(fakeHome, ".t3"); fs.mkdirSync(configDir, { recursive: true }); const configPath = path.join(configDir, "keybindings.json"); fs.writeFileSync(configPath, JSON.stringify([{ key: "mod+j", command: "terminal.toggle" }]), "utf8"); vi.spyOn(os, "homedir").mockReturnValue(fakeHome); vi.spyOn(fs, "renameSync").mockImplementation(() => { throw new Error("rename failed"); }); expect(() => upsertKeybindingRule(logger, { key: "mod+shift+r", command: "script.run-tests.run", }), ).toThrow(/rename failed/); expect(fs.readFileSync(configPath, "utf8")).toBe( JSON.stringify([{ key: "mod+j", command: "terminal.toggle" }]), ); const tempFiles = fs .readdirSync(configDir) .filter((file) => file.includes("keybindings.json.") && file.endsWith(".tmp")); expect(tempFiles).toEqual([]); }); });