test(native): add unit tests for loadNative() load-order and env-var override#1414
Conversation
When a locally compiled `crates/codegraph-core/*.node` binary exists it is now loaded before the published npm platform package. This ensures that Rust changes (like the prototype-method extraction from PR #1339) are picked up immediately in development without waiting for a new npm release. Load order is now: 1. NAPI_RS_NATIVE_LIBRARY_PATH env var — explicit override 2. crates/codegraph-core/<platform>.node — freshly compiled dev binary 3. @optave/codegraph-<platform> npm pkg — published production binary Closes #1361
…+ static block + field def
- Add `(class name: ...)` query patterns for JS/TS class expressions so that
`return class Foo extends Bar { ... }` records the extends relationship in
ctx.classes — previously only class_declaration was captured, leaving class
expressions invisible to resolveThisDispatch.
- Add `class_static_block` → `ClassName.<static>` synthetic method definition
in both query path (extractClassMembersWalk) and walk path (walkJavaScriptNode).
Calls inside `static { super.f(); }` blocks are now attributed to a method-kind
node so the CHA parents map can resolve `super.f()` to the parent class.
- Add `field_definition`/`public_field_definition` → `ClassName.fieldName` method
definition when the field value is an arrow function or function expression.
`static f = () => { ... }` becomes a resolvable `A.f` node so
`resolveThisDispatch` can emit the `B.<static> → A.f` edge.
- Mirror all three changes in the native Rust extractor for parity.
- Add 6 parser unit tests and import Jelly micro-test fixtures for super,
super2, super3, super4, super5 as ground-truth benchmarks.
Benchmark result: super fixture 31% → 38% recall (B.<static> → A.f now resolved).
docs check acknowledged
Closes #1377
…llbacks into buildCallEdges After resolveCallTargets returns empty: 1. Retry with class-qualified name (ClassName.method) for this-receiver calls 2. Resolve via Object.defineProperty accessor receiver typeMap or same-file lookup These two blocks existed in buildFileCallEdges (build-edges.ts) but were missing from the incremental path, causing divergence between full and watch-mode builds. Also adds the missing callerName argument to resolveCallTargets. Closes #1384
PLATFORM_PACKAGES[platformKey] was inlined in loadNative() while getNativePackageVersion() used resolvePlatformPackage(). Any future change to the lookup (fallback key, default value) would only apply to one site. Call resolvePlatformPackage() consistently in both places.
…ame in buildCallEdges Incremental rebuilds using buildCallEdges (incremental.ts) were missing two things needed for Phase 8.3f scoped-key resolution (#1358 / #1369): 1. The typeMap was not seeded with callee::restName entries from objectRestParamBindings × paramBindings. Without this seeding the scoped key `callee::restName` (e.g. `f2::rest`) is absent from the map, so resolveByMethodOrGlobal's third fallback (`typeMap.get(callerName::receiver)`) has nothing to find and the rest-param call goes unresolved. 2. caller.callerName was not passed to resolveCallTargets, so even if the scoped key was present in the typeMap (e.g. from WASM extraction), the `${callerName}::${effectiveReceiver}` lookup in resolveByMethodOrGlobal never fired. Fix: mirror what buildObjectRestParamPostPass (native full-build post-pass) and buildCallEdgesJS (WASM full-build path) already do: - Compute restNameCallees to know how many callees share a rest name. - Seed typeMap[callee::restName] for each objectRestParamBinding × paramBinding pair. - Also seed the unscoped key when only one callee uses that rest name, so resolution still works when callerName is null (findCaller couldn't match). - Pass caller.callerName to resolveCallTargets (already present from #1389). Also syncs call-resolver.ts and build-edges.ts with the scoped-key changes from PR #1368 (merged to main separately), which this branch was missing. docs check acknowledged Closes #1369
…or static blocks (#1389) The merge conflict resolution in 8d5ba14 dropped two changes from ca3123f: 1. The 'class' case in walkJavaScriptNode's switch — class expressions like 'return class Child extends Parent { ... }' were never dispatched to handleClassDecl, so ctx.classes remained empty and no extends relationship was recorded. 2. handleStaticBlock used kind: 'function' instead of kind: 'method', so the CHA parents map could not walk up to the parent class for super.method() resolution. Also update the existing test that asserted kind: 'function'.
…n with local binary (#1389)
Greptile SummaryAdds
Confidence Score: 5/5Test-only addition with no changes to production code; safe to merge. The change is entirely additive — new test file only. The five tests correctly isolate singleton state via vi.resetModules + dynamic import, assertions match the actual load-order logic in native.ts, and the feedback from previous review rounds (redundant restore, null-result cache, node: pass-through) has been incorporated. No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[loadNative called] --> B{_cached !== undefined?}
B -- Yes --> C[return _cached]
B -- No --> D{NAPI_RS_NATIVE_LIBRARY_PATH set?}
D -- Yes --> E[_require envPath]
E -- success --> F[_cached = module\nreturn module]
E -- error --> G[_cached = null\nwarn to stderr\nreturn null]
D -- No --> H{localFile defined?\nexistsSync localPath?}
H -- Yes --> I[_require localPath]
I -- success --> J[_cached = module\nreturn module]
I -- error --> K[_cached = null\nwarn\nreturn null]
H -- No --> L{Platform in PLATFORM_PACKAGES?}
L -- Yes --> M[_require pkg]
M -- success --> N[_cached = module\nreturn module]
M -- error --> O[_loadError = err\n_cached = null\nreturn null]
L -- No --> P[_loadError = unsupported platform\n_cached = null\nreturn null]
Reviews (8): Last reviewed commit: "Merge branch 'main' into test/native-loa..." | Re-trigger Greptile |
| afterEach(() => { | ||
| stderrSpy.mockRestore(); | ||
| vi.restoreAllMocks(); | ||
| vi.unstubAllEnvs(); | ||
| }); |
There was a problem hiding this comment.
Redundant spy restore:
stderrSpy.mockRestore() is called on line 57, and then vi.restoreAllMocks() is called on line 58 — which already invokes mockRestore() on every active spy, including stderrSpy. The explicit call is a no-op and can be removed to keep afterEach clean.
| afterEach(() => { | |
| stderrSpy.mockRestore(); | |
| vi.restoreAllMocks(); | |
| vi.unstubAllEnvs(); | |
| }); | |
| afterEach(() => { | |
| vi.restoreAllMocks(); | |
| vi.unstubAllEnvs(); | |
| }); |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Fixed — removed the redundant stderrSpy.mockRestore() call. vi.restoreAllMocks() already covers all active spies including stderrSpy.
| expect(result).toBeNull(); | ||
| // Only the env-path require was attempted — no fall-through to local or npm | ||
| expect(requireFn).toHaveBeenCalledTimes(1); | ||
| expect(requireFn).toHaveBeenCalledWith('/bad/path.node'); | ||
| // A warning mentioning the env var name was emitted to stderr | ||
| const stderr = stderrSpy.mock.calls.map((c) => String(c[0])).join(''); | ||
| expect(stderr).toContain('NAPI_RS_NATIVE_LIBRARY_PATH'); |
There was a problem hiding this comment.
The null-result singleton cache is exercised by test 1 (positive path) but not by this test for the negative path. A second
loadNative() call here would confirm that, after a failed env-path load, the _cached = null short-circuit fires and require is not retried — mirroring the toHaveBeenCalledTimes(1) assertion in test 1.
| expect(result).toBeNull(); | |
| // Only the env-path require was attempted — no fall-through to local or npm | |
| expect(requireFn).toHaveBeenCalledTimes(1); | |
| expect(requireFn).toHaveBeenCalledWith('/bad/path.node'); | |
| // A warning mentioning the env var name was emitted to stderr | |
| const stderr = stderrSpy.mock.calls.map((c) => String(c[0])).join(''); | |
| expect(stderr).toContain('NAPI_RS_NATIVE_LIBRARY_PATH'); | |
| expect(result).toBeNull(); | |
| // Only the env-path require was attempted — no fall-through to local or npm | |
| expect(requireFn).toHaveBeenCalledTimes(1); | |
| expect(requireFn).toHaveBeenCalledWith('/bad/path.node'); | |
| // A warning mentioning the env var name was emitted to stderr | |
| const stderr = stderrSpy.mock.calls.map((c) => String(c[0])).join(''); | |
| expect(stderr).toContain('NAPI_RS_NATIVE_LIBRARY_PATH'); | |
| // Null result is cached — second call must not invoke require again | |
| expect(loadNative()).toBeNull(); | |
| expect(requireFn).toHaveBeenCalledTimes(1); |
There was a problem hiding this comment.
Fixed — added the null-result cache second-call assertion to the bad-env-path test. A second loadNative() call now confirms requireFn is still only called once.
| localBinaryOk, | ||
| npmPackageOk, | ||
| }: { | ||
| localBinaryOk: boolean; | ||
| npmPackageOk: boolean; | ||
| }) { | ||
| return vi.fn((path: string) => { | ||
| const isAbsolute = path.startsWith('/') || /^[A-Z]:\\/.test(path); | ||
| if (isAbsolute) { | ||
| if (localBinaryOk) return FAKE_ADDON; | ||
| throw new Error(`ENOENT: no such file: ${path}`); | ||
| } | ||
| if (path.startsWith('@optave/')) { | ||
| if (npmPackageOk) return FAKE_ADDON; | ||
| throw new Error(`Cannot find module '${path}'`); | ||
| } | ||
| throw new Error(`Unexpected require call: ${path}`); | ||
| }); |
There was a problem hiding this comment.
makeMockRequire handles absolute paths and @optave/ package names, but has no branch for bare Node built-in specifiers (e.g. node:fs). In loadNative(), detectLibc() calls _require('node:fs') for the Linux platform. Because all current tests mock the platform as darwin or freebsd, detectLibc() is never triggered and the tests pass today. However, if a future test adds a Linux scenario (e.g. linux-x64-gnu), makeMockRequire will throw Unexpected require call: node:fs, producing a misleading failure.
There was a problem hiding this comment.
Fixed — added a node: pass-through branch in makeMockRequire that delegates to the real require for Node built-ins. This prevents a misleading Unexpected require call: node:fs failure if a future test exercises the Linux code path through detectLibc().
- Remove redundant stderrSpy.mockRestore() before vi.restoreAllMocks() - Add null-result cache assertion in bad-env-path test (second call must not retry require) - Add node:fs pass-through in makeMockRequire for future Linux tests
…3 and 4 Test 3 (local binary present) now asserts the result is cached on a second call — require is not re-invoked. Test 4 (npm fallback) now asserts the local binary path was attempted first (two require calls total), pinning the load-order contract.
|
Addressed the two additional issues from the Greptile review summary:
|
|
Merge conflict resolution (main → this branch): Two files had conflicts:
Test update: The
All 5 native-loader tests pass. |
Summary
tests/unit/native-loader.test.tswith 5 tests that exercise each load path inloadNative()in isolationvi.resetModules()+vi.doMock()+ dynamicimport()to get a fresh module with clean singleton state (_cached/_loadErrorreset per test)Tests added
NAPI_RS_NATIVE_LIBRARY_PATHset and validNAPI_RS_NATIVE_LIBRARY_PATHset but bad pathnull; no fall-through to local binary or npm package; WARN emitted@optave/codegraph-darwin-arm64)nullwithout any require calls;getNative()throwsEngineErrorCloses #1392