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", () => {...@@ -171,6 +171,31 @@ describe("server keybindings", () => {
171 expect(fs.readFileSync(configPath, "utf8")).toBe("{ not-json");171 expect(fs.readFileSync(configPath, "utf8")).toBe("{ not-json");
172 });172 });
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
174 it("cleans up temp files when atomic keybinding write fails", () => {199 it("cleans up temp files when atomic keybinding write fails", () => {
175 const fakeHome = makeTempDir("t3code-keybindings-atomic-");200 const fakeHome = makeTempDir("t3code-keybindings-atomic-");
176 const configDir = path.join(fakeHome, ".t3");201 const configDir = path.join(fakeHome, ".t3");
apps/server/src/keybindings.ts+13-5
...@@ -292,6 +292,12 @@ const DEFAULT_RESOLVED_KEYBINDINGS = compileDefaultKeybindings();...@@ -292,6 +292,12 @@ const DEFAULT_RESOLVED_KEYBINDINGS = compileDefaultKeybindings();
292const KEYBINDINGS_CONFIG_PATH = path.join(".t3", "keybindings.json");292const KEYBINDINGS_CONFIG_PATH = path.join(".t3", "keybindings.json");
293const MAX_KEYBINDINGS = 256;293const 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
295function resolveKeybindingsConfigPath(): string {301function resolveKeybindingsConfigPath(): string {
296 return path.join(os.homedir(), KEYBINDINGS_CONFIG_PATH);302 return path.join(os.homedir(), KEYBINDINGS_CONFIG_PATH);
297}303}
...@@ -308,7 +314,7 @@ function loadCustomKeybindingsConfig(...@@ -308,7 +314,7 @@ function loadCustomKeybindingsConfig(
308 const parsed = JSON.parse(raw) as unknown;314 const parsed = JSON.parse(raw) as unknown;
309 if (!Array.isArray(parsed)) {315 if (!Array.isArray(parsed)) {
310 if (options?.throwOnUnreadableConfig) {316 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");
312 }318 }
313 logger.warn("ignoring keybindings config with unsupported format; expected array", {319 logger.warn("ignoring keybindings config with unsupported format; expected array", {
314 path: configPath,320 path: configPath,
...@@ -343,10 +349,12 @@ function loadCustomKeybindingsConfig(...@@ -343,10 +349,12 @@ function loadCustomKeybindingsConfig(
343 return [];349 return [];
344 }350 }
345 if (options?.throwOnUnreadableConfig) {351 if (options?.throwOnUnreadableConfig) {
346 throw new Error(352 if (error instanceof KeybindingsConfigParseError) {
347 `Unable to parse keybindings config at ${configPath}: ${353 throw error;
348 error instanceof Error ? error.message : String(error)354 }
349 }`,355 throw new KeybindingsConfigParseError(
356 configPath,
357 error instanceof Error ? error.message : String(error),
350 { cause: error },358 { cause: error },
351 );359 );
352 }360 }