From e52db0bbc2bc6324beec55a5082f624754c845f1 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 17 Feb 2026 22:08:13 -0800 Subject: [PATCH] Fix keybindings parse error wrapping behavior Co-authored-by: codex --- apps/server/src/keybindings.test.ts | 25 +++++++++++++++++++++++++ apps/server/src/keybindings.ts | 18 +++++++++++++----- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/apps/server/src/keybindings.test.ts b/apps/server/src/keybindings.test.ts index 3bd533da95c329ee0d7a29370a2341a38efd920c..73a8ce667e4486893daf8cf9952eb2ebc167f73c 100644 --- a/apps/server/src/keybindings.test.ts +++ b/apps/server/src/keybindings.test.ts @@ -171,6 +171,31 @@ describe("server keybindings", () => { expect(fs.readFileSync(configPath, "utf8")).toBe("{ not-json"); }); + it("reports non-array config parse errors without duplicate prefix", () => { + const fakeHome = makeTempDir("t3code-keybindings-non-array-"); + 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); + + expect(() => + upsertKeybindingRule(logger, { + key: "mod+shift+r", + command: "script.run-tests.run", + }), + ).toThrow(new RegExp(`Unable to parse keybindings config at ${configPath}: expected JSON array`)); + + expect(() => + upsertKeybindingRule(logger, { + key: "mod+shift+r", + command: "script.run-tests.run", + }), + ).not.toThrow( + new RegExp(`Unable to parse keybindings config at ${configPath}: Unable to parse keybindings config at`), + ); + }); + it("cleans up temp files when atomic keybinding write fails", () => { const fakeHome = makeTempDir("t3code-keybindings-atomic-"); const configDir = path.join(fakeHome, ".t3"); diff --git a/apps/server/src/keybindings.ts b/apps/server/src/keybindings.ts index edbb08794c519cb03ad1ce4840ad966e4776ddc9..dd5e401e9e756e060ff554b943efd0565f0f6a30 100644 --- a/apps/server/src/keybindings.ts +++ b/apps/server/src/keybindings.ts @@ -292,6 +292,12 @@ const DEFAULT_RESOLVED_KEYBINDINGS = compileDefaultKeybindings(); const KEYBINDINGS_CONFIG_PATH = path.join(".t3", "keybindings.json"); const MAX_KEYBINDINGS = 256; +class KeybindingsConfigParseError extends Error { + constructor(configPath: string, detail: string, options?: { cause?: unknown }) { + super(`Unable to parse keybindings config at ${configPath}: ${detail}`, options); + } +} + function resolveKeybindingsConfigPath(): string { return path.join(os.homedir(), KEYBINDINGS_CONFIG_PATH); } @@ -308,7 +314,7 @@ function loadCustomKeybindingsConfig( const parsed = JSON.parse(raw) as unknown; if (!Array.isArray(parsed)) { if (options?.throwOnUnreadableConfig) { - throw new Error(`Unable to parse keybindings config at ${configPath}: expected JSON array`); + throw new KeybindingsConfigParseError(configPath, "expected JSON array"); } logger.warn("ignoring keybindings config with unsupported format; expected array", { path: configPath, @@ -343,10 +349,12 @@ function loadCustomKeybindingsConfig( return []; } if (options?.throwOnUnreadableConfig) { - throw new Error( - `Unable to parse keybindings config at ${configPath}: ${ - error instanceof Error ? error.message : String(error) - }`, + if (error instanceof KeybindingsConfigParseError) { + throw error; + } + throw new KeybindingsConfigParseError( + configPath, + error instanceof Error ? error.message : String(error), { cause: error }, ); } -- 2.54.0