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>
431 lines
18 KiB
TypeScript
431 lines
18 KiB
TypeScript
import { EventEmitter } from 'node:events';
|
||
import { readFileSync } from 'node:fs';
|
||
import path from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||
import type { ChildProcess } from 'node:child_process';
|
||
import type {
|
||
SidecarRequest,
|
||
SidecarResponse,
|
||
} from '../../src/core/embeddings/embedding-sidecar-protocol.js';
|
||
|
||
const embeddingsDir = path.resolve(
|
||
path.dirname(fileURLToPath(import.meta.url)),
|
||
'../../src/core/embeddings',
|
||
);
|
||
|
||
class FakeChild extends EventEmitter {
|
||
killed = false;
|
||
connected = true;
|
||
stdout = new EventEmitter();
|
||
stderr = new EventEmitter();
|
||
send = vi.fn((msg: SidecarRequest) => {
|
||
queueMicrotask(() => {
|
||
const response = this.respond(msg);
|
||
if (response) this.emit('message', response);
|
||
});
|
||
return true;
|
||
});
|
||
kill = vi.fn((signal?: NodeJS.Signals) => {
|
||
this.killed = true;
|
||
this.emit('close', signal === 'SIGKILL' ? null : 0, signal ?? null);
|
||
return true;
|
||
});
|
||
unref = vi.fn();
|
||
|
||
respond(msg: SidecarRequest): SidecarResponse | undefined {
|
||
if (msg.type === 'init') {
|
||
return { id: msg.id, type: 'ready', device: 'cpu' };
|
||
}
|
||
if (msg.type === 'embed') {
|
||
return {
|
||
id: msg.id,
|
||
type: 'vectors',
|
||
vectors: msg.texts.map(() => [0.25, 0.5, 0.75]),
|
||
};
|
||
}
|
||
return undefined;
|
||
}
|
||
}
|
||
|
||
describe('embedding sidecar client', () => {
|
||
const originalUrl = process.env.GITNEXUS_EMBEDDING_URL;
|
||
const originalModel = process.env.GITNEXUS_EMBEDDING_MODEL;
|
||
const originalHfTimeout = process.env.HF_DOWNLOAD_TIMEOUT_MS;
|
||
const originalHfAttempts = process.env.HF_MAX_ATTEMPTS;
|
||
const originalSidecarTimeout = process.env.GITNEXUS_EMBEDDING_SIDECAR_TIMEOUT_MS;
|
||
const hostPlatform = process.platform;
|
||
const hostArch = process.arch;
|
||
|
||
let forkMock: ReturnType<typeof vi.fn>;
|
||
let children: FakeChild[];
|
||
|
||
beforeEach(async () => {
|
||
delete process.env.GITNEXUS_EMBEDDING_URL;
|
||
// Local-success cases must not inherit a darwin/x64 host blocker.
|
||
Object.defineProperty(process, 'platform', { value: 'linux', configurable: true });
|
||
Object.defineProperty(process, 'arch', { value: 'x64', configurable: true });
|
||
children = [];
|
||
forkMock = vi.fn((_script: string, _args: string[], _opts: unknown) => {
|
||
const child = new FakeChild();
|
||
children.push(child);
|
||
return child as unknown as ChildProcess;
|
||
});
|
||
const client = await import('../../src/core/embeddings/embedding-sidecar-client.js');
|
||
client._resetEmbeddingSidecarForTests();
|
||
client._setForkForTests(forkMock);
|
||
});
|
||
|
||
afterEach(async () => {
|
||
const client = await import('../../src/core/embeddings/embedding-sidecar-client.js');
|
||
client._resetEmbeddingSidecarForTests();
|
||
client._setForkForTests(null);
|
||
if (originalUrl === undefined) delete process.env.GITNEXUS_EMBEDDING_URL;
|
||
else process.env.GITNEXUS_EMBEDDING_URL = originalUrl;
|
||
if (originalModel === undefined) delete process.env.GITNEXUS_EMBEDDING_MODEL;
|
||
else process.env.GITNEXUS_EMBEDDING_MODEL = originalModel;
|
||
if (originalHfTimeout === undefined) delete process.env.HF_DOWNLOAD_TIMEOUT_MS;
|
||
else process.env.HF_DOWNLOAD_TIMEOUT_MS = originalHfTimeout;
|
||
if (originalHfAttempts === undefined) delete process.env.HF_MAX_ATTEMPTS;
|
||
else process.env.HF_MAX_ATTEMPTS = originalHfAttempts;
|
||
if (originalSidecarTimeout === undefined)
|
||
delete process.env.GITNEXUS_EMBEDDING_SIDECAR_TIMEOUT_MS;
|
||
else process.env.GITNEXUS_EMBEDDING_SIDECAR_TIMEOUT_MS = originalSidecarTimeout;
|
||
Object.defineProperty(process, 'platform', { value: hostPlatform, configurable: true });
|
||
Object.defineProperty(process, 'arch', { value: hostArch, configurable: true });
|
||
vi.useRealTimers();
|
||
vi.unstubAllGlobals();
|
||
});
|
||
|
||
it('strips GITNEXUS_EMBEDDING_URL and does not inherit stdout', async () => {
|
||
process.env.GITNEXUS_EMBEDDING_URL = 'http://custom.example/v1';
|
||
const { ensureEmbeddingSidecar } =
|
||
await import('../../src/core/embeddings/embedding-sidecar-client.js');
|
||
await ensureEmbeddingSidecar();
|
||
|
||
expect(forkMock).toHaveBeenCalledTimes(1);
|
||
const opts = forkMock.mock.calls[0][2] as {
|
||
env: NodeJS.ProcessEnv;
|
||
stdio: unknown;
|
||
};
|
||
expect(opts.env.GITNEXUS_EMBEDDING_URL).toBeUndefined();
|
||
expect(opts.stdio).toEqual(['ignore', 'ignore', 'pipe', 'ipc']);
|
||
});
|
||
|
||
it('reports no sidecar device until init is ready', async () => {
|
||
const { getSidecarDevice, ensureEmbeddingSidecar } =
|
||
await import('../../src/core/embeddings/embedding-sidecar-client.js');
|
||
expect(getSidecarDevice()).toBeNull();
|
||
await ensureEmbeddingSidecar();
|
||
expect(getSidecarDevice()).toBe('cpu');
|
||
});
|
||
|
||
it('rejects a forceDevice that conflicts with the initialized sidecar', async () => {
|
||
const { ensureEmbeddingSidecar } =
|
||
await import('../../src/core/embeddings/embedding-sidecar-client.js');
|
||
await expect(ensureEmbeddingSidecar()).resolves.toEqual({ device: 'cpu' });
|
||
await expect(ensureEmbeddingSidecar({ forceDevice: 'cuda' })).rejects.toThrow(
|
||
/already initialized on cpu; cannot switch to cuda/,
|
||
);
|
||
await expect(ensureEmbeddingSidecar({ forceDevice: 'cpu' })).resolves.toEqual({
|
||
device: 'cpu',
|
||
});
|
||
});
|
||
|
||
it('forks once for two batches', async () => {
|
||
const { sidecarEmbedBatch } =
|
||
await import('../../src/core/embeddings/embedding-sidecar-client.js');
|
||
const first = await sidecarEmbedBatch(['a']);
|
||
const second = await sidecarEmbedBatch(['b']);
|
||
expect(first).toHaveLength(1);
|
||
expect(second).toHaveLength(1);
|
||
expect(forkMock).toHaveBeenCalledTimes(1);
|
||
});
|
||
|
||
it('does not fork on darwin/x64', async () => {
|
||
const orig = { platform: process.platform, arch: process.arch };
|
||
Object.defineProperty(process, 'platform', { value: 'darwin', configurable: true });
|
||
Object.defineProperty(process, 'arch', { value: 'x64', configurable: true });
|
||
try {
|
||
const { ensureEmbeddingSidecar } =
|
||
await import('../../src/core/embeddings/embedding-sidecar-client.js');
|
||
await expect(ensureEmbeddingSidecar()).rejects.toThrow(/macOS Intel/);
|
||
expect(forkMock).not.toHaveBeenCalled();
|
||
} finally {
|
||
Object.defineProperty(process, 'platform', { value: orig.platform, configurable: true });
|
||
Object.defineProperty(process, 'arch', { value: orig.arch, configurable: true });
|
||
}
|
||
});
|
||
|
||
it('does not fork on an empty batch', async () => {
|
||
const { sidecarEmbedBatch } =
|
||
await import('../../src/core/embeddings/embedding-sidecar-client.js');
|
||
await expect(sidecarEmbedBatch([])).resolves.toEqual([]);
|
||
expect(forkMock).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('does not fork in HTTP mode', 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 (_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 { embedBatch, isEmbedderReady } = await import('../../src/core/embeddings/embedder.js');
|
||
expect(isEmbedderReady()).toBe(true);
|
||
const batch = await embedBatch(['hello']);
|
||
expect(batch).toHaveLength(1);
|
||
expect(forkMock).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('marks local embeddings unavailable on native abort and does not respawn', async () => {
|
||
const { sidecarEmbedBatch } =
|
||
await import('../../src/core/embeddings/embedding-sidecar-client.js');
|
||
await sidecarEmbedBatch(['first']);
|
||
expect(forkMock).toHaveBeenCalledTimes(1);
|
||
|
||
children[0].emit('close', null, 'SIGSEGV');
|
||
|
||
await expect(sidecarEmbedBatch(['second'])).rejects.toThrow(
|
||
/unavailable after the sidecar aborted/,
|
||
);
|
||
expect(forkMock).toHaveBeenCalledTimes(1);
|
||
});
|
||
|
||
it('does not respawn after init-time SIGSEGV', async () => {
|
||
forkMock.mockImplementation(() => {
|
||
const child = new FakeChild();
|
||
children.push(child);
|
||
child.send = vi.fn(() => {
|
||
queueMicrotask(() => child.emit('close', null, 'SIGSEGV'));
|
||
return true;
|
||
});
|
||
return child as unknown as ChildProcess;
|
||
});
|
||
const { ensureEmbeddingSidecar } =
|
||
await import('../../src/core/embeddings/embedding-sidecar-client.js');
|
||
await expect(ensureEmbeddingSidecar()).rejects.toThrow(/Embedding sidecar died/);
|
||
expect(forkMock).toHaveBeenCalledTimes(1);
|
||
await expect(ensureEmbeddingSidecar()).rejects.toThrow(/unavailable after the sidecar aborted/);
|
||
expect(forkMock).toHaveBeenCalledTimes(1);
|
||
});
|
||
|
||
it('SIGKILLs a stalled embed request and rejects with a timeout', async () => {
|
||
process.env.GITNEXUS_EMBEDDING_SIDECAR_TIMEOUT_MS = '50';
|
||
vi.useFakeTimers();
|
||
const { sidecarEmbedBatch } =
|
||
await import('../../src/core/embeddings/embedding-sidecar-client.js');
|
||
await sidecarEmbedBatch(['warmup']);
|
||
children[0].send = vi.fn(() => true);
|
||
const pending = sidecarEmbedBatch(['stalled']);
|
||
const assertion = expect(pending).rejects.toThrow(/timed out after 50ms \(embed\)/);
|
||
await vi.advanceTimersByTimeAsync(50);
|
||
await assertion;
|
||
expect(children[0].kill).toHaveBeenCalledWith('SIGKILL');
|
||
});
|
||
|
||
it('rejects embedBatch when aborted while waiting on an outstanding embed request', async () => {
|
||
const { embedBatch } = await import('../../src/core/embeddings/embedder.js');
|
||
await embedBatch(['warmup']);
|
||
children[0].send = vi.fn(() => true);
|
||
const controller = new AbortController();
|
||
const pending = embedBatch(['stalled'], { signal: controller.signal });
|
||
await vi.waitFor(() => expect(children[0].send).toHaveBeenCalled());
|
||
controller.abort();
|
||
await expect(pending).rejects.toThrow();
|
||
});
|
||
|
||
it('cleans up a pending waiter when IPC send throws', async () => {
|
||
process.env.GITNEXUS_EMBEDDING_SIDECAR_TIMEOUT_MS = '50';
|
||
const { sidecarEmbedBatch } =
|
||
await import('../../src/core/embeddings/embedding-sidecar-client.js');
|
||
await sidecarEmbedBatch(['warmup']);
|
||
vi.useFakeTimers();
|
||
children[0].send = vi.fn(() => {
|
||
throw new Error('Channel closed');
|
||
});
|
||
await expect(sidecarEmbedBatch(['x'])).rejects.toThrow(/Channel closed/);
|
||
expect(children[0].kill).not.toHaveBeenCalled();
|
||
await vi.advanceTimersByTimeAsync(50);
|
||
expect(children[0].kill).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('lets a joining caller abort without cancelling shared sidecar init', async () => {
|
||
let releaseInit: (() => void) | undefined;
|
||
forkMock.mockImplementation(() => {
|
||
const child = new FakeChild();
|
||
children.push(child);
|
||
child.send = vi.fn((msg: SidecarRequest) => {
|
||
if (msg.type === 'init') {
|
||
releaseInit = () => child.emit('message', { id: msg.id, type: 'ready', device: 'cpu' });
|
||
return true;
|
||
}
|
||
queueMicrotask(() => {
|
||
const response = child.respond(msg);
|
||
if (response) child.emit('message', response);
|
||
});
|
||
return true;
|
||
});
|
||
return child as unknown as ChildProcess;
|
||
});
|
||
const { ensureEmbeddingSidecar } =
|
||
await import('../../src/core/embeddings/embedding-sidecar-client.js');
|
||
const first = ensureEmbeddingSidecar();
|
||
await vi.waitFor(() => expect(releaseInit).toBeDefined());
|
||
const joining = new AbortController();
|
||
const second = ensureEmbeddingSidecar({ signal: joining.signal });
|
||
joining.abort();
|
||
await expect(second).rejects.toThrow();
|
||
releaseInit!();
|
||
await expect(first).resolves.toEqual({ device: 'cpu' });
|
||
expect(forkMock).toHaveBeenCalledTimes(1);
|
||
});
|
||
|
||
it('does not cancel shared sidecar init when the first waiter aborts', async () => {
|
||
let releaseInit: (() => void) | undefined;
|
||
forkMock.mockImplementation(() => {
|
||
const child = new FakeChild();
|
||
children.push(child);
|
||
child.send = vi.fn((msg: SidecarRequest) => {
|
||
if (msg.type === 'init') {
|
||
releaseInit = () => child.emit('message', { id: msg.id, type: 'ready', device: 'cpu' });
|
||
return true;
|
||
}
|
||
queueMicrotask(() => {
|
||
const response = child.respond(msg);
|
||
if (response) child.emit('message', response);
|
||
});
|
||
return true;
|
||
});
|
||
return child as unknown as ChildProcess;
|
||
});
|
||
const { ensureEmbeddingSidecar } =
|
||
await import('../../src/core/embeddings/embedding-sidecar-client.js');
|
||
const firstAbort = new AbortController();
|
||
const first = ensureEmbeddingSidecar({ signal: firstAbort.signal });
|
||
await vi.waitFor(() => expect(releaseInit).toBeDefined());
|
||
const second = ensureEmbeddingSidecar();
|
||
firstAbort.abort();
|
||
await expect(first).rejects.toThrow();
|
||
releaseInit!();
|
||
await expect(second).resolves.toEqual({ device: 'cpu' });
|
||
expect(forkMock).toHaveBeenCalledTimes(1);
|
||
});
|
||
|
||
it('rejects an aborted embed wait without killing the sidecar', async () => {
|
||
const { sidecarEmbedBatch } =
|
||
await import('../../src/core/embeddings/embedding-sidecar-client.js');
|
||
await sidecarEmbedBatch(['warmup']);
|
||
children[0].send = vi.fn(() => true);
|
||
const controller = new AbortController();
|
||
const pending = sidecarEmbedBatch(['stalled'], { signal: controller.signal });
|
||
await vi.waitFor(() => expect(children[0].send).toHaveBeenCalled());
|
||
controller.abort();
|
||
await expect(pending).rejects.toThrow();
|
||
expect(children[0].kill).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('respawns a dead sidecar with the last init embeddingConfig and forceDevice', async () => {
|
||
const { ensureEmbeddingSidecar, sidecarEmbedBatch } =
|
||
await import('../../src/core/embeddings/embedding-sidecar-client.js');
|
||
await ensureEmbeddingSidecar({
|
||
embeddingConfig: { dimensions: 768 },
|
||
forceDevice: 'cpu',
|
||
});
|
||
expect(forkMock).toHaveBeenCalledTimes(1);
|
||
expect(children[0].send.mock.calls[0][0]).toMatchObject({
|
||
type: 'init',
|
||
embeddingConfig: { dimensions: 768 },
|
||
forceDevice: 'cpu',
|
||
});
|
||
children[0].emit('close', 1, null);
|
||
await sidecarEmbedBatch(['again']);
|
||
expect(forkMock).toHaveBeenCalledTimes(2);
|
||
expect(children[1].send.mock.calls[0][0]).toMatchObject({
|
||
type: 'init',
|
||
embeddingConfig: { dimensions: 768 },
|
||
forceDevice: 'cpu',
|
||
});
|
||
});
|
||
|
||
it('does not use worker_threads or import the embeddings barrel', () => {
|
||
const clientSrc = readFileSync(path.join(embeddingsDir, 'embedding-sidecar-client.ts'), 'utf8');
|
||
const façadeSrc = readFileSync(path.join(embeddingsDir, 'embedder.ts'), 'utf8');
|
||
const importLines = clientSrc
|
||
.split('\n')
|
||
.filter(
|
||
(line) =>
|
||
/^\s*import\b/.test(line) || /^\s*\} from /.test(line) || /import\s*\(/.test(line),
|
||
)
|
||
.join('\n');
|
||
expect(clientSrc).not.toMatch(/worker_threads/);
|
||
expect(clientSrc).not.toMatch(/new Worker\b/);
|
||
expect(importLines).not.toContain('embedding-pipeline');
|
||
expect(importLines).not.toContain('embedding-identity');
|
||
expect(importLines).not.toContain('./index.js');
|
||
expect(façadeSrc).not.toMatch(/from\s+['"]@huggingface\/transformers['"]/);
|
||
expect(façadeSrc).not.toMatch(/import\s*\(\s*['"]@huggingface\/transformers['"]/);
|
||
expect(façadeSrc).not.toMatch(/from\s+['"]onnxruntime-node['"]/);
|
||
expect(façadeSrc).not.toMatch(/import\s*\(\s*['"]onnxruntime-node['"]/);
|
||
expect(façadeSrc).not.toMatch(/from\s+['"].*onnxruntime-common-resolver['"]/);
|
||
expect(façadeSrc).not.toMatch(/import\s*\(\s*['"].*onnxruntime-common-resolver['"]/);
|
||
expect(façadeSrc).not.toMatch(/from\s+['"].*embedding-local-init['"]/);
|
||
expect(façadeSrc).not.toMatch(/import\s*\(\s*['"].*embedding-local-init['"]/);
|
||
});
|
||
|
||
it('sizes the init deadline from the HF download budget, not a 15s process lifetime', async () => {
|
||
const { SIDECAR_INIT_IPC_SLACK_MS, sidecarInitTimeoutMs } =
|
||
await import('../../src/core/embeddings/embedding-sidecar-client.js');
|
||
delete process.env.HF_DOWNLOAD_TIMEOUT_MS;
|
||
delete process.env.HF_MAX_ATTEMPTS;
|
||
// 3 attempts × 5 min plus 2s + 4s exponential backoff, plus IPC slack.
|
||
expect(sidecarInitTimeoutMs()).toBe(
|
||
5 * 60 * 1_000 * 3 + 2_000 + 4_000 + SIDECAR_INIT_IPC_SLACK_MS,
|
||
);
|
||
expect(sidecarInitTimeoutMs()).toBeGreaterThan(15_000);
|
||
|
||
process.env.HF_DOWNLOAD_TIMEOUT_MS = '120000';
|
||
process.env.HF_MAX_ATTEMPTS = '2';
|
||
expect(sidecarInitTimeoutMs()).toBe(240_000 + 2_000 + SIDECAR_INIT_IPC_SLACK_MS);
|
||
|
||
process.env.HF_DOWNLOAD_TIMEOUT_MS = String(60 * 60 * 1_000);
|
||
process.env.HF_MAX_ATTEMPTS = '1';
|
||
expect(sidecarInitTimeoutMs()).toBe(30 * 60 * 1_000 + SIDECAR_INIT_IPC_SLACK_MS);
|
||
|
||
process.env.HF_DOWNLOAD_TIMEOUT_MS = '120000';
|
||
process.env.HF_MAX_ATTEMPTS = '9.5';
|
||
expect(sidecarInitTimeoutMs()).toBe(
|
||
120_000 * 9 + 2_000 * (2 ** 8 - 1) + SIDECAR_INIT_IPC_SLACK_MS,
|
||
);
|
||
});
|
||
|
||
it('clears the reap wait timeout once the child closes', async () => {
|
||
vi.useFakeTimers();
|
||
const { ensureEmbeddingSidecar, reapEmbeddingSidecarAndWait } =
|
||
await import('../../src/core/embeddings/embedding-sidecar-client.js');
|
||
await ensureEmbeddingSidecar();
|
||
await reapEmbeddingSidecarAndWait(5_000);
|
||
expect(vi.getTimerCount()).toBe(0);
|
||
});
|
||
|
||
it('does not let a reaped child reset its replacement', async () => {
|
||
const { ensureEmbeddingSidecar, reapEmbeddingSidecar, sidecarEmbedBatch } =
|
||
await import('../../src/core/embeddings/embedding-sidecar-client.js');
|
||
await ensureEmbeddingSidecar();
|
||
const first = children[0];
|
||
reapEmbeddingSidecar();
|
||
await ensureEmbeddingSidecar();
|
||
expect(forkMock).toHaveBeenCalledTimes(2);
|
||
first.emit('error', new Error('late error from reaped child'));
|
||
first.emit('close', 1, null);
|
||
await expect(sidecarEmbedBatch(['still-alive'])).resolves.toHaveLength(1);
|
||
expect(forkMock).toHaveBeenCalledTimes(2);
|
||
});
|
||
});
|