GitNexus/gitnexus/test/unit/embedding-runtime-resolution.test.ts
Gergő Magyar ac9a4e9abd
Some checks are pending
Gitleaks / gitleaks (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
fix(embeddings): keep ONNX off the install and analyze critical path (#3287)
* fix(embeddings): isolate local ONNX inference in a child_process sidecar

The analyze parent must not load onnxruntime-node. Fork a sidecar for
vectors only and reap it on worker exit; keep Ladybug writes in-process.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(embeddings): share the sidecar client across MCP, serve, and sync

Query hosts now use the core façade instead of a second in-process ONNX
embedder. Search skips an empty table, sync reaps beside closeLbug, and
ready means the stack is resolvable rather than a warm singleton.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(embeddings): refuse Intel Mac and unloadable prefix before npm heal

Analyze, sync, install, and the sidecar client now consult the platform
blocker before forking or downloading the optional stack. HTTP stays the
escape hatch; wasm is not treated as a rescue.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(embeddings): take the ONNX stack off default npm install

Pins live in gitnexusEmbeddingStack. embeddings install writes prefix
overrides before npm spawn. Leftover 1.6.12 package-first trees are residual.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(publish): drop grammar source from the published tarball

Every vendored grammar has 6/6 prebuilds, so files ships those plus
Leiden and FTS instead of parser.c. First ship stays above 80 MiB.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(embeddings): match MCP missing-stack warn to the R20 copy

Default install no longer calls the stack optional, so the once-per-backend
stderr assertion must look for the new lead line.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(review): bound sidecar death, cancel writes, and publish-file guards

Init-time native crashes no longer respawn a child on every query. Local
embedBatch honors AbortSignal after sidecar return, MCP query() surfaces
vector-lane degradation, disconnect always reaps, and the grammar prepack
guard checks files globs instead of on-disk prebuilds.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(embeddings): share runtime preflight and sidecar reap helpers

Analyze and embeddings-sync used the same blocker/prefix/install gate
with different error routing. One assessment keeps those paths aligned
without changing CLI vs thrown-error behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address PR review feedback (#3287)

Keep a reaped sidecar from resetting its replacement, wait for dispose,
tighten the publish-files guard, and stop assuming a leftover ONNX tree in CI.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address remaining PR review feedback (#3287)

Clear the sidecar reap timeout, add init IPC slack, and isolate embeddings-sync tests from HTTP-mode env.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(embeddings): unstub globals after sidecar HTTP-mode tests

Keep a leaked fetch stub from failing assertions out of later tests in the same file.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(embeddings): pin sidecar success cases off darwin/x64

The runtime blocker reads the real process platform before the fork mock, so local-success tests must not inherit an Intel Mac host.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address remaining PR review feedback (#3287)

Keep vector degradation per query, treat leftover Intel-Mac stacks as not ready, and document that the CLI image no longer ships ONNX.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Simplify embedding sidecar shutdown and search hot paths

Drop redundant sidecar reaps and unused child helpers, and run FTS alongside semantic search.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address remaining PR review feedback (#3287)

Share HF attempt parsing with the sidecar init deadline, abort embed waits without killing the child, and restore last init options on recreate.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address remaining PR review feedback (#3287)

Treat sub-1 HF attempt env values as invalid, and drop leaked sidecar waiters when IPC send throws.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Address remaining PR review feedback (#3287)

Keep sidecar init on a shared chain; each waiter can abort only its own wait.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ci): declare embedding-table existence probe as unordered LIMIT

The empty-table skip in semanticSearch is existence-only; declare it so the #2787 determinism guard stops failing coverage shard 3/3.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-15 12:11:47 +01:00

250 lines
9.9 KiB
TypeScript

import { describe, it, expect, vi, afterEach } from 'vitest';
/**
* Tier-resolution tests for `resolveEmbeddingRuntime` (#2372).
*
* The package tier uses runtime-install's module-scope require (anchored at its
* own `import.meta.url`); the prefix tier uses a require anchored at
* `<prefix>/noop.js`. After U4 a clean tree has no embedding packages, but a
* leftover 1.6.12 package-first install still can. We mock `createRequire` so
* the package tier is driven by a fixture map, exercising the partial / full /
* missing permutations without depending on this workspace's node_modules.
*
* This file has ZERO static import of runtime-install.js (the dual-instance
* rule): every load goes through the dynamic-import harness, so no real
* process-global loader state is ever touched.
*/
const RUNTIME_INSTALL = '../../src/core/embeddings/runtime-install.js';
const RUNTIME_SUPPORT = '../../src/core/embeddings/runtime-support.js';
const PREFIX = '/fake/embedding-runtime';
const toPosix = (p: string): string => p.replace(/\\/g, '/');
/** A require()-like function whose .resolve() is driven 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;
},
},
);
}
/** Load runtime-install with createRequire routed: package anchor vs <prefix>/noop.js. */
async function loadWithTiers(pkg: Record<string, string>, prefix: Record<string, string>) {
vi.resetModules();
process.env.GITNEXUS_EMBEDDING_RUNTIME_DIR = PREFIX;
const packageRequire = fakeRequire(pkg);
const prefixRequire = fakeRequire(prefix);
vi.doMock('node:module', async (io) => {
const orig = await io<typeof import('node:module')>();
return {
...orig,
createRequire: (from: string | URL) =>
toPosix(String(from)) === `${PREFIX}/noop.js` ? prefixRequire : packageRequire,
};
});
const runtimeInstall = await import(RUNTIME_INSTALL);
const runtimeSupport = await import(RUNTIME_SUPPORT);
return { runtimeInstall, runtimeSupport };
}
const BOTH = {
'@huggingface/transformers': '/x/transformers/index.js',
'onnxruntime-node': '/x/onnxruntime-node/index.js',
};
const ONLY_TRANSFORMERS = { '@huggingface/transformers': '/x/transformers/index.js' };
const NONE: Record<string, string> = {};
afterEach(() => {
vi.doUnmock('node:module');
delete process.env.GITNEXUS_EMBEDDING_RUNTIME_DIR;
});
describe('resolveEmbeddingRuntime — tier resolution', () => {
it('reports package source when both packages resolve from the package anchor', async () => {
const { runtimeInstall } = await loadWithTiers(BOTH, NONE);
expect(runtimeInstall.resolveEmbeddingRuntime()).toEqual({ source: 'package' });
});
it('reports runtime-prefix when the package tier misses and the prefix has both', async () => {
const { runtimeInstall } = await loadWithTiers(NONE, BOTH);
expect(runtimeInstall.resolveEmbeddingRuntime()).toEqual({ source: 'runtime-prefix' });
});
it('returns null when the prefix is partial (transformers but no onnxruntime-node)', async () => {
const { runtimeInstall } = await loadWithTiers(NONE, ONLY_TRANSFORMERS);
expect(runtimeInstall.resolveEmbeddingRuntime()).toBeNull();
});
it('isLocalEmbeddingStackInstalled is false for a partial prefix', async () => {
const { runtimeSupport } = await loadWithTiers(NONE, ONLY_TRANSFORMERS);
expect(runtimeSupport.isLocalEmbeddingStackInstalled()).toBe(false);
});
});
type ResolveHook = (
specifier: string,
context: unknown,
next: (s: string, c: unknown) => unknown,
) => unknown;
const HOOK_CTX = { conditions: [] as string[], importAttributes: {} };
const esmMiss = (): Error =>
Object.assign(new Error("Cannot find package 'x'"), { code: 'ERR_MODULE_NOT_FOUND' });
const exportsMiss = (): Error =>
Object.assign(new Error('No known export'), { code: 'ERR_PACKAGE_PATH_NOT_EXPORTED' });
const cjsMiss = (): Error =>
Object.assign(new Error("Cannot find module 'x'"), { code: 'MODULE_NOT_FOUND' });
/** Load runtime-install with a registerHooks spy + createRequire routing, and return the resolve closure. */
async function loadWithHook(pkg: Record<string, string>, prefix: Record<string, string>) {
vi.resetModules();
process.env.GITNEXUS_EMBEDDING_RUNTIME_DIR = PREFIX;
const spy = vi.fn();
const packageRequire = fakeRequire(pkg);
const prefixRequire = fakeRequire(prefix);
vi.doMock('node:module', async (io) => {
const orig = await io<typeof import('node:module')>();
return {
...orig,
registerHooks: spy,
createRequire: (from: string | URL) =>
toPosix(String(from)) === `${PREFIX}/noop.js` ? prefixRequire : packageRequire,
};
});
const runtimeInstall = await import(RUNTIME_INSTALL);
runtimeInstall.ensureEmbeddingStackResolvable();
const resolve = (spy.mock.calls[0][0] as { resolve: ResolveHook }).resolve;
return { resolve };
}
describe('ensureEmbeddingStackResolvable — onnxruntime-common source gate', () => {
it('package-sourced stack: an onnxruntime-common miss rethrows (leaves #307 in control)', async () => {
const { resolve } = await loadWithHook(BOTH, NONE);
const next = vi.fn(() => {
throw esmMiss();
});
expect(() => resolve('onnxruntime-common', HOOK_CTX, next)).toThrow();
expect(next).toHaveBeenCalledTimes(1);
});
it('prefix-sourced stack: an onnxruntime-common miss re-anchors to the prefix', async () => {
const { resolve } = await loadWithHook(NONE, BOTH);
const next = vi
.fn()
.mockImplementationOnce(() => {
throw esmMiss();
})
.mockImplementationOnce(() => ({ url: 'redirected', shortCircuit: true }));
resolve('onnxruntime-common', HOOK_CTX, next);
expect(next).toHaveBeenCalledTimes(2);
expect((next.mock.calls[1][1] as { parentURL: string }).parentURL).toContain('noop.js');
});
it('null-sourced stack: an onnxruntime-common miss rethrows', async () => {
const { resolve } = await loadWithHook(NONE, NONE);
const next = vi.fn(() => {
throw esmMiss();
});
expect(() => resolve('onnxruntime-common', HOOK_CTX, next)).toThrow();
expect(next).toHaveBeenCalledTimes(1);
});
it('transformers miss re-anchors regardless of source', async () => {
const { resolve } = await loadWithHook(BOTH, NONE);
const next = vi
.fn()
.mockImplementationOnce(() => {
throw esmMiss();
})
.mockImplementationOnce(() => ({ url: 'ok', shortCircuit: true }));
resolve('@huggingface/transformers', HOOK_CTX, next);
expect(next).toHaveBeenCalledTimes(2);
const anchor = (next.mock.calls[1][1] as { parentURL: string }).parentURL;
expect(anchor).toMatch(/^file:\/\//);
expect(anchor).toContain('noop.js');
});
it('re-anchors on ERR_PACKAGE_PATH_NOT_EXPORTED as well as ERR_MODULE_NOT_FOUND', async () => {
const { resolve } = await loadWithHook(BOTH, NONE);
const next = vi
.fn()
.mockImplementationOnce(() => {
throw exportsMiss();
})
.mockImplementationOnce(() => ({ url: 'ok', shortCircuit: true }));
resolve('@huggingface/transformers', HOOK_CTX, next);
expect(next).toHaveBeenCalledTimes(2);
expect((next.mock.calls[1][1] as { parentURL: string }).parentURL).toContain('noop.js');
});
it('a non-stack specifier passes straight through', async () => {
const { resolve } = await loadWithHook(BOTH, NONE);
const next = vi.fn(() => ({ url: 'x', shortCircuit: true }));
resolve('some-other-pkg', HOOK_CTX, next);
expect(next).toHaveBeenCalledTimes(1);
});
it('a CJS MODULE_NOT_FOUND (not ERR_) is rethrown, never re-anchored', async () => {
const { resolve } = await loadWithHook(NONE, BOTH);
const next = vi.fn(() => {
throw cjsMiss();
});
expect(() => resolve('onnxruntime-node', HOOK_CTX, next)).toThrow();
expect(next).toHaveBeenCalledTimes(1);
});
it('re-entrancy latch: a hook re-entered during the source probe passes straight through', async () => {
vi.resetModules();
process.env.GITNEXUS_EMBEDDING_RUNTIME_DIR = PREFIX;
const spy = vi.fn();
const reentrantNext = vi.fn(() => ({ url: 'passthrough', shortCircuit: true }));
const captured: { resolve?: ResolveHook } = {};
// A prefix require whose .resolve re-enters the closure — simulating a Node
// that routed require.resolve through the sync hook. The latch must make the
// re-entrant call pass straight through instead of recursing into the gate.
const prefixRequire = {
resolve: (specifier: string) => {
captured.resolve?.('onnxruntime-common', HOOK_CTX, reentrantNext);
return `/x/${specifier}`;
},
};
const packageRequire = fakeRequire(NONE);
vi.doMock('node:module', async (io) => {
const orig = await io<typeof import('node:module')>();
return {
...orig,
registerHooks: spy,
createRequire: (from: string | URL) =>
toPosix(String(from)) === `${PREFIX}/noop.js` ? prefixRequire : packageRequire,
};
});
const runtimeInstall = await import(RUNTIME_INSTALL);
runtimeInstall.ensureEmbeddingStackResolvable();
captured.resolve = (spy.mock.calls[0][0] as { resolve: ResolveHook }).resolve;
const outerNext = vi
.fn()
.mockImplementationOnce(() => {
throw esmMiss();
})
.mockImplementationOnce(() => ({ url: 'redirected', shortCircuit: true }));
// Must not stack-overflow; the re-entrant probe call short-circuits.
captured.resolve('onnxruntime-common', HOOK_CTX, outerNext);
expect(reentrantNext).toHaveBeenCalled();
expect(outerNext).toHaveBeenCalledTimes(2);
});
});