diff --git a/src/utils/modifiers.test.ts b/src/utils/modifiers.test.ts new file mode 100644 index 0000000..b17f783 --- /dev/null +++ b/src/utils/modifiers.test.ts @@ -0,0 +1,61 @@ +import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test' +import { platform } from 'os' + +const isDarwin = platform() === 'darwin' + +describe('modifiers — modifiers-napi stub fail-closed', () => { + let originalPlatform:PropertyDescriptor | undefined + let requireMock: ReturnType + + beforeEach(() => { + // Stub require('modifiers-napi') to return the reservation-stub shape + // (no exports) that's published on public npm as modifiers-napi@0.0.1. + requireMock = mock((moduleName: string) => { + if (moduleName !== 'modifiers-napi') { + throw new Error(`unexpected require: ${moduleName}`) + } + return {} as { isModifierPressed?: (m: string) => boolean } + }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ;(globalThis as any).require = requireMock + }) + + afterEach(() => { + mock.restore() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + delete (globalThis as any).require + }) + + test('isModifierPressed returns false instead of throwing when the native export is missing', async () => { + const originalPlatformDesc = Object.getOwnPropertyDescriptor(process, 'platform') + if (!isDarwin) { + Object.defineProperty(process, 'platform', { value: 'darwin', configurable: true }) + } + try { + const { isModifierPressed } = await import('./modifiers.js') + // Must not throw TypeError. Stub has no isModifierPressed export, so the + // guard path runs and yields false (modifier reported as not pressed). + expect(() => isModifierPressed('shift')).not.toThrow() + expect(isModifierPressed('shift')).toBe(false) + expect(isModifierPressed('command')).toBe(false) + } finally { + if (originalPlatformDesc) { + Object.defineProperty(process, 'platform', originalPlatformDesc) + } + } + }) + + test('isModifierPressed returns false on non-darwin platforms regardless of stub', async () => { + Object.defineProperty(process, 'platform', { value: 'linux', configurable: true }) + try { + const { isModifierPressed } = await import('./modifiers.js') + expect(isModifierPressed('shift')).toBe(false) + // The stub require() must not have been consulted on non-darwin. + expect(requireMock).toHaveBeenCalledTimes(0) + } finally { + if (originalPlatform) { + Object.defineProperty(process, 'platform', originalPlatform) + } + } + }) +}) \ No newline at end of file diff --git a/src/utils/modifiers.ts b/src/utils/modifiers.ts index 08bde4b..6f5579a 100644 --- a/src/utils/modifiers.ts +++ b/src/utils/modifiers.ts @@ -23,14 +23,24 @@ export function prewarmModifiers(): void { /** * Check if a specific modifier key is currently pressed (synchronous). + * + * `modifiers-napi` is a reserved stub (`0.0.1`, no exports) on public npm. When + * the native module is unavailable we fail closed: report the modifier as not + * pressed. That disables Apple_Terminal's best-effort Shift+Enter newline + * detection (a nicety) without breaking Enter submit (which the stub otherwise + * crashes on every keystroke). */ export function isModifierPressed(modifier: ModifierKey): boolean { if (process.platform !== 'darwin') { return false } - // Dynamic import to avoid loading native module at top level const { isModifierPressed: nativeIsModifierPressed } = // eslint-disable-next-line @typescript-eslint/no-require-imports - require('modifiers-napi') as { isModifierPressed: (m: string) => boolean } + require('modifiers-napi') as { + isModifierPressed?: (m: string) => boolean + } + if (typeof nativeIsModifierPressed !== 'function') { + return false + } return nativeIsModifierPressed(modifier) }