|
| 1 | +/** |
| 2 | + * Unit tests for loadNative() load-order and env-var override. |
| 3 | + * |
| 4 | + * Each test uses vi.resetModules() + vi.doMock() + dynamic import() |
| 5 | + * so every test gets a fresh native module with isolated singleton state |
| 6 | + * (_cached / _loadError reset on each fresh import). |
| 7 | + */ |
| 8 | + |
| 9 | +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; |
| 10 | + |
| 11 | +// Minimal stand-in for a successfully loaded NativeAddon. |
| 12 | +const FAKE_ADDON = Object.freeze({ extractSymbols: () => [] }); |
| 13 | + |
| 14 | +/** |
| 15 | + * Build a mock require function that simulates present/absent binaries. |
| 16 | + * Distinguishes local binary calls (absolute filesystem path) from |
| 17 | + * npm package calls (package name starting with '@optave/'). |
| 18 | + */ |
| 19 | +function makeMockRequire({ |
| 20 | + localBinaryOk, |
| 21 | + npmPackageOk, |
| 22 | +}: { |
| 23 | + localBinaryOk: boolean; |
| 24 | + npmPackageOk: boolean; |
| 25 | +}) { |
| 26 | + return vi.fn((path: string) => { |
| 27 | + // Pass-through for Node built-ins (e.g. node:fs used by detectLibc on Linux) |
| 28 | + if (path.startsWith('node:')) return require(path); |
| 29 | + const isAbsolute = path.startsWith('/') || /^[A-Z]:\\/.test(path); |
| 30 | + if (isAbsolute) { |
| 31 | + if (localBinaryOk) return FAKE_ADDON; |
| 32 | + throw new Error(`ENOENT: no such file: ${path}`); |
| 33 | + } |
| 34 | + if (path.startsWith('@optave/')) { |
| 35 | + if (npmPackageOk) return FAKE_ADDON; |
| 36 | + throw new Error(`Cannot find module '${path}'`); |
| 37 | + } |
| 38 | + throw new Error(`Unexpected require call: ${path}`); |
| 39 | + }); |
| 40 | +} |
| 41 | + |
| 42 | +function mockDeps( |
| 43 | + requireFn: ReturnType<typeof vi.fn>, |
| 44 | + platform = 'darwin', |
| 45 | + arch = 'arm64', |
| 46 | + localBinaryExists = true, |
| 47 | +) { |
| 48 | + vi.doMock('node:module', () => ({ createRequire: () => requireFn })); |
| 49 | + vi.doMock('node:os', () => ({ default: { platform: () => platform, arch: () => arch } })); |
| 50 | + vi.doMock('node:fs', () => ({ |
| 51 | + existsSync: (p: string) => { |
| 52 | + // existsSync is used for the local dev binary path (absolute filesystem path) |
| 53 | + const isAbsolute = p.startsWith('/') || /^[A-Z]:\\/.test(p); |
| 54 | + if (isAbsolute) return localBinaryExists; |
| 55 | + return false; |
| 56 | + }, |
| 57 | + })); |
| 58 | +} |
| 59 | + |
| 60 | +describe('loadNative', () => { |
| 61 | + let stderrSpy: ReturnType<typeof vi.spyOn>; |
| 62 | + |
| 63 | + beforeEach(() => { |
| 64 | + vi.resetModules(); |
| 65 | + vi.unstubAllEnvs(); |
| 66 | + stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true); |
| 67 | + }); |
| 68 | + |
| 69 | + afterEach(() => { |
| 70 | + vi.restoreAllMocks(); |
| 71 | + vi.unstubAllEnvs(); |
| 72 | + }); |
| 73 | + |
| 74 | + it('NAPI_RS_NATIVE_LIBRARY_PATH set and valid: returns module and caches it', async () => { |
| 75 | + vi.stubEnv('NAPI_RS_NATIVE_LIBRARY_PATH', '/explicit/addon.node'); |
| 76 | + const requireFn = vi.fn((p: string) => { |
| 77 | + if (p === '/explicit/addon.node') return FAKE_ADDON; |
| 78 | + throw new Error(`Unexpected: ${p}`); |
| 79 | + }); |
| 80 | + mockDeps(requireFn); |
| 81 | + |
| 82 | + const { loadNative } = await import('../../src/infrastructure/native.js'); |
| 83 | + |
| 84 | + const r1 = loadNative(); |
| 85 | + const r2 = loadNative(); |
| 86 | + |
| 87 | + expect(r1).toBe(FAKE_ADDON); |
| 88 | + expect(r2).toBe(FAKE_ADDON); |
| 89 | + // require was called only once — second call hit the singleton cache |
| 90 | + expect(requireFn).toHaveBeenCalledTimes(1); |
| 91 | + expect(requireFn).toHaveBeenCalledWith('/explicit/addon.node'); |
| 92 | + }); |
| 93 | + |
| 94 | + it('NAPI_RS_NATIVE_LIBRARY_PATH set but bad path: warns, returns null, does not fall through', async () => { |
| 95 | + vi.stubEnv('NAPI_RS_NATIVE_LIBRARY_PATH', '/bad/path.node'); |
| 96 | + const requireFn = vi.fn((_p: string) => { |
| 97 | + throw new Error('ENOENT: no such file'); |
| 98 | + }); |
| 99 | + mockDeps(requireFn); |
| 100 | + |
| 101 | + const { loadNative } = await import('../../src/infrastructure/native.js'); |
| 102 | + |
| 103 | + const result = loadNative(); |
| 104 | + |
| 105 | + expect(result).toBeNull(); |
| 106 | + // Only the env-path require was attempted — no fall-through to local or npm |
| 107 | + expect(requireFn).toHaveBeenCalledTimes(1); |
| 108 | + expect(requireFn).toHaveBeenCalledWith('/bad/path.node'); |
| 109 | + // A warning mentioning the env var name was emitted to stderr |
| 110 | + const stderr = stderrSpy.mock.calls.map((c) => String(c[0])).join(''); |
| 111 | + expect(stderr).toContain('NAPI_RS_NATIVE_LIBRARY_PATH'); |
| 112 | + // Null result is cached — second call must not invoke require again |
| 113 | + expect(loadNative()).toBeNull(); |
| 114 | + expect(requireFn).toHaveBeenCalledTimes(1); |
| 115 | + }); |
| 116 | + |
| 117 | + it('no env var, local binary present: loads local binary, skips npm package', async () => { |
| 118 | + const requireFn = makeMockRequire({ localBinaryOk: true, npmPackageOk: true }); |
| 119 | + // existsSync returns true for local binary path → require is called for local binary |
| 120 | + mockDeps(requireFn, 'darwin', 'arm64', true); |
| 121 | + |
| 122 | + const { loadNative } = await import('../../src/infrastructure/native.js'); |
| 123 | + |
| 124 | + const result = loadNative(); |
| 125 | + |
| 126 | + expect(result).toBe(FAKE_ADDON); |
| 127 | + // npm package require was never attempted |
| 128 | + expect(requireFn).not.toHaveBeenCalledWith(expect.stringContaining('@optave/')); |
| 129 | + // Result is cached — second call must not invoke require again |
| 130 | + const callsBefore = requireFn.mock.calls.length; |
| 131 | + expect(loadNative()).toBe(FAKE_ADDON); |
| 132 | + expect(requireFn).toHaveBeenCalledTimes(callsBefore); |
| 133 | + }); |
| 134 | + |
| 135 | + it('no env var, no local binary, npm package present: loads npm package', async () => { |
| 136 | + const requireFn = makeMockRequire({ localBinaryOk: false, npmPackageOk: true }); |
| 137 | + // existsSync returns false → local binary require is skipped, falls through to npm package |
| 138 | + mockDeps(requireFn, 'darwin', 'arm64', false); |
| 139 | + |
| 140 | + const { loadNative } = await import('../../src/infrastructure/native.js'); |
| 141 | + |
| 142 | + const result = loadNative(); |
| 143 | + |
| 144 | + expect(result).toBe(FAKE_ADDON); |
| 145 | + // local binary was skipped (existsSync returned false), only npm package require was called |
| 146 | + expect(requireFn).toHaveBeenCalledTimes(1); |
| 147 | + expect(requireFn).toHaveBeenCalledWith('@optave/codegraph-darwin-arm64'); |
| 148 | + }); |
| 149 | + |
| 150 | + it('no env var, unsupported platform: returns null and getNative() throws EngineError', async () => { |
| 151 | + // freebsd-x64 is absent from both PLATFORM_LOCAL_BINARIES and |
| 152 | + // PLATFORM_PACKAGES, so no require calls should be made at all. |
| 153 | + const requireFn = vi.fn(() => FAKE_ADDON); |
| 154 | + mockDeps(requireFn, 'freebsd', 'x64'); |
| 155 | + |
| 156 | + const { loadNative, getNative } = await import('../../src/infrastructure/native.js'); |
| 157 | + |
| 158 | + expect(loadNative()).toBeNull(); |
| 159 | + expect(requireFn).not.toHaveBeenCalled(); |
| 160 | + expect(() => getNative()).toThrow(/Native codegraph-core not available/); |
| 161 | + }); |
| 162 | +}); |
0 commit comments