| author | |
| committer | |
| log | e52db0bbc2bc6324beec55a5082f624754c845f1 |
| tree | 194ff4536812b762637b65e31de38c635c8359ea |
| parent | 71d8f741436b69c1adc21245af6a8fd2969e467f |
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 | 171 | expect(fs.readFileSync(configPath, "utf8")).toBe("{ not-json"); |
| 172 | 172 | }); |
| 173 | 173 | |
| 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 | 199 | it("cleans up temp files when atomic keybinding write fails", () => { |
| 175 | 200 | const fakeHome = makeTempDir("t3code-keybindings-atomic-"); |
| 176 | 201 | const configDir = path.join(fakeHome, ".t3"); |
apps/server/src/keybindings.ts+13-5| ... | ... | @@ -292,6 +292,12 @@ const DEFAULT_RESOLVED_KEYBINDINGS = compileDefaultKeybindings(); |
| 292 | 292 | const KEYBINDINGS_CONFIG_PATH = path.join(".t3", "keybindings.json"); |
| 293 | 293 | const MAX_KEYBINDINGS = 256; |
| 294 | 294 | |
| 295 | class 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 | ||
| 295 | 301 | function resolveKeybindingsConfigPath(): string { |
| 296 | 302 | return path.join(os.homedir(), KEYBINDINGS_CONFIG_PATH); |
| 297 | 303 | } |
| ... | ... | @@ -308,7 +314,7 @@ function loadCustomKeybindingsConfig( |
| 308 | 314 | const parsed = JSON.parse(raw) as unknown; |
| 309 | 315 | if (!Array.isArray(parsed)) { |
| 310 | 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 | 319 | logger.warn("ignoring keybindings config with unsupported format; expected array", { |
| 314 | 320 | path: configPath, |
| ... | ... | @@ -343,10 +349,12 @@ function loadCustomKeybindingsConfig( |
| 343 | 349 | return []; |
| 344 | 350 | } |
| 345 | 351 | 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), | |
| 350 | 358 | { cause: error }, |
| 351 | 359 | ); |
| 352 | 360 | } |