mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-01 02:01:24 +00:00
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): 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>
458 lines
19 KiB
TypeScript
458 lines
19 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import {
|
|
getLocalEmbeddingRuntimeBlocker,
|
|
getMissingLocalEmbeddingStackMessage,
|
|
isLocalEmbeddingRuntimeBlockerMessage,
|
|
isLocalEmbeddingSidecarAbortMessage,
|
|
isLocalEmbeddingStackInstalled,
|
|
isMissingLocalEmbeddingStackMessage,
|
|
LOCAL_EMBEDDING_SIDECAR_ABORT_LEAD,
|
|
localEmbeddingStackMissingMessage,
|
|
localEmbeddingPrefixUnloadableMessage,
|
|
} from '../../src/core/embeddings/runtime-support.js';
|
|
|
|
/**
|
|
* Spy that fires whenever @huggingface/transformers is actually imported.
|
|
* Hoisted so the vi.mock factory below can reference it. The mock replaces the
|
|
* real module entirely, so this suite never loads onnxruntime-node — it is safe
|
|
* to run on any platform, including hosts without the native binding.
|
|
*/
|
|
const { transformersImported } = vi.hoisted(() => ({ transformersImported: vi.fn() }));
|
|
|
|
vi.mock('@huggingface/transformers', () => {
|
|
transformersImported();
|
|
const fakePipeline: any = async () => ({ data: new Float32Array(384) });
|
|
return {
|
|
pipeline: vi.fn(async () => fakePipeline),
|
|
env: { allowLocalModels: true, cacheDir: '', remoteHost: '' },
|
|
};
|
|
});
|
|
|
|
/**
|
|
* Spy for the CUDA-13 build-matching resolver hook. Child `initLocalEmbedder`
|
|
* must call this before importing transformers.js — mocked (rather than
|
|
* exercising the real resolver's env/subprocess probing) to keep this suite
|
|
* fast and platform-independent; `onnxruntime-node-resolver.test.ts` covers
|
|
* the resolver's own decision logic.
|
|
*/
|
|
const { resolverHookInstalled } = vi.hoisted(() => ({ resolverHookInstalled: vi.fn() }));
|
|
|
|
vi.mock('../../src/core/embeddings/onnxruntime-node-resolver.js', () => ({
|
|
ensureOnnxRuntimeNodeMatchesSystem: () => resolverHookInstalled(),
|
|
isEffectiveCudaAvailable: () => false,
|
|
}));
|
|
|
|
vi.mock('../../src/core/embeddings/embedding-sidecar-client.js', () => ({
|
|
ensureEmbeddingSidecar: vi.fn(async () => ({ device: 'cpu' })),
|
|
getSidecarDevice: () => 'cpu',
|
|
reapEmbeddingSidecar: vi.fn(),
|
|
reapEmbeddingSidecarAndWait: vi.fn(async () => {}),
|
|
sidecarEmbedBatch: vi.fn(async (texts: string[]) => texts.map(() => new Float32Array(384))),
|
|
}));
|
|
|
|
/**
|
|
* Mock `module.registerHooks` with a spy (#2372). Without this, a successful
|
|
* local `initEmbedder()` calls the REAL `ensureEmbeddingStackResolvable` /
|
|
* onnxruntime-common resolver, which register process-global resolution hooks in
|
|
* the vitest worker — and `vi.resetModules()` (beforeEach) resets their one-shot
|
|
* guards, so each test re-registers real hooks that are never deregistered,
|
|
* silently redirecting resolution for every later test in the worker. Spreading
|
|
* `importOriginal` keeps `createRequire` real, so the CJS resolution probes still
|
|
* work.
|
|
*/
|
|
const { registerHooksSpy } = vi.hoisted(() => ({ registerHooksSpy: vi.fn() }));
|
|
|
|
vi.mock('node:module', async (importOriginal) => ({
|
|
...(await importOriginal<typeof import('node:module')>()),
|
|
registerHooks: registerHooksSpy,
|
|
}));
|
|
|
|
const EMBED_ENV_KEYS = [
|
|
'GITNEXUS_EMBEDDING_URL',
|
|
'GITNEXUS_EMBEDDING_MODEL',
|
|
'GITNEXUS_EMBEDDING_API_KEY',
|
|
'GITNEXUS_EMBEDDING_DIMS',
|
|
] as const;
|
|
|
|
const savedEnv = Object.fromEntries(EMBED_ENV_KEYS.map((k) => [k, process.env[k]]));
|
|
|
|
/** Stub process.platform/arch via DI-friendly defineProperty; returns a restore fn. */
|
|
const stubPlatform = (platform: NodeJS.Platform, arch: NodeJS.Architecture): (() => void) => {
|
|
const orig = { platform: process.platform, arch: process.arch };
|
|
Object.defineProperty(process, 'platform', { value: platform, configurable: true });
|
|
Object.defineProperty(process, 'arch', { value: arch, configurable: true });
|
|
return () => {
|
|
Object.defineProperty(process, 'platform', { value: orig.platform, configurable: true });
|
|
Object.defineProperty(process, 'arch', { value: orig.arch, configurable: true });
|
|
};
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.resetModules();
|
|
transformersImported.mockClear();
|
|
resolverHookInstalled.mockClear();
|
|
registerHooksSpy.mockClear();
|
|
for (const key of EMBED_ENV_KEYS) delete process.env[key];
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
for (const key of EMBED_ENV_KEYS) {
|
|
if (savedEnv[key] === undefined) delete process.env[key];
|
|
else process.env[key] = savedEnv[key];
|
|
}
|
|
});
|
|
|
|
describe('getLocalEmbeddingRuntimeBlocker', () => {
|
|
it('blocks darwin/x64 (macOS Intel)', () => {
|
|
expect(getLocalEmbeddingRuntimeBlocker({ platform: 'darwin', arch: 'x64' })).not.toBeNull();
|
|
});
|
|
|
|
it('returns null for darwin/arm64, linux/x64, and win32/x64', () => {
|
|
expect(getLocalEmbeddingRuntimeBlocker({ platform: 'darwin', arch: 'arm64' })).toBeNull();
|
|
expect(getLocalEmbeddingRuntimeBlocker({ platform: 'linux', arch: 'x64' })).toBeNull();
|
|
expect(getLocalEmbeddingRuntimeBlocker({ platform: 'win32', arch: 'x64' })).toBeNull();
|
|
});
|
|
|
|
it('explains macOS Intel, local embeddings, the ONNX native binding, and safe alternatives', () => {
|
|
const msg = getLocalEmbeddingRuntimeBlocker({ platform: 'darwin', arch: 'x64' });
|
|
expect(msg).not.toBeNull();
|
|
const text = msg as string;
|
|
// What failed
|
|
expect(text).toMatch(/macOS Intel/);
|
|
expect(text).toMatch(/local semantic embeddings/i);
|
|
expect(text).toMatch(/ONNX/);
|
|
expect(text).toMatch(/native binding/i);
|
|
// Does NOT imply wasm rescues it, and does NOT leak the raw native error
|
|
expect(text).toMatch(/wasm does not help/i);
|
|
expect(text).not.toMatch(/Cannot find module/);
|
|
// Safe alternatives
|
|
expect(text).toMatch(/without --embeddings/);
|
|
expect(text).toContain('GITNEXUS_EMBEDDING_URL');
|
|
expect(text).toMatch(/Linux or Apple Silicon/);
|
|
expect(text).toMatch(/Official CLI Docker images no longer/);
|
|
// Addresses the GitNexus device knob too, not only ONNX_WEB_BACKEND (R3 / #1987)
|
|
expect(text).toContain('GITNEXUS_EMBEDDING_DEVICE');
|
|
});
|
|
|
|
it('reads platform/arch from process when no options are given', () => {
|
|
// Stub the process so the no-arg call must consult process.platform/arch —
|
|
// this falsifiably exercises the `?? process.platform` / `?? process.arch`
|
|
// fallback (a plain null === null on the CI host would not).
|
|
const restoreBlocked = stubPlatform('darwin', 'x64');
|
|
try {
|
|
expect(getLocalEmbeddingRuntimeBlocker()).not.toBeNull();
|
|
expect(getLocalEmbeddingRuntimeBlocker()).toBe(
|
|
getLocalEmbeddingRuntimeBlocker({ platform: 'darwin', arch: 'x64' }),
|
|
);
|
|
} finally {
|
|
restoreBlocked();
|
|
}
|
|
|
|
const restoreSupported = stubPlatform('linux', 'x64');
|
|
try {
|
|
expect(getLocalEmbeddingRuntimeBlocker()).toBeNull();
|
|
} finally {
|
|
restoreSupported();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('isLocalEmbeddingRuntimeBlockerMessage', () => {
|
|
it('recognises the blocker message and rejects unrelated errors', () => {
|
|
const blocker = getLocalEmbeddingRuntimeBlocker({ platform: 'darwin', arch: 'x64' }) as string;
|
|
expect(isLocalEmbeddingRuntimeBlockerMessage(blocker)).toBe(true);
|
|
expect(isLocalEmbeddingRuntimeBlockerMessage('ECONNREFUSED while downloading model')).toBe(
|
|
false,
|
|
);
|
|
expect(
|
|
isLocalEmbeddingRuntimeBlockerMessage(
|
|
"Cannot find module '../bin/.../onnxruntime_binding.node'",
|
|
),
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
/** Build a module-not-found error the way Node does (message + `code`). */
|
|
const moduleNotFound = (message: string, code: string): NodeJS.ErrnoException => {
|
|
const err: NodeJS.ErrnoException = new Error(message);
|
|
err.code = code;
|
|
return err;
|
|
};
|
|
|
|
describe('getMissingLocalEmbeddingStackMessage (#2370 pruned optional stack)', () => {
|
|
it('maps an ESM import failure for @huggingface/transformers to the guidance message', () => {
|
|
const err = moduleNotFound(
|
|
"Cannot find package '@huggingface/transformers' imported from /x/dist/core/embeddings/embedder.js",
|
|
'ERR_MODULE_NOT_FOUND',
|
|
);
|
|
expect(getMissingLocalEmbeddingStackMessage(err)).toBe(localEmbeddingStackMissingMessage());
|
|
});
|
|
|
|
it('maps a CJS require failure for onnxruntime-node to the guidance message', () => {
|
|
const err = moduleNotFound("Cannot find module 'onnxruntime-node'", 'MODULE_NOT_FOUND');
|
|
expect(getMissingLocalEmbeddingStackMessage(err)).toBe(localEmbeddingStackMissingMessage());
|
|
});
|
|
|
|
it('ignores module-not-found errors for unrelated packages', () => {
|
|
const err = moduleNotFound("Cannot find package 'graphology'", 'ERR_MODULE_NOT_FOUND');
|
|
expect(getMissingLocalEmbeddingStackMessage(err)).toBeNull();
|
|
});
|
|
|
|
it('ignores the macOS-Intel native-binding path error (a file path, not the bare specifier)', () => {
|
|
// #1515-style failure: the PACKAGE is installed but its native binding file
|
|
// is absent — must NOT be misreported as a pruned optional install.
|
|
const err = moduleNotFound(
|
|
"Cannot find module '/x/node_modules/onnxruntime-node/bin/napi-v6/darwin/x64/onnxruntime_binding.node'",
|
|
'MODULE_NOT_FOUND',
|
|
);
|
|
expect(getMissingLocalEmbeddingStackMessage(err)).toBeNull();
|
|
});
|
|
|
|
it('ignores errors without a module-not-found code and non-Error values', () => {
|
|
expect(
|
|
getMissingLocalEmbeddingStackMessage(new Error("Cannot find package 'onnxruntime-node'")),
|
|
).toBeNull();
|
|
expect(
|
|
getMissingLocalEmbeddingStackMessage("Cannot find package 'onnxruntime-node'"),
|
|
).toBeNull();
|
|
expect(getMissingLocalEmbeddingStackMessage(undefined)).toBeNull();
|
|
});
|
|
|
|
it('produces guidance naming every recovery path', () => {
|
|
const msg = localEmbeddingStackMissingMessage();
|
|
expect(msg).toContain('gitnexus embeddings install');
|
|
expect(msg).toContain('GITNEXUS_EMBEDDING_RUNTIME_DIR');
|
|
expect(msg).not.toContain('ONNXRUNTIME_NODE_INSTALL=skip');
|
|
expect(msg).toContain('GLOBAL_AGENT_HTTPS_PROXY');
|
|
expect(msg).toContain('GITNEXUS_EMBEDDING_URL');
|
|
expect(msg).toContain('1.6.12');
|
|
// Must not trip analyze.ts's generic "installation may be corrupt" branch.
|
|
expect(msg).not.toMatch(/Cannot find (module|package)/);
|
|
expect(msg).not.toContain('MODULE_NOT_FOUND');
|
|
});
|
|
});
|
|
|
|
describe('localEmbeddingPrefixUnloadableMessage', () => {
|
|
it('tells the user to upgrade this Node, not to install from another Node and retry', () => {
|
|
const msg = localEmbeddingPrefixUnloadableMessage();
|
|
expect(msg).toContain('module.registerHooks');
|
|
expect(msg).toContain('Upgrade this');
|
|
expect(msg).toContain('cannot add that API here');
|
|
expect(msg).not.toMatch(/then retry/i);
|
|
});
|
|
});
|
|
|
|
describe('isMissingLocalEmbeddingStackMessage', () => {
|
|
it('recognises its own message and rejects the platform blocker and unrelated errors', () => {
|
|
expect(isMissingLocalEmbeddingStackMessage(localEmbeddingStackMissingMessage())).toBe(true);
|
|
const blocker = getLocalEmbeddingRuntimeBlocker({ platform: 'darwin', arch: 'x64' }) as string;
|
|
expect(isMissingLocalEmbeddingStackMessage(blocker)).toBe(false);
|
|
expect(isLocalEmbeddingRuntimeBlockerMessage(localEmbeddingStackMissingMessage())).toBe(false);
|
|
expect(isMissingLocalEmbeddingStackMessage('ECONNREFUSED while downloading model')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('isLocalEmbeddingSidecarAbortMessage', () => {
|
|
it('recognises sidecar abort and sidecar-dead text', () => {
|
|
expect(isLocalEmbeddingSidecarAbortMessage(LOCAL_EMBEDDING_SIDECAR_ABORT_LEAD)).toBe(true);
|
|
expect(isLocalEmbeddingSidecarAbortMessage('Embedding sidecar died (signal SIGSEGV)')).toBe(
|
|
true,
|
|
);
|
|
expect(isLocalEmbeddingSidecarAbortMessage(localEmbeddingStackMissingMessage())).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('isLocalEmbeddingStackInstalled', () => {
|
|
it('probes stack resolution without importing transformers.js', () => {
|
|
isLocalEmbeddingStackInstalled();
|
|
expect(transformersImported).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('lazy transformers.js import', () => {
|
|
it('control: the spy fires when transformers.js is actually imported', async () => {
|
|
expect(transformersImported).not.toHaveBeenCalled();
|
|
await import('@huggingface/transformers');
|
|
expect(transformersImported).toHaveBeenCalled();
|
|
});
|
|
|
|
it('importing the guard module does not import transformers.js', async () => {
|
|
await import('../../src/core/embeddings/runtime-support.js');
|
|
expect(transformersImported).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('importing the core embedder does not import transformers.js at module load', async () => {
|
|
await import('../../src/core/embeddings/embedder.js');
|
|
expect(transformersImported).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('importing the MCP embedder does not import transformers.js at module load', async () => {
|
|
await import('../../src/mcp/core/embedder.js');
|
|
expect(transformersImported).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('initEmbedder local-runtime guard (darwin/x64)', () => {
|
|
it('rejects the core initEmbedder before importing transformers.js', async () => {
|
|
const restore = stubPlatform('darwin', 'x64');
|
|
try {
|
|
const { initEmbedder } = await import('../../src/core/embeddings/embedder.js');
|
|
await expect(initEmbedder()).rejects.toThrow(/macOS Intel/);
|
|
// The guard must short-circuit before the lazy transformers.js import.
|
|
expect(transformersImported).not.toHaveBeenCalled();
|
|
} finally {
|
|
restore();
|
|
}
|
|
});
|
|
|
|
it('rejects with a clean GitNexus message, not the raw native module error', async () => {
|
|
const restore = stubPlatform('darwin', 'x64');
|
|
try {
|
|
const { initEmbedder } = await import('../../src/core/embeddings/embedder.js');
|
|
const err = (await initEmbedder().catch((e) => e)) as Error;
|
|
expect(err.message).toMatch(/native binding/i);
|
|
expect(err.message).not.toMatch(/Cannot find module/);
|
|
expect(err.message).not.toMatch(/onnxruntime_binding/);
|
|
} finally {
|
|
restore();
|
|
}
|
|
});
|
|
|
|
it('rejects the MCP initEmbedder before importing transformers.js', async () => {
|
|
const restore = stubPlatform('darwin', 'x64');
|
|
try {
|
|
const { initEmbedder } = await import('../../src/mcp/core/embedder.js');
|
|
await expect(initEmbedder()).rejects.toThrow(/macOS Intel/);
|
|
expect(transformersImported).not.toHaveBeenCalled();
|
|
} finally {
|
|
restore();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('HTTP embedding mode on darwin/x64', () => {
|
|
it('is not blocked by the local-runtime guard and never touches the native runtime', async () => {
|
|
process.env.GITNEXUS_EMBEDDING_URL = 'http://test:8080/v1';
|
|
process.env.GITNEXUS_EMBEDDING_MODEL = 'test-model';
|
|
const mockVec = Array.from({ length: 384 }, (_, i) => i / 384);
|
|
// Size the response to the request's `input` length so both the single
|
|
// (embedText) and batched (embedBatch) calls get matching vector counts.
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn(async (_url: string, init: { body: string }) => {
|
|
const n = (JSON.parse(init.body) as { input: string[] }).input.length;
|
|
return {
|
|
ok: true,
|
|
json: async () => ({ data: Array.from({ length: n }, () => ({ embedding: mockVec })) }),
|
|
};
|
|
}),
|
|
);
|
|
|
|
const restore = stubPlatform('darwin', 'x64');
|
|
try {
|
|
const { embedText, embedBatch, isEmbedderReady } =
|
|
await import('../../src/core/embeddings/embedder.js');
|
|
|
|
// HTTP mode is ready without any local/native initialization.
|
|
expect(isEmbedderReady()).toBe(true);
|
|
|
|
const single = await embedText('hello from macOS Intel');
|
|
expect(single).toBeInstanceOf(Float32Array);
|
|
expect(single.length).toBe(384);
|
|
|
|
const batch = await embedBatch(['a', 'b']);
|
|
expect(batch).toHaveLength(2);
|
|
|
|
// HTTP embeddings must route through fetch, never the local ONNX runtime.
|
|
expect(fetch).toHaveBeenCalled();
|
|
expect(transformersImported).not.toHaveBeenCalled();
|
|
} finally {
|
|
restore();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('MCP embedQuery on darwin/x64', () => {
|
|
it('routes HTTP mode through httpEmbedQuery without importing transformers.js', async () => {
|
|
process.env.GITNEXUS_EMBEDDING_URL = 'http://test:8080/v1';
|
|
process.env.GITNEXUS_EMBEDDING_MODEL = 'test-model';
|
|
const mockVec = Array.from({ length: 384 }, (_, i) => i / 384);
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn(async () => ({
|
|
ok: true,
|
|
json: async () => ({ data: [{ embedding: mockVec }] }),
|
|
})),
|
|
);
|
|
|
|
const restore = stubPlatform('darwin', 'x64');
|
|
try {
|
|
const { embedQuery } = await import('../../src/mcp/core/embedder.js');
|
|
const vec = await embedQuery('query from macOS Intel');
|
|
|
|
// httpEmbedQuery validates against the default 384 dims (no GITNEXUS_EMBEDDING_DIMS
|
|
// set), so the reused stub stays 384-length; resize the stub + DIMS together to vary it.
|
|
expect(Array.isArray(vec)).toBe(true);
|
|
expect(vec).toHaveLength(384);
|
|
expect(fetch).toHaveBeenCalled();
|
|
expect(transformersImported).not.toHaveBeenCalled();
|
|
} finally {
|
|
restore();
|
|
}
|
|
});
|
|
|
|
it('rejects local mode before importing transformers.js', async () => {
|
|
// No GITNEXUS_EMBEDDING_* env (cleared in beforeEach) → local mode → embedQuery
|
|
// calls initEmbedder, which throws the guard before the lazy transformers import.
|
|
const restore = stubPlatform('darwin', 'x64');
|
|
try {
|
|
const { embedQuery } = await import('../../src/mcp/core/embedder.js');
|
|
await expect(embedQuery('query from macOS Intel')).rejects.toThrow(/macOS Intel/);
|
|
expect(transformersImported).not.toHaveBeenCalled();
|
|
} finally {
|
|
restore();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('CUDA-13 resolver hook installation (both local-embedding entrypoints)', () => {
|
|
// Parent façade must not install CUDA / transformers hooks; only the
|
|
// sidecar child local-init path does.
|
|
it('core embedder does not install the resolver hook in the parent on local init', async () => {
|
|
const restore = stubPlatform('linux', 'x64');
|
|
try {
|
|
const { initEmbedder } = await import('../../src/core/embeddings/embedder.js');
|
|
await expect(initEmbedder()).resolves.toBeDefined();
|
|
|
|
expect(resolverHookInstalled).not.toHaveBeenCalled();
|
|
expect(transformersImported).not.toHaveBeenCalled();
|
|
} finally {
|
|
restore();
|
|
}
|
|
});
|
|
|
|
it('MCP embedder does not install the resolver hook in the parent on local init', async () => {
|
|
const restore = stubPlatform('linux', 'x64');
|
|
try {
|
|
const { initEmbedder } = await import('../../src/mcp/core/embedder.js');
|
|
await expect(initEmbedder()).resolves.toBeDefined();
|
|
|
|
expect(resolverHookInstalled).not.toHaveBeenCalled();
|
|
expect(transformersImported).not.toHaveBeenCalled();
|
|
} finally {
|
|
restore();
|
|
}
|
|
});
|
|
|
|
it('does not register process-global resolution hooks in the parent on local init (#2372)', async () => {
|
|
const restore = stubPlatform('linux', 'x64');
|
|
try {
|
|
const { initEmbedder } = await import('../../src/core/embeddings/embedder.js');
|
|
await expect(initEmbedder()).resolves.toBeDefined();
|
|
expect(registerHooksSpy).not.toHaveBeenCalled();
|
|
} finally {
|
|
restore();
|
|
}
|
|
});
|
|
});
|