mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (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
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
322 lines
13 KiB
TypeScript
322 lines
13 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
import { EventEmitter } from 'node:events';
|
|
import { homedir } from 'node:os';
|
|
import { join, resolve } from 'node:path';
|
|
import { createRequire } from 'node:module';
|
|
import {
|
|
ANALYZE_EMBEDDING_INSTALL_TIMEOUT_MS,
|
|
buildEmbeddingInstallCommand,
|
|
composeWin32NpmCommand,
|
|
getEmbeddingInstallTimeoutMs,
|
|
getEmbeddingRuntimeDir,
|
|
getEmbeddingStackSpecs,
|
|
installEmbeddingRuntime,
|
|
quoteWin32Arg,
|
|
resolveEmbeddingRuntime,
|
|
} from '../../src/core/embeddings/runtime-install.js';
|
|
|
|
const require = createRequire(import.meta.url);
|
|
|
|
// The spawn flow is exercised through a controllable fake child; nothing real
|
|
// is spawned. Only `spawn` is overridden — `execFileSync` (the win32 taskkill
|
|
// path) keeps its real binding. No `node:module` mock and no resetModules here,
|
|
// so the static import of runtime-install is safe (see the dual-instance rule).
|
|
const spawnMock = vi.fn();
|
|
vi.mock('node:child_process', async (importOriginal) => {
|
|
const orig = await importOriginal<typeof import('node:child_process')>();
|
|
return { ...orig, spawn: (...args: unknown[]) => spawnMock(...args) };
|
|
});
|
|
|
|
class FakeChild extends EventEmitter {
|
|
stdout = new EventEmitter();
|
|
stderr = new EventEmitter();
|
|
pid = 4242;
|
|
kill = vi.fn();
|
|
}
|
|
|
|
const ENV_KEYS = [
|
|
'GITNEXUS_EMBEDDING_RUNTIME_DIR',
|
|
'ONNXRUNTIME_NODE_INSTALL',
|
|
'GITNEXUS_EMBEDDING_INSTALL_TIMEOUT_MS',
|
|
] as const;
|
|
const savedEnv = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]]));
|
|
|
|
beforeEach(() => {
|
|
for (const key of ENV_KEYS) delete process.env[key];
|
|
});
|
|
|
|
afterEach(() => {
|
|
for (const key of ENV_KEYS) {
|
|
if (savedEnv[key] === undefined) delete process.env[key];
|
|
else process.env[key] = savedEnv[key];
|
|
}
|
|
});
|
|
|
|
describe('getEmbeddingRuntimeDir', () => {
|
|
it('defaults to ~/.gitnexus/embedding-runtime and honours the env override', () => {
|
|
expect(getEmbeddingRuntimeDir()).toBe(join(homedir(), '.gitnexus', 'embedding-runtime'));
|
|
// resolve() so the expectation matches on Windows too (where an absolute
|
|
// POSIX path picks up the cwd drive letter).
|
|
process.env.GITNEXUS_EMBEDDING_RUNTIME_DIR = resolve('/custom/runtime');
|
|
expect(getEmbeddingRuntimeDir()).toBe(resolve('/custom/runtime'));
|
|
});
|
|
|
|
it('resolves a relative override to an absolute path', () => {
|
|
process.env.GITNEXUS_EMBEDDING_RUNTIME_DIR = 'rel/runtime';
|
|
expect(getEmbeddingRuntimeDir()).toBe(resolve('rel/runtime'));
|
|
});
|
|
|
|
it('falls through to the default for an empty or whitespace override', () => {
|
|
const fallback = join(homedir(), '.gitnexus', 'embedding-runtime');
|
|
process.env.GITNEXUS_EMBEDDING_RUNTIME_DIR = '';
|
|
expect(getEmbeddingRuntimeDir()).toBe(fallback);
|
|
process.env.GITNEXUS_EMBEDDING_RUNTIME_DIR = ' ';
|
|
expect(getEmbeddingRuntimeDir()).toBe(fallback);
|
|
});
|
|
});
|
|
|
|
describe('getEmbeddingStackSpecs', () => {
|
|
it('mirrors the optionalDependencies manifest exactly (drift guard, #2370)', () => {
|
|
const manifest = require('../../package.json') as {
|
|
optionalDependencies: Record<string, string>;
|
|
};
|
|
expect(getEmbeddingStackSpecs()).toEqual({
|
|
'@huggingface/transformers': manifest.optionalDependencies['@huggingface/transformers'],
|
|
'onnxruntime-node': manifest.optionalDependencies['onnxruntime-node'],
|
|
});
|
|
expect(manifest.optionalDependencies['@huggingface/transformers']).toBeDefined();
|
|
expect(manifest.optionalDependencies['onnxruntime-node']).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('buildEmbeddingInstallCommand', () => {
|
|
it('defaults to a registry-only install: --ignore-scripts and the CUDA-download skip env', () => {
|
|
process.env.GITNEXUS_EMBEDDING_RUNTIME_DIR = resolve('/custom/runtime');
|
|
const { args, env } = buildEmbeddingInstallCommand();
|
|
expect(args.slice(0, 3)).toEqual(['install', '--prefix', resolve('/custom/runtime')]);
|
|
expect(args).toContain('--ignore-scripts');
|
|
const specs = getEmbeddingStackSpecs();
|
|
expect(args).toContain(`@huggingface/transformers@${specs['@huggingface/transformers']}`);
|
|
expect(args).toContain(`onnxruntime-node@${specs['onnxruntime-node']}`);
|
|
expect(env.ONNXRUNTIME_NODE_INSTALL).toBe('skip');
|
|
});
|
|
|
|
it('with cuda: runs install scripts and leaves the CUDA download enabled', () => {
|
|
const { args, env } = buildEmbeddingInstallCommand({ cuda: true });
|
|
expect(args).not.toContain('--ignore-scripts');
|
|
expect(env.ONNXRUNTIME_NODE_INSTALL).toBeUndefined();
|
|
});
|
|
|
|
it('with cuda: clears an inherited ONNXRUNTIME_NODE_INSTALL=skip', () => {
|
|
process.env.ONNXRUNTIME_NODE_INSTALL = 'skip';
|
|
const { env } = buildEmbeddingInstallCommand({ cuda: true });
|
|
expect(env.ONNXRUNTIME_NODE_INSTALL).toBeUndefined();
|
|
});
|
|
|
|
it('without cuda: sets the skip env even when the ambient value differs', () => {
|
|
process.env.ONNXRUNTIME_NODE_INSTALL = 'something-else';
|
|
const { env } = buildEmbeddingInstallCommand();
|
|
expect(env.ONNXRUNTIME_NODE_INSTALL).toBe('skip');
|
|
});
|
|
});
|
|
|
|
describe('resolveEmbeddingRuntime', () => {
|
|
it('finds the normally-installed stack (package source wins over the prefix)', () => {
|
|
process.env.GITNEXUS_EMBEDDING_RUNTIME_DIR = '/nonexistent/for/this/test';
|
|
expect(resolveEmbeddingRuntime()).toEqual({ source: 'package' });
|
|
});
|
|
});
|
|
|
|
describe('getEmbeddingInstallTimeoutMs', () => {
|
|
it('returns the caller default when the env override is unset', () => {
|
|
expect(getEmbeddingInstallTimeoutMs(ANALYZE_EMBEDDING_INSTALL_TIMEOUT_MS)).toBe(
|
|
ANALYZE_EMBEDDING_INSTALL_TIMEOUT_MS,
|
|
);
|
|
});
|
|
|
|
it('lets the env override win over the caller default (user can raise a short deadline)', () => {
|
|
process.env.GITNEXUS_EMBEDDING_INSTALL_TIMEOUT_MS = '900000';
|
|
expect(getEmbeddingInstallTimeoutMs(ANALYZE_EMBEDDING_INSTALL_TIMEOUT_MS)).toBe(900000);
|
|
});
|
|
|
|
it('ignores a non-positive env override and uses the caller default', () => {
|
|
process.env.GITNEXUS_EMBEDDING_INSTALL_TIMEOUT_MS = '-5';
|
|
expect(getEmbeddingInstallTimeoutMs(ANALYZE_EMBEDDING_INSTALL_TIMEOUT_MS)).toBe(
|
|
ANALYZE_EMBEDDING_INSTALL_TIMEOUT_MS,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('quoteWin32Arg', () => {
|
|
it('quotes a spaced path as a single token', () => {
|
|
expect(quoteWin32Arg('C:\\Users\\John Doe\\.gitnexus\\rt')).toBe(
|
|
'"C:\\Users\\John Doe\\.gitnexus\\rt"',
|
|
);
|
|
});
|
|
|
|
it('quotes a caret semver spec so cmd.exe cannot eat the ^', () => {
|
|
expect(quoteWin32Arg('@huggingface/transformers@^4.1.0')).toBe(
|
|
'"@huggingface/transformers@^4.1.0"',
|
|
);
|
|
});
|
|
|
|
it('doubles the trailing backslash run so the closing quote is not escaped', () => {
|
|
// A spaced path (needs quoting) ending in a backslash: the added closing
|
|
// quote must not be escaped by that trailing backslash.
|
|
expect(quoteWin32Arg('C:\\Users\\John Doe\\')).toBe('"C:\\Users\\John Doe\\\\"');
|
|
});
|
|
|
|
it('quotes the empty string', () => {
|
|
expect(quoteWin32Arg('')).toBe('""');
|
|
});
|
|
|
|
it('leaves plain args untouched', () => {
|
|
expect(quoteWin32Arg('install')).toBe('install');
|
|
expect(quoteWin32Arg('--no-fund')).toBe('--no-fund');
|
|
});
|
|
|
|
it('throws on an embedded double quote', () => {
|
|
expect(() => quoteWin32Arg('a"b')).toThrow(/double quote/);
|
|
});
|
|
|
|
it('throws on NUL/CR/LF', () => {
|
|
expect(() => quoteWin32Arg('a\nb')).toThrow(/NUL\/CR\/LF/);
|
|
expect(() => quoteWin32Arg('a\rb')).toThrow(/NUL\/CR\/LF/);
|
|
expect(() => quoteWin32Arg('a\0b')).toThrow(/NUL\/CR\/LF/);
|
|
});
|
|
|
|
it('composeWin32NpmCommand leaves npm unquoted and quotes the args', () => {
|
|
const line = composeWin32NpmCommand(['install', '--prefix', 'C:\\a b\\rt']);
|
|
expect(line).toBe('npm install --prefix "C:\\a b\\rt"');
|
|
});
|
|
});
|
|
|
|
describe('installEmbeddingRuntime — spawn lifecycle', () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
spawnMock.mockReset();
|
|
});
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('rejects with a timeout message and SIGKILLs the child when npm never exits', async () => {
|
|
const child = new FakeChild();
|
|
spawnMock.mockReturnValue(child);
|
|
const p = installEmbeddingRuntime({}, 1000);
|
|
const assertion = expect(p).rejects.toThrow(
|
|
/timed out after 1000ms.*GITNEXUS_EMBEDDING_INSTALL_TIMEOUT_MS/s,
|
|
);
|
|
await vi.advanceTimersByTimeAsync(1000);
|
|
await assertion;
|
|
expect(child.kill).toHaveBeenCalledWith('SIGKILL');
|
|
});
|
|
|
|
it('honours GITNEXUS_EMBEDDING_INSTALL_TIMEOUT_MS for the default timeout', async () => {
|
|
process.env.GITNEXUS_EMBEDDING_INSTALL_TIMEOUT_MS = '1234';
|
|
const child = new FakeChild();
|
|
spawnMock.mockReturnValue(child);
|
|
const p = installEmbeddingRuntime();
|
|
const assertion = expect(p).rejects.toThrow(/timed out after 1234ms/);
|
|
await vi.advanceTimersByTimeAsync(1234);
|
|
await assertion;
|
|
});
|
|
|
|
it('lets an explicit timeoutMs override the env default', async () => {
|
|
process.env.GITNEXUS_EMBEDDING_INSTALL_TIMEOUT_MS = '999999';
|
|
const child = new FakeChild();
|
|
spawnMock.mockReturnValue(child);
|
|
const p = installEmbeddingRuntime({}, 500);
|
|
const assertion = expect(p).rejects.toThrow(/timed out after 500ms/);
|
|
await vi.advanceTimersByTimeAsync(500);
|
|
await assertion;
|
|
});
|
|
|
|
it('names the signal instead of "exit null" when the child is killed', async () => {
|
|
const child = new FakeChild();
|
|
spawnMock.mockReturnValue(child);
|
|
const p = installEmbeddingRuntime({}, 10_000);
|
|
const assertion = expect(p).rejects.toThrow(/killed with SIGKILL/);
|
|
child.emit('close', null, 'SIGKILL');
|
|
await assertion;
|
|
});
|
|
|
|
it('resolves on exit 0 and removes the parent-exit listener', async () => {
|
|
const child = new FakeChild();
|
|
spawnMock.mockReturnValue(child);
|
|
const before = process.listenerCount('exit');
|
|
const p = installEmbeddingRuntime({}, 10_000);
|
|
child.emit('close', 0, null);
|
|
await expect(p).resolves.toBeUndefined();
|
|
expect(process.listenerCount('exit')).toBe(before);
|
|
});
|
|
|
|
it('rejects once on child error; a later close does not double-settle', async () => {
|
|
const child = new FakeChild();
|
|
spawnMock.mockReturnValue(child);
|
|
const p = installEmbeddingRuntime({}, 10_000);
|
|
const assertion = expect(p).rejects.toThrow('spawn npm ENOENT');
|
|
child.emit('error', new Error('spawn npm ENOENT'));
|
|
await assertion;
|
|
expect(() => child.emit('close', 1, null)).not.toThrow();
|
|
});
|
|
|
|
it('on win32 spawns a single composed command string, no args array (DEP0190-free)', async () => {
|
|
const realPlatform = process.platform;
|
|
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
|
try {
|
|
// A spaced prefix must flow through compose+quote into ONE string arg. Use
|
|
// resolve() so the path is absolute on the real host too (a bare POSIX path
|
|
// picks up the cwd drive on Windows); the space survives either way.
|
|
process.env.GITNEXUS_EMBEDDING_RUNTIME_DIR = resolve('/opt/John Doe/rt');
|
|
const child = new FakeChild();
|
|
spawnMock.mockReturnValue(child);
|
|
const p = installEmbeddingRuntime({}, 10_000);
|
|
child.emit('close', 0, null);
|
|
await p;
|
|
const call = spawnMock.mock.calls[0] as [unknown, unknown];
|
|
// Byte-identical to the pure compose of the same args, and the spaced
|
|
// prefix appears quoted — host-independent (both sides use the real fns).
|
|
expect(call[0]).toBe(composeWin32NpmCommand(buildEmbeddingInstallCommand().args));
|
|
expect(call[0]).toContain(quoteWin32Arg(getEmbeddingRuntimeDir()));
|
|
expect(call[1]).toMatchObject({ shell: true });
|
|
} finally {
|
|
Object.defineProperty(process, 'platform', { value: realPlatform, configurable: true });
|
|
}
|
|
});
|
|
|
|
it('on posix spawns the array form with no shell', async () => {
|
|
const realPlatform = process.platform;
|
|
Object.defineProperty(process, 'platform', { value: 'linux', configurable: true });
|
|
try {
|
|
const child = new FakeChild();
|
|
spawnMock.mockReturnValue(child);
|
|
const p = installEmbeddingRuntime({}, 10_000);
|
|
child.emit('close', 0, null);
|
|
await p;
|
|
const call = spawnMock.mock.calls[0] as [unknown, unknown, unknown];
|
|
expect(call[0]).toBe('npm');
|
|
expect(Array.isArray(call[1])).toBe(true);
|
|
expect(call[2]).not.toMatchObject({ shell: true });
|
|
} finally {
|
|
Object.defineProperty(process, 'platform', { value: realPlatform, configurable: true });
|
|
}
|
|
});
|
|
|
|
it('spawns with cwd set to homedir(), independent of process.cwd()', async () => {
|
|
const realPlatform = process.platform;
|
|
// Force the posix branch so the options object is at a stable arg position.
|
|
Object.defineProperty(process, 'platform', { value: 'linux', configurable: true });
|
|
try {
|
|
const child = new FakeChild();
|
|
spawnMock.mockReturnValue(child);
|
|
const p = installEmbeddingRuntime({}, 10_000);
|
|
child.emit('close', 0, null);
|
|
await p;
|
|
const call = spawnMock.mock.calls[0] as [unknown, unknown, unknown];
|
|
expect(call[2]).toMatchObject({ cwd: homedir() });
|
|
} finally {
|
|
Object.defineProperty(process, 'platform', { value: realPlatform, configurable: true });
|
|
}
|
|
});
|
|
});
|