authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-17 22:08:13-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-18 00:59:06-08:00
loge52db0bbc2bc6324beec55a5082f624754c845f1
tree194ff4536812b762637b65e31de38c635c8359ea
parent71d8f741436b69c1adc21245af6a8fd2969e467f

Fix keybindings parse error wrapping behavior

Co-authored-by: codex <codex@users.noreply.github.com>

2 files changed, 38 insertions(+), 5 deletions(-)

apps/server/src/keybindings.test.ts+25
......@@ -171,6 +171,31 @@ describe("server keybindings", () => {
171171 expect(fs.readFileSync(configPath, "utf8")).toBe("{ not-json");
172172 });
173173
174 it("reports non-array config parse errors without duplicate prefix", () => {
175 const fakeHome = makeTempDir("t3code-keybindings-non-array-");
176 const configDir = path.join(fakeHome, ".t3");
177 fs.mkdirSync(configDir, { recursive: true });
178 const configPath = path.join(configDir, "keybindings.json");
179 fs.writeFileSync(configPath, JSON.stringify({ key: "mod+j", command: "terminal.toggle" }), "utf8");
180 vi.spyOn(os, "homedir").mockReturnValue(fakeHome);
181
182 expect(() =>
183 upsertKeybindingRule(logger, {
184 key: "mod+shift+r",
185 command: "script.run-tests.run",
186 }),
187 ).toThrow(new RegExp(`Unable to parse keybindings config at ${configPath}: expected JSON array`));
188
189 expect(() =>
190 upsertKeybindingRule(logger, {
191 key: "mod+shift+r",
192 command: "script.run-tests.run",
193 }),
194 ).not.toThrow(
195 new RegExp(`Unable to parse keybindings config at ${configPath}: Unable to parse keybindings config at`),
196 );
197 });
198
174199 it("cleans up temp files when atomic keybinding write fails", () => {
175200 const fakeHome = makeTempDir("t3code-keybindings-atomic-");
176201 const configDir = path.join(fakeHome, ".t3");
apps/server/src/keybindings.ts+13-5
......@@ -292,6 +292,12 @@ const DEFAULT_RESOLVED_KEYBINDINGS = compileDefaultKeybindings();
292292const KEYBINDINGS_CONFIG_PATH = path.join(".t3", "keybindings.json");
293293const MAX_KEYBINDINGS = 256;
294294
295class KeybindingsConfigParseError extends Error {
296 constructor(configPath: string, detail: string, options?: { cause?: unknown }) {
297 super(`Unable to parse keybindings config at ${configPath}: ${detail}`, options);
298 }
299}
300
295301function resolveKeybindingsConfigPath(): string {
296302 return path.join(os.homedir(), KEYBINDINGS_CONFIG_PATH);
297303}
......@@ -308,7 +314,7 @@ function loadCustomKeybindingsConfig(
308314 const parsed = JSON.parse(raw) as unknown;
309315 if (!Array.isArray(parsed)) {
310316 if (options?.throwOnUnreadableConfig) {
311 throw new Error(`Unable to parse keybindings config at ${configPath}: expected JSON array`);
317 throw new KeybindingsConfigParseError(configPath, "expected JSON array");
312318 }
313319 logger.warn("ignoring keybindings config with unsupported format; expected array", {
314320 path: configPath,
......@@ -343,10 +349,12 @@ function loadCustomKeybindingsConfig(
343349 return [];
344350 }
345351 if (options?.throwOnUnreadableConfig) {
346 throw new Error(
347 `Unable to parse keybindings config at ${configPath}: ${
348 error instanceof Error ? error.message : String(error)
349 }`,
352 if (error instanceof KeybindingsConfigParseError) {
353 throw error;
354 }
355 throw new KeybindingsConfigParseError(
356 configPath,
357 error instanceof Error ? error.message : String(error),
350358 { cause: error },
351359 );
352360 }