Skip to content

Commit 3d27696

Browse files
committed
Merge remote-tracking branch 'origin/main' into test/more1-fixture-1388
2 parents 3782954 + 906a8b0 commit 3d27696

2 files changed

Lines changed: 166 additions & 0 deletions

File tree

src/domain/graph/builder/stages/build-edges.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -852,6 +852,10 @@ function buildObjectRestParamPostPass(
852852
typeMap.set(orpb.restName, { type: pb.argName, confidence: 0.65 });
853853
}
854854
}
855+
// restNames tracks every rest-parameter name found, regardless of whether the
856+
// scoped key was already in typeMap. This ensures the post-pass (below) processes
857+
// all calls whose receiver matches a known rest binding — not just those whose
858+
// typeMap entry was seeded in this iteration.
855859
restNames.add(orpb.restName);
856860
}
857861
}

tests/unit/native-loader.test.ts

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
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

Comments
 (0)