GitNexus/gitnexus/test/unit/onnxruntime-node-resolver.test.ts
ACT900 5aada28da5
fix(embeddings): use system-matched onnxruntime-node CUDA build so CUDA 13 hosts use the GPU (#2341)
* fix(embeddings): use system-matched onnxruntime-node CUDA build so CUDA 13 hosts use the GPU

transformers.js exact-pins a CUDA-12 onnxruntime-node while gitnexus' own dep floats to a CUDA-13 build. npm/pnpm cannot dedupe an exact pin against a range, so npm i -g installs two copies and the gitnexus overrides block (root-only) is inert. On a CUDA-13-only host the nested CUDA-12 provider cannot load libcublasLt.so.12, the CUDA EP fails, and embeddings silently fall back to CPU (isCudaAvailable() also only probed .so.12).

Add onnxruntime-node-resolver.ts (module.registerHooks redirect to the host-matching build, no-op elsewhere) mirroring onnxruntime-common-resolver.ts; probe libcublasLt .so.12 OR .so.13 against the copy that actually loads; unit test with 12 cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(embeddings): wire CUDA-13 build-match resolver into MCP query embedder

The MCP query-time embedder (src/mcp/core/embedder.ts) has its own,
separate initEmbedder() used for semantic search — it only called
ensureOnnxRuntimeCommonResolvable() before importing transformers.js, so
the CUDA-13 build-matching redirect added for the analyze/CLI embedder
never applied here. A CUDA-13 host running MCP search with
--embedding-device cuda still loaded the mismatched default onnxruntime-node
build.

Wire ensureOnnxRuntimeNodeMatchesSystem() into the same call site, mirroring
the core embedder's ordering (registered after the common-resolver fallback,
before the dynamic transformers import).

* fix(embeddings): gate CUDA redirect decision on registerHooks availability

decide() computed the CUDA-major redirect independent of whether Node's
module.registerHooks API actually exists — only ensureOnnxRuntimeNodeMatchesSystem()
checked that. On Node 22.0-22.14 (allowed by this package's engines
floor; registerHooks needs >=22.15), isCudaAvailable() could therefore
report a redirect target that ensureOnnxRuntimeNodeMatchesSystem() then
silently failed to install, so transformers.js loaded the mismatched
default onnxruntime-node build while the embedder still requested
device:'cuda' against it — reintroducing the uncatchable native crash
this probe exists to prevent.

Move the registerHooks check to the top of decide() so the probe and the
loader can never disagree, and skip CUDA-major probing entirely in that
case (a redirect could never install anyway).

Also fixes a related test-helper bug found while writing this unit's
tests: loadResolver's destructuring default (`registerHooks = vi.fn()`)
silently substituted a real mock function even when a test passed
`registerHooks: undefined` to simulate Node < 22.15 — meaning the
existing 'no-ops... when registerHooks is unavailable' test never
actually exercised that path. Distinguish 'omitted' from 'explicitly
undefined' via an 'in' check.

* test(embeddings): drive decide() -> redirect:true and assert the resolve() closure

The PR's actual shipped behavior — the installed registerHooks resolve()
closure, and the full redirect-active decision path — had zero executed
test coverage. All 4 prior ensureOnnxRuntimeNodeMatchesSystem tests avoided
driving decide() into redirect:true because require.resolve/createRequire
were never mocked, so the two-distinct-directory comparison decide()
depends on always resolved against whatever's actually installed in this
test's real node_modules (a single real copy, not the PR's two-copy
scenario).

Extend loadResolver()'s existing node:module mock to also fake createRequire,
keyed by call origin, so resolveOurOrtNodeDir/resolveDefaultOrtNodeDir can be
driven to two distinct fake directories with distinct CUDA majors — reaching
redirect:true without adding any injection points to production code. Then
capture the installed resolve() closure (mirroring the sibling
onnxruntime-common-resolver.test.ts's captureResolve() pattern) and assert
its three branches directly: onnxruntime-node redirect, onnxruntime-common
redirect, and passthrough for any other specifier.

* fix(embeddings): distinguish ldd detection-failure from no-CUDA-provider

ortCudaMajor treated any execFileSync('ldd', ...) failure with no usable
stdout (missing ldd binary, permission-denied .so, sandboxed exec)
identically to 'CUDA provider genuinely absent'. The pre-PR detection
(hasOrtCudaProvider) only used existsSync, never ldd, so this is a
regression: a CUDA-12 host that worked fine before this PR can now
silently fall back to CPU if ldd itself can't run, even though the
provider .so and system CUDA libs are both genuinely present.

readSoNeeded now reports whether ldd produced any usable output at all,
distinct from 'ldd ran and just found no matching NEEDED entry' (the
existing, already-handled '=> not found' case). When detection genuinely
fails, log a warning so an operator can tell 'CPU fallback because
detection itself failed' apart from 'CPU fallback because no CUDA build
shipped' — the return value stays null either way (the type can't
distinguish a third state), but the two cases are now observably
different via the log.

* fix(embeddings): check ourDir independently of whether defaultDir resolved

decide()'s ourDir fallback lookup was nested inside
'if (systemMajor != null && defaultDir)', so a null defaultDir (transformers'
own onnxruntime-node resolution failing outright, e.g. a partial/broken
install) skipped checking ourDir entirely — getEffectiveOnnxRuntimeNodeDir()
returned null even when gitnexus' own matching CUDA-13 copy would have
resolved fine and worked.

defaultDir resolving is not a precondition for the comparison: an
unresolvable default already counts as 'the default doesn't match', so the
ourDir check now runs whenever systemMajor is known, regardless of whether
defaultDir resolved.

* fix(embeddings): prefer CUDA 13 globally across the env-var directory scan

detectSystemCudaMajor's CUDA_PATH/LD_LIBRARY_PATH scan returned on the
first CUDA-major match within a single dir/sub pair, so a stale .so.12
found early (e.g. a leftover CUDA_PATH entry from a prior install) shadowed
a genuine .so.13 found later in the search path, even though the scan's
own ordering (checking 13 before 12 within each pair) was clearly intended
to prefer 13 wherever possible.

Keep scanning the full search space once a 12 is found, only returning
early once a 13 is found (the best possible answer) or the space is
exhausted.

* fix(embeddings): have onnxruntime-common-resolver defer to the effective onnxruntime-node dir

onnxruntime-common-resolver.ts independently re-derived transformers'
default onnxruntime-node dir (its own copy of the 'resolve transformers'
main entry, then onnxruntime-node' walk) to compute which onnxruntime-common
to pair with — duplicating onnxruntime-node-resolver.ts's own walk, and
capable of disagreeing with it: when the CUDA-major redirect is active,
this hook would still pair onnxruntime-common with transformers' default
(unredirected) onnxruntime-node, not the redirected copy the other hook
just switched onnxruntime-node itself to.

Have it call the already-exported getEffectiveOnnxRuntimeNodeDir() instead
— the same decision the CUDA-major redirect hook uses — so both hooks
always agree on which onnxruntime-node they're pairing onnxruntime-common
against, and the duplicated resolve-walk is removed entirely rather than
merely factored out.

* fix(embeddings): cache the effective CUDA major to remove redundant subprocess spawns

isCudaAvailable() in embedder.ts re-invoked ortCudaMajor/detectSystemCudaMajor
directly even though decide() (via getEffectiveOnnxRuntimeNodeDir) had
already computed both to make its redirect decision — a second, wasted
ldconfig + up to 2 ldd spawns on every initEmbedder() call.

Add effectiveMajor to the memoized Decision, computed once inside decide()
alongside effectiveDir/systemMajor, and export a single
isEffectiveCudaAvailable() that reads straight from the cached decision.
embedder.ts's local isCudaAvailable() wrapper (and its now-unused
getEffectiveOnnxRuntimeNodeDir/ortCudaMajor/detectSystemCudaMajor imports)
is replaced by this one exported function.

* fix(embeddings): surface CUDA redirect state at info level and in doctor

A successful CUDA-build redirect logged only at logger.debug (filtered
by the default 'info' level), and gitnexus doctor's embeddings section
never mentioned the redirect at all — leaving no diagnostic path for
'why is my CUDA-13 host still on CPU' after this PR ships.

Log the successful-redirect line at info (no-redirect/failure paths stay
at debug, since those are the common, expected case). Add
cudaRedirectDoctorStatus(), a pure summary of decide()'s already-computed
decision mirroring doctor.ts's existing localEmbeddingDoctorStatus shape,
and print it as a new literal (non-i18n) 'CUDA:' line in doctor's
embeddings section alongside the existing 'Support:' line, matching that
line's established convention.

* test(embeddings): register onnxruntime-node-resolver.test.ts in the cross-platform subset

The new test file guards on process.platform (linux/darwin cases) but was
absent from cross-platform-tests.ts's PLATFORM_LOGIC list, which
TESTING.md says platform-sensitive tests should be added to — so it never
ran on the Windows/macOS CI matrix, only Ubuntu.

Note: the sibling onnxruntime-common-resolver.test.ts has the identical,
pre-existing gap (it predates this PR) — left as-is here, since fixing
unrelated pre-existing test-registration debt is out of scope for this
PR's own follow-up fixes.

* test(embeddings): strengthen weak assertions, add garbled-output and CUDA_PATH coverage

Three of the four ensureOnnxRuntimeNodeMatchesSystem tests only asserted
'doesn't throw' rather than a concrete outcome — including one literally
named 'idempotent' that never asserted a call count on its own spy.
Strengthen each to assert real outcomes (module stays functional after a
no-op; spy call counts; return-value shape), while keeping the true
install-once idempotency proof in the redirect-active test added earlier
(this file's no-redirect scenario can't exercise it, since registerHooks
is never called either way).

Add the missing edge cases flagged in review: a CUDA_PATH-only fallback
scan test (mirroring the existing LD_LIBRARY_PATH one), and garbled/
unrecognized ldconfig and ldd output cases for both detectSystemCudaMajor
and ortCudaMajor, confirming neither falsely matches a CUDA major on
unparseable input. Also parameterize the non-linux platform test across
both darwin and win32 rather than darwin alone.

Not changed: the process.env reassignment vs. Object.defineProperty
'inconsistency' flagged in review — process.env, unlike process.platform,
has no getter-only restriction, so plain reassignment is already correct
and switching it to Object.defineProperty would be unnecessary ceremony.

* docs(embeddings): note the npm link/symlinked dev-checkout resolution caveat

resolveOurOrtNodeDir/resolveDefaultOrtNodeDir anchor to this module's own
real (post-symlink) location via import.meta.url, so a linked local dev
checkout may resolve against its own node_modules rather than the
consuming app's. Narrow, dev-only blast radius (regular npm/pnpm installs
are unaffected) — document-only, no structural fix warranted.

* fix(test): point the windowsHide spawn-family registry at the file that actually spawns

hooks.test.ts's windowsHide regression check still listed
gitnexus/src/core/embeddings/embedder.ts as a child_process-spawning
file, but this PR itself already moved all execFileSync usage out of
embedder.ts and into the new onnxruntime-node-resolver.ts — without
updating this registry. The check was silently failing at the PR's own
head commit (confirmed: 0 spawn-family calls found in embedder.ts,
'expected 0 to be greater than 0'), a pre-existing gap this fix-pass
surfaced via a full-suite run rather than something introduced by any of
the preceding follow-up commits.

Swap the registry entry to onnxruntime-node-resolver.ts, which does
import execFileSync (ldd + ldconfig, both already correctly passing
windowsHide: true).

* fix(test): make onnxruntime-node-resolver.test.ts path comparisons OS-agnostic

Registering this file in cross-platform-tests.ts's PLATFORM_LOGIC (a
prior commit in this series) means it now runs on the Windows CI matrix,
not just Ubuntu — and several of the fakeDirs-based tests (redirect:true,
ourDir-independent, subprocess-count, doctor-status) compared the
resolver's real join()/dirname() output against hardcoded forward-slash
fixture strings via exact-match or .startsWith().

Node's  module is bound to path.win32 (or path.posix) based on the
REAL host OS at process start — stubbing process.platform later, as these
tests already do for the resolver's own platform branching, has no effect
on it. So on a genuine Windows runner, join(effectiveDir, 'package.json')
backslash-normalizes even under a faked platform:'linux', silently
breaking every forward-slash comparison in this file: the createRequire
dispatch would route to the wrong fake require, throw MODULE_NOT_FOUND,
get swallowed by ensureOnnxRuntimeNodeMatchesSystem's outer try/catch, and
registerHooks would never fire — the redirect-active tests would fail
outright on Windows CI.

Normalize every comparison point (the createRequire dispatcher, and the
shared execFileSync/existsSync mocks) with a single toPosix() helper.
Added a forceWin32Path test option (using path.win32's real join/dirname
behavior) to prove this holds without needing an actual Windows runner —
confirmed by temporarily reverting the fix and observing the new test
fail with the exact predicted mismatch before restoring it.

* chore(autofix): apply prettier + eslint fixes via /autofix command

* fix(embeddings): keep CUDA auto-detect working on Node < 22.15 when the default build already matches

The registerHooks guard in decide() returned effectiveMajor: null
unconditionally, so on Node 22.0-22.14 / 23.0-23.4 (engines floor is
>=22.0.0) isEffectiveCudaAvailable() was always false and a CUDA-12 host
whose default onnxruntime-node build already matched — which needs no
hook at all to use the GPU — silently regressed from CUDA to CPU on the
auto device path (pre-PR isCudaAvailable() behavior).

Probe the system and the default copy regardless of registerHooks
availability; only the ourDir redirect branch stays gated on it, so the
probe still never reports a redirect target that cannot be installed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-02 07:53:32 +01:00

759 lines
32 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, it, expect, vi, afterEach } from 'vitest';
import path from 'node:path';
/**
* Tests for the CUDA-build-matching onnxruntime-node redirect.
*
* `@huggingface/transformers` exact-pins a CUDA-12 `onnxruntime-node`, while
* gitnexus' own dep floats to a CUDA-13 build; on a CUDA-13 host this module
* redirects transformers to the matching copy so embeddings use the GPU instead
* of silently falling back to CPU. The detection primitives (`ldconfig` / `ldd`
* / path scan) and `module.registerHooks` are mocked so the pure decision logic
* is asserted without touching the real loader or the host's CUDA install.
*/
const RESOLVER = '../../src/core/embeddings/onnxruntime-node-resolver.js';
const REAL_PLATFORM = process.platform;
const REAL_ENV = { ...process.env };
/**
* Node's `path` module is bound to `path.win32` or `path.posix` based on the
* REAL host OS at process start — stubbing `process.platform` later (as this
* file's tests do, for the resolver's OWN platform branching) has no effect
* on it. So on a genuine Windows CI runner, the resolver's `join(...)` calls
* normalize our forward-slash fake dirs to backslash-separated strings,
* which would silently fail to match the forward-slash fixtures/prefixes
* below. Normalize before every comparison so these tests are host-OS-agnostic.
*/
const toPosix = (p: string): string => p.replace(/\\/g, '/');
/** Three fake, distinct onnxruntime-node locations for driving decide() into redirect:true. */
interface FakeDirs {
/** gitnexus' own top-level onnxruntime-node dir (resolved via the module's own require). */
ourDir: string;
/** transformers' pinned/nested onnxruntime-node dir (resolved via createRequire(transformersMain)). */
defaultDir: string;
/** fake resolved path for require.resolve('@huggingface/transformers'). */
transformersMain: string;
/** When false, createRequire(transformersMain).resolve('onnxruntime-node/package.json') throws
* (simulating resolveDefaultOrtNodeDir() failing outright) instead of resolving to `defaultDir`. */
defaultResolvable?: boolean;
}
interface LoadOpts {
registerHooks?: unknown;
platform?: NodeJS.Platform;
execFileSync?: (cmd: string, args: string[]) => string;
existsSync?: (p: string) => boolean;
fakeDirs?: FakeDirs;
/** Force the resolver's `join`/`dirname` calls to use `path.win32` semantics
* (backslash-normalized output) regardless of the real host OS — proves the
* `toPosix()` normalization above actually works, rather than merely being
* argued for (#2341 follow-up). */
forceWin32Path?: boolean;
}
/** A require()-like function whose .resolve() is driven entirely by a specifier -> path map. */
function fakeRequire(resolveMap: Record<string, string>) {
return Object.assign(
(specifier: string) => {
throw new Error(`fakeRequire: unexpected require(${specifier})`);
},
{
resolve: (specifier: string) => {
const hit = resolveMap[specifier];
if (!hit) {
throw Object.assign(new Error(`Cannot find module '${specifier}'`), {
code: 'MODULE_NOT_FOUND',
});
}
return hit;
},
},
);
}
/**
* (Re)load the resolver with detection primitives + `registerHooks` mocked.
* `vi.resetModules()` clears the module-level decision cache and one-shot guard,
* so each test gets a pristine resolver.
*
* When `fakeDirs` is supplied, `createRequire` is also mocked so the module's
* two CJS resolve-walks (`resolveOurOrtNodeDir`/`resolveDefaultOrtNodeDir`, and
* the nodeUrl/commonUrl lookup inside `ensureOnnxRuntimeNodeMatchesSystem`) each
* resolve against a distinct fake directory instead of whatever's actually
* installed in this test's real node_modules — the only way to drive
* `decide() -> redirect:true` deterministically without touching production code.
*/
async function loadResolver(opts: LoadOpts = {}) {
vi.resetModules();
// Destructuring defaults (`= vi.fn()`) only apply when the property is
// `undefined` — but callers pass `registerHooks: undefined` specifically to
// simulate Node < 22.15 (no synchronous-hooks API), so a plain destructuring
// default would silently substitute a real mock function and defeat that.
// `'registerHooks' in opts` distinguishes "omitted → default to a spy" from
// "explicitly undefined → simulate its absence".
const registerHooks = 'registerHooks' in opts ? opts.registerHooks : vi.fn();
const {
platform = 'linux',
execFileSync = () => {
throw Object.assign(new Error('enoent'), { code: 'ENOENT' });
},
existsSync = () => false,
fakeDirs,
forceWin32Path = false,
} = opts;
if (forceWin32Path) {
vi.doMock('node:path', () => ({ ...path.win32, default: path.win32 }));
}
vi.doMock('node:module', async (io) => {
const orig = await io<typeof import('node:module')>();
if (!fakeDirs) return { ...orig, registerHooks };
const ourRequire = fakeRequire({
'@huggingface/transformers': fakeDirs.transformersMain,
'onnxruntime-node/package.json': `${fakeDirs.ourDir}/package.json`,
});
const defaultRequire = fakeRequire(
fakeDirs.defaultResolvable === false
? {}
: { 'onnxruntime-node/package.json': `${fakeDirs.defaultDir}/package.json` },
);
const effectiveRequire = fakeRequire({
'onnxruntime-node': `${fakeDirs.ourDir}/index.js`,
'onnxruntime-common': `${fakeDirs.ourDir}/node_modules/onnxruntime-common/index.js`,
});
return {
...orig,
registerHooks,
createRequire: (from: string) => {
// `from` is produced by the resolver's own `join(effectiveDir, 'package.json')`
// call — backslash-normalized on a real Windows host even though
// `fakeDirs.ourDir` etc. are forward-slash fixtures; normalize before comparing.
const normalizedFrom = toPosix(from);
if (normalizedFrom === fakeDirs.transformersMain) return defaultRequire;
if (normalizedFrom === `${fakeDirs.ourDir}/package.json`) return effectiveRequire;
return ourRequire;
},
};
});
vi.doMock('node:child_process', async (io) => ({
...(await io<typeof import('node:child_process')>()),
// Normalize args (the `.so` path for `ldd`) so callers' forward-slash
// prefix checks match regardless of which path module the resolver's
// own `join(...)` calls were bound to on the host running this test.
execFileSync: (cmd: string, args: string[]) => execFileSync(cmd, args.map(toPosix)),
}));
vi.doMock('node:fs', async (io) => ({
...(await io<typeof import('node:fs')>()),
existsSync: (p: unknown) => existsSync(toPosix(String(p))),
}));
Object.defineProperty(process, 'platform', { value: platform, configurable: true });
return import(RESOLVER);
}
afterEach(() => {
vi.doUnmock('node:module');
vi.doUnmock('node:child_process');
vi.doUnmock('node:fs');
vi.doUnmock('node:path');
Object.defineProperty(process, 'platform', { value: REAL_PLATFORM, configurable: true });
process.env = { ...REAL_ENV };
});
describe('detectSystemCudaMajor', () => {
it.each(['darwin', 'win32'] as const)(
'returns null on non-linux platforms (%s)',
async (platform) => {
const mod = await loadResolver({ platform });
expect(mod.detectSystemCudaMajor()).toBeNull();
},
);
it('prefers CUDA 13 over 12 when ldconfig lists both', async () => {
const mod = await loadResolver({
execFileSync: () =>
'libcublasLt.so.13 (libc6,x86-64) => /usr/local/cuda/lib64/libcublasLt.so.13\n' +
'libcublasLt.so.12 (libc6,x86-64) => /old/libcublasLt.so.12',
});
expect(mod.detectSystemCudaMajor()).toBe(13);
});
it('detects CUDA 12 when only .so.12 is present', async () => {
const mod = await loadResolver({
execFileSync: () => 'libcublasLt.so.12 (libc6,x86-64) => /usr/lib/libcublasLt.so.12',
});
expect(mod.detectSystemCudaMajor()).toBe(12);
});
it('falls back to an LD_LIBRARY_PATH scan when ldconfig is unavailable', async () => {
process.env.LD_LIBRARY_PATH = '/opt/cuda/lib64';
const mod = await loadResolver({
execFileSync: () => {
throw new Error('ldconfig missing');
},
existsSync: (p) => p === '/opt/cuda/lib64/libcublasLt.so.13',
});
expect(mod.detectSystemCudaMajor()).toBe(13);
});
it('returns null when no cuBLASLt is found anywhere', async () => {
const mod = await loadResolver({ execFileSync: () => 'libfoo.so => /x/libfoo.so' });
expect(mod.detectSystemCudaMajor()).toBeNull();
});
it('falls back to a CUDA_PATH scan when ldconfig is unavailable (#2341 follow-up)', async () => {
// Mirrors the existing LD_LIBRARY_PATH-only test above — CUDA_PATH is
// scanned first in the fallback loop and was previously untested on its own.
process.env.CUDA_PATH = '/opt/cuda';
const mod = await loadResolver({
execFileSync: () => {
throw new Error('ldconfig missing');
},
existsSync: (p) => p === '/opt/cuda/lib64/libcublasLt.so.13',
});
expect(mod.detectSystemCudaMajor()).toBe(13);
});
it('returns null (not a false match) when the ldconfig output is garbled/unrecognized', async () => {
const mod = await loadResolver({
execFileSync: () => 'some-corrupted-binary-output-\x00\xff-not-a-cuda-lib-line',
});
expect(mod.detectSystemCudaMajor()).toBeNull();
});
it('prefers a CUDA 13 found later in the search path over a CUDA 12 found earlier (#2341 follow-up)', async () => {
// A stale CUDA_PATH entry (e.g. left over from a prior install) only has
// .so.12; LD_LIBRARY_PATH, scanned after it, has the genuine .so.13. The
// scan must not stop at the first match — it must keep looking for a
// better (13) answer across the WHOLE search space.
process.env.CUDA_PATH = '/opt/old-cuda-12';
process.env.LD_LIBRARY_PATH = '/opt/cuda-13/lib64';
const mod = await loadResolver({
execFileSync: () => {
throw new Error('ldconfig missing');
},
existsSync: (p) =>
p === '/opt/old-cuda-12/libcublasLt.so.12' || p === '/opt/cuda-13/lib64/libcublasLt.so.13',
});
expect(mod.detectSystemCudaMajor()).toBe(13);
});
});
describe('ortCudaMajor', () => {
it('returns null when the CUDA provider .so is absent', async () => {
const mod = await loadResolver({ existsSync: () => false });
expect(mod.ortCudaMajor('/pkg/onnxruntime-node')).toBeNull();
});
it('reads CUDA 13 from the provider .so NEEDED entries', async () => {
const mod = await loadResolver({
existsSync: () => true,
execFileSync: () => 'libcublasLt.so.13 => /usr/local/cuda/lib64/libcublasLt.so.13',
});
expect(mod.ortCudaMajor('/pkg/onnxruntime-node')).toBe(13);
});
it('reads CUDA 12 even when the NEEDED lib is unresolved (ldd non-zero exit)', async () => {
const mod = await loadResolver({
existsSync: () => true,
execFileSync: () => {
// ldd exits non-zero with the "=> not found" line on stdout
throw Object.assign(new Error('ldd failed'), {
stdout: 'libcublasLt.so.12 => not found',
});
},
});
expect(mod.ortCudaMajor('/pkg/onnxruntime-node')).toBe(12);
});
it('returns null (not a false match) when the ldd output is garbled/unrecognized (#2341 follow-up)', async () => {
const mod = await loadResolver({
existsSync: () => true,
execFileSync: () => 'libunrelated.so.1 => /x/libunrelated.so.1\nlibc.so.6 => /lib/libc.so.6',
});
expect(mod.ortCudaMajor('/pkg/onnxruntime-node')).toBeNull();
});
it('warns (detection failed) when ldd produces no usable output at all, distinct from the silent no-provider case (#2341 follow-up)', async () => {
// Capture AFTER loadResolver() so the capture targets the same (freshly
// reset) logger.js instance the resolver module itself imports — the
// module registry is cleared by loadResolver()'s vi.resetModules().
const mod = await loadResolver({
existsSync: () => true,
// Simulates a missing `ldd` binary (ENOENT) or a permission-denied
// `.so`: execFileSync throws with no `stdout` at all, unlike the
// "=> not found" case above which still yields usable text.
execFileSync: () => {
throw Object.assign(new Error('spawn ldd ENOENT'), { code: 'ENOENT' });
},
});
const { _captureLogger } = await import('../../src/core/logger.js');
const cap = _captureLogger();
try {
expect(mod.ortCudaMajor('/pkg/onnxruntime-node')).toBeNull();
const records = cap.records();
expect(
records.some((r) => r.msg?.includes('Could not read CUDA provider dependencies')),
).toBe(true);
} finally {
cap.restore();
}
});
it('does not warn when the CUDA provider .so is simply absent (no detection was even attempted)', async () => {
const mod = await loadResolver({ existsSync: () => false });
const { _captureLogger } = await import('../../src/core/logger.js');
const cap = _captureLogger();
try {
expect(mod.ortCudaMajor('/pkg/onnxruntime-node')).toBeNull();
const records = cap.records();
expect(
records.some((r) => r.msg?.includes('Could not read CUDA provider dependencies')),
).toBe(false);
} finally {
cap.restore();
}
});
});
describe('ensureOnnxRuntimeNodeMatchesSystem', () => {
it('no-ops gracefully when registerHooks is unavailable (Node < 22.15), leaving the module otherwise functional', async () => {
const mod = await loadResolver({ registerHooks: undefined });
expect(() => mod.ensureOnnxRuntimeNodeMatchesSystem()).not.toThrow();
// The one-shot guard tripping (or not) must not corrupt decide()'s cache —
// subsequent calls to the other exports still work normally afterward.
expect(() => mod.getEffectiveOnnxRuntimeNodeDir()).not.toThrow();
expect(mod.isEffectiveCudaAvailable()).toBe(false); // redirect can never be active without registerHooks
});
it('installs no hook when there is no system CUDA (no redirect needed)', async () => {
const spy = vi.fn();
// non-linux → detectSystemCudaMajor() === null → decide() → redirect: false
const mod = await loadResolver({ registerHooks: spy, platform: 'darwin' });
mod.ensureOnnxRuntimeNodeMatchesSystem();
expect(spy).not.toHaveBeenCalled();
});
it('is idempotent in the no-redirect case: a second call is still a no-op (registerHooks never called)', async () => {
const spy = vi.fn();
const mod = await loadResolver({ registerHooks: spy, platform: 'darwin' });
mod.ensureOnnxRuntimeNodeMatchesSystem();
mod.ensureOnnxRuntimeNodeMatchesSystem();
// (True install-once idempotency, where a redirect WOULD fire without the
// guard, is covered by "installs registerHooks exactly once when the
// redirect is active" below — this case only proves repeated calls stay
// side-effect-free when there's nothing to install.)
expect(spy).not.toHaveBeenCalled();
});
it('exposes an effective onnxruntime-node dir (string or null) for the CUDA probe, never throwing', async () => {
const mod = await loadResolver({ platform: 'darwin' });
// Non-linux: no redirect, so the effective dir is transformers' default —
// a string when resolvable in the test tree (it really is, in this repo),
// or null if resolution ever genuinely fails.
let result: string | null | undefined;
expect(() => {
result = mod.getEffectiveOnnxRuntimeNodeDir();
}).not.toThrow();
expect(result === null || typeof result === 'string').toBe(true);
});
});
describe('decide() — registerHooks gating (#2341 follow-up)', () => {
// ensureOnnxRuntimeNodeMatchesSystem() can never install a redirect on
// Node < 22.15 (no registerHooks), so decide() must never report `ourDir`
// as the effective target there. But transformers' DEFAULT copy still loads
// without any hook, so its CUDA major must still be probed: a CUDA-12 host
// on Node 22.0–22.14 whose default build already matches has to keep the
// GPU it auto-selected before this redirect existed (pre-PR
// isCudaAvailable() behavior), not silently fall back to CPU.
const fakeDirs = {
ourDir: '/fake/our/onnxruntime-node',
defaultDir: '/fake/transformers-nested/onnxruntime-node',
transformersMain: '/fake/transformers/dist/transformers.node.mjs',
};
const soPrefix = (dir: string) => `${dir}/bin/napi-v6/linux`;
// System CUDA major is the parameter; the two bundled copies are fixed at
// ours=13 / default=12 (the PR's own documented layout).
function loadOldNodeResolver(systemMajor: 12 | 13) {
return loadResolver({
registerHooks: undefined,
platform: 'linux',
fakeDirs,
existsSync: (p) =>
p.startsWith(soPrefix(fakeDirs.ourDir)) || p.startsWith(soPrefix(fakeDirs.defaultDir)),
execFileSync: (cmd, args) => {
if (cmd === 'ldconfig')
return `libcublasLt.so.${systemMajor} (libc6,x86-64) => /usr/local/cuda/lib64/libcublasLt.so.${systemMajor}`;
if (cmd === 'ldd') {
const target = args[0] ?? '';
if (target.startsWith(soPrefix(fakeDirs.ourDir)))
return 'libcublasLt.so.13 => /usr/local/cuda-13/lib64/libcublasLt.so.13';
if (target.startsWith(soPrefix(fakeDirs.defaultDir)))
return 'libcublasLt.so.12 => /usr/local/cuda-12/lib64/libcublasLt.so.12';
}
throw new Error(`unexpected execFileSync(${cmd}, ${JSON.stringify(args)})`);
},
});
}
it('never reports a redirect target that cannot be installed (CUDA-13 host, mismatched default)', async () => {
const mod = await loadOldNodeResolver(13);
expect(toPosix(String(mod.getEffectiveOnnxRuntimeNodeDir()))).toBe(fakeDirs.defaultDir);
expect(mod.isEffectiveCudaAvailable()).toBe(false);
expect(() => mod.ensureOnnxRuntimeNodeMatchesSystem()).not.toThrow();
});
it('still probes the default copy: a CUDA-12 host whose default build matches keeps the GPU', async () => {
const mod = await loadOldNodeResolver(12);
expect(toPosix(String(mod.getEffectiveOnnxRuntimeNodeDir()))).toBe(fakeDirs.defaultDir);
expect(mod.isEffectiveCudaAvailable()).toBe(true);
});
});
describe('ensureOnnxRuntimeNodeMatchesSystem — redirect:true (#2341 follow-up)', () => {
// The prior test suite never drove decide() into redirect:true (it never
// faked createRequire), so the actual installed resolve() closure — the PR's
// real shipped behavior — had zero test coverage. Reproduce the PR's own
// documented common case: system has CUDA 13, transformers' default build is
// CUDA 12, gitnexus' own top-level build is CUDA 13.
const fakeDirs = {
ourDir: '/fake/our/onnxruntime-node',
defaultDir: '/fake/transformers-nested/onnxruntime-node',
transformersMain: '/fake/transformers/dist/transformers.node.mjs',
};
const soPath = (dir: string) => `${dir}/bin/napi-v6/linux`; // arch-agnostic prefix match below
function loadRedirectActiveResolver(registerHooksSpy: unknown) {
return loadResolver({
registerHooks: registerHooksSpy,
platform: 'linux',
fakeDirs,
existsSync: (p) =>
p.startsWith(soPath(fakeDirs.ourDir)) || p.startsWith(soPath(fakeDirs.defaultDir)),
execFileSync: (cmd, args) => {
if (cmd === 'ldconfig')
return 'libcublasLt.so.13 (libc6,x86-64) => /usr/local/cuda/lib64/libcublasLt.so.13';
if (cmd === 'ldd') {
const target = args[0] ?? '';
if (target.startsWith(soPath(fakeDirs.ourDir))) {
return 'libcublasLt.so.13 => /usr/local/cuda-13/lib64/libcublasLt.so.13';
}
if (target.startsWith(soPath(fakeDirs.defaultDir))) {
return 'libcublasLt.so.12 => /usr/local/cuda-12/lib64/libcublasLt.so.12';
}
}
throw new Error(`unexpected execFileSync(${cmd}, ${JSON.stringify(args)})`);
},
});
}
it('reports the redirect-active effective dir as our own CUDA-13 build', async () => {
const mod = await loadRedirectActiveResolver(vi.fn());
expect(mod.getEffectiveOnnxRuntimeNodeDir()).toBe(fakeDirs.ourDir);
});
it('installs registerHooks exactly once when the redirect is active', async () => {
const spy = vi.fn();
const mod = await loadRedirectActiveResolver(spy);
mod.ensureOnnxRuntimeNodeMatchesSystem();
expect(spy).toHaveBeenCalledTimes(1);
expect(typeof spy.mock.calls[0][0].resolve).toBe('function');
});
it('the installed resolve() closure redirects onnxruntime-node and onnxruntime-common, and passes through everything else', async () => {
const spy = vi.fn();
const mod = await loadRedirectActiveResolver(spy);
mod.ensureOnnxRuntimeNodeMatchesSystem();
const resolve = spy.mock.calls[0][0].resolve as (
s: string,
c: never,
n: (s: string, c: never) => unknown,
) => unknown;
const ctx = {} as never;
const next = vi.fn(() => ({ url: 'file:///should-not-be-used', shortCircuit: true }));
const nodeResult = resolve('onnxruntime-node', ctx, next) as {
url: string;
shortCircuit: boolean;
};
expect(nodeResult).toEqual({
url: expect.stringContaining('/fake/our/onnxruntime-node/index.js'),
shortCircuit: true,
});
expect(next).not.toHaveBeenCalled();
const commonResult = resolve('onnxruntime-common', ctx, next) as {
url: string;
shortCircuit: boolean;
};
expect(commonResult).toEqual({
url: expect.stringContaining(
'/fake/our/onnxruntime-node/node_modules/onnxruntime-common/index.js',
),
shortCircuit: true,
});
expect(next).not.toHaveBeenCalled();
resolve('some-other-package', ctx, next);
expect(next).toHaveBeenCalledWith('some-other-package', ctx);
});
it('isEffectiveCudaAvailable() reports true when the redirect-active effective build matches the system', async () => {
const mod = await loadRedirectActiveResolver(vi.fn());
expect(mod.isEffectiveCudaAvailable()).toBe(true);
});
it('logs the successful redirect at info level (#2341 follow-up)', async () => {
const mod = await loadRedirectActiveResolver(vi.fn());
const { _captureLogger } = await import('../../src/core/logger.js');
const cap = _captureLogger();
try {
mod.ensureOnnxRuntimeNodeMatchesSystem();
const record = cap
.records()
.find((r) => r.msg?.includes('Redirected onnxruntime-node to system-matched CUDA build'));
expect(record).toBeDefined();
expect(record?.level).toBe(30); // pino 'info'
} finally {
cap.restore();
}
});
it('does not log at info when no redirect is needed (common, expected path)', async () => {
// Non-linux -> no system CUDA -> decide() never redirects.
const mod = await loadResolver({ registerHooks: vi.fn(), platform: 'darwin' });
const { _captureLogger } = await import('../../src/core/logger.js');
const cap = _captureLogger('debug'); // capture below the default 'info' to prove nothing else fires either
try {
mod.ensureOnnxRuntimeNodeMatchesSystem();
const infoOrAboveRecords = cap.records().filter((r) => (r.level ?? 0) >= 30);
expect(infoOrAboveRecords).toHaveLength(0);
} finally {
cap.restore();
}
});
});
describe('cudaRedirectDoctorStatus (#2341 follow-up)', () => {
it('reports n/a when there is no system CUDA', async () => {
const mod = await loadResolver({ registerHooks: vi.fn(), platform: 'darwin' });
expect(mod.cudaRedirectDoctorStatus()).toEqual({
status: 'n/a (no system CUDA detected)',
detail: null,
});
});
it('reports the redirect-active status with the effective dir as detail', async () => {
const fakeDirs = {
ourDir: '/fake/our/onnxruntime-node-doctor',
defaultDir: '/fake/transformers-nested/onnxruntime-node-doctor',
transformersMain: '/fake/transformers/doctor/index.js',
};
const soPath = (dir: string) => `${dir}/bin/napi-v6/linux`;
const mod = await loadResolver({
registerHooks: vi.fn(),
platform: 'linux',
fakeDirs,
existsSync: (p) =>
p.startsWith(soPath(fakeDirs.ourDir)) || p.startsWith(soPath(fakeDirs.defaultDir)),
execFileSync: (cmd, args) => {
if (cmd === 'ldconfig')
return 'libcublasLt.so.13 (libc6,x86-64) => /usr/local/cuda/lib64/libcublasLt.so.13';
if (cmd === 'ldd') {
const target = args[0] ?? '';
if (target.startsWith(soPath(fakeDirs.ourDir)))
return 'libcublasLt.so.13 => /a/libcublasLt.so.13';
if (target.startsWith(soPath(fakeDirs.defaultDir)))
return 'libcublasLt.so.12 => /a/libcublasLt.so.12';
}
throw new Error(`unexpected execFileSync(${cmd}, ${JSON.stringify(args)})`);
},
});
expect(mod.cudaRedirectDoctorStatus()).toEqual({
status: expect.stringContaining('redirected onnxruntime-node to the CUDA 13 build'),
detail: fakeDirs.ourDir,
});
});
it('reports a mismatch status (with no fix available) when neither copy ships a matching CUDA provider', async () => {
const mod = await loadResolver({
registerHooks: vi.fn(),
platform: 'linux',
existsSync: () => false, // no onnxruntime-node copy ships a CUDA provider .so at all
execFileSync: (cmd) => {
if (cmd === 'ldconfig')
return 'libcublasLt.so.13 (libc6,x86-64) => /usr/local/cuda/lib64/libcublasLt.so.13';
throw new Error('ldd should not be reached when existsSync is false');
},
});
// `detail` (the resolved effectiveDir) isn't asserted here — resolveDefaultOrtNodeDir()
// isn't mocked in this test, so it resolves against this sandbox's real
// node_modules and its exact value isn't the point of this case; the
// redirect-active test above already covers `detail` precisely.
expect(mod.cudaRedirectDoctorStatus().status).toContain(
'no CUDA 13-matched onnxruntime-node build found',
);
});
});
describe('isEffectiveCudaAvailable — no redundant subprocess spawns (#2341 follow-up)', () => {
it('probes ldconfig/ldd only once total, regardless of how many times the effective dir and CUDA match are queried', async () => {
const fakeDirs = {
ourDir: '/fake/our/onnxruntime-node-u8',
defaultDir: '/fake/transformers-nested/onnxruntime-node-u8',
transformersMain: '/fake/transformers/u8/index.js',
};
const soPath = (dir: string) => `${dir}/bin/napi-v6/linux`;
const existsSyncSpy = vi.fn(
(p: string) =>
p.startsWith(soPath(fakeDirs.ourDir)) || p.startsWith(soPath(fakeDirs.defaultDir)),
);
const execFileSyncSpy = vi.fn((cmd: string, args: string[]) => {
if (cmd === 'ldconfig')
return 'libcublasLt.so.13 (libc6,x86-64) => /usr/local/cuda/lib64/libcublasLt.so.13';
if (cmd === 'ldd') {
const target = args[0] ?? '';
if (target.startsWith(soPath(fakeDirs.ourDir))) {
return 'libcublasLt.so.13 => /usr/local/cuda-13/lib64/libcublasLt.so.13';
}
if (target.startsWith(soPath(fakeDirs.defaultDir))) {
return 'libcublasLt.so.12 => /usr/local/cuda-12/lib64/libcublasLt.so.12';
}
}
throw new Error(`unexpected execFileSync(${cmd}, ${JSON.stringify(args)})`);
});
const mod = await loadResolver({
registerHooks: vi.fn(),
platform: 'linux',
fakeDirs,
existsSync: existsSyncSpy,
execFileSync: execFileSyncSpy,
});
// Query the decision through both public entry points, each more than once.
mod.getEffectiveOnnxRuntimeNodeDir();
mod.isEffectiveCudaAvailable();
mod.getEffectiveOnnxRuntimeNodeDir();
expect(mod.isEffectiveCudaAvailable()).toBe(true);
// decide() is memoized: exactly one ldconfig call (system major) and one
// ldd call per onnxruntime-node dir actually probed (default + ours) —
// never re-invoked across the 4 queries above.
const ldconfigCalls = execFileSyncSpy.mock.calls.filter(([cmd]) => cmd === 'ldconfig');
const lddCalls = execFileSyncSpy.mock.calls.filter(([cmd]) => cmd === 'ldd');
expect(ldconfigCalls).toHaveLength(1);
expect(lddCalls).toHaveLength(2); // defaultDir once, ourDir once
});
});
describe('decide() — ourDir checked independently of defaultDir (#2341 follow-up)', () => {
it("picks ourDir as the effective target when transformers' own onnxruntime-node resolution fails outright", async () => {
const fakeDirs = {
ourDir: '/fake/our/onnxruntime-node-u5',
defaultDir: '/fake/unreachable/onnxruntime-node-u5',
transformersMain: '/fake/transformers/u5/index.js',
defaultResolvable: false, // createRequire(transformersMain).resolve(...) throws -> defaultDir stays null
};
const soPrefix = (dir: string) => `${dir}/bin/napi-v6/linux`;
const mod = await loadResolver({
registerHooks: vi.fn(),
platform: 'linux',
fakeDirs,
existsSync: (p) => p.startsWith(soPrefix(fakeDirs.ourDir)),
execFileSync: (cmd, args) => {
if (cmd === 'ldconfig') {
return 'libcublasLt.so.13 (libc6,x86-64) => /usr/local/cuda/lib64/libcublasLt.so.13';
}
if (cmd === 'ldd' && (args[0] ?? '').startsWith(soPrefix(fakeDirs.ourDir))) {
return 'libcublasLt.so.13 => /usr/local/cuda-13/lib64/libcublasLt.so.13';
}
throw new Error(`unexpected execFileSync(${cmd}, ${JSON.stringify(args)})`);
},
});
// Before this fix, the ourDir fallback lookup was nested inside
// `if (systemMajor != null && defaultDir)`, so a null defaultDir skipped
// checking ourDir entirely and this would incorrectly return null.
expect(mod.getEffectiveOnnxRuntimeNodeDir()).toBe(fakeDirs.ourDir);
});
});
describe('cross-platform path handling (#2341 follow-up)', () => {
// This test file was added to cross-platform-tests.ts's PLATFORM_LOGIC list
// (so it now runs on the Windows CI matrix, not just Ubuntu). Node's `path`
// module is bound to path.win32 on a real Windows host regardless of any
// process.platform stub — so the resolver's own join(effectiveDir,
// 'package.json') calls backslash-normalize even when these tests fake
// platform: 'linux'. forceWin32Path proves the toPosix() normalization
// added above actually handles that, rather than merely being argued for.
const fakeDirs = {
ourDir: '/fake/our/onnxruntime-node',
defaultDir: '/fake/transformers-nested/onnxruntime-node',
transformersMain: '/fake/transformers/dist/transformers.node.mjs',
};
const soPath = (dir: string) => `${dir}/bin/napi-v6/linux`;
it('resolves the redirect-active dir and installs the resolve() closure correctly even when join()/dirname() backslash-normalize (simulated real Windows)', async () => {
const spy = vi.fn();
const mod = await loadResolver({
registerHooks: spy,
platform: 'linux',
fakeDirs,
forceWin32Path: true,
existsSync: (p) =>
p.startsWith(soPath(fakeDirs.ourDir)) || p.startsWith(soPath(fakeDirs.defaultDir)),
execFileSync: (cmd, args) => {
if (cmd === 'ldconfig')
return 'libcublasLt.so.13 (libc6,x86-64) => /usr/local/cuda/lib64/libcublasLt.so.13';
if (cmd === 'ldd') {
const target = args[0] ?? '';
if (target.startsWith(soPath(fakeDirs.ourDir)))
return 'libcublasLt.so.13 => /a/libcublasLt.so.13';
if (target.startsWith(soPath(fakeDirs.defaultDir)))
return 'libcublasLt.so.12 => /a/libcublasLt.so.12';
}
throw new Error(`unexpected execFileSync(${cmd}, ${JSON.stringify(args)})`);
},
});
expect(mod.getEffectiveOnnxRuntimeNodeDir()).toBe(fakeDirs.ourDir);
expect(mod.isEffectiveCudaAvailable()).toBe(true);
mod.ensureOnnxRuntimeNodeMatchesSystem();
// The real proof: registerHooks must actually fire. Before the toPosix()
// fix, the createRequire dispatcher's `from === ...` comparison would
// mismatch against a backslash-joined `from` under forceWin32Path,
// ensureOnnxRuntimeNodeMatchesSystem's outer try/catch would silently
// swallow the resulting MODULE_NOT_FOUND, and this would never fire.
expect(spy).toHaveBeenCalledTimes(1);
const resolve = spy.mock.calls[0][0].resolve as (
s: string,
c: never,
n: (s: string, c: never) => unknown,
) => unknown;
const ctx = {} as never;
const next = vi.fn();
const nodeResult = resolve('onnxruntime-node', ctx, next) as {
url: string;
shortCircuit: boolean;
};
expect(nodeResult.shortCircuit).toBe(true);
expect(toPosix(nodeResult.url)).toContain('/fake/our/onnxruntime-node/index.js');
expect(next).not.toHaveBeenCalled();
});
});