mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-15 04:18:05 +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>
378 lines
14 KiB
TypeScript
378 lines
14 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import {
|
|
displayWidth,
|
|
doctorCommand,
|
|
localEmbeddingDoctorStatus,
|
|
padDisplayEnd,
|
|
nativeStatusLine,
|
|
pageSizeDoctorLines,
|
|
poolSizeDoctorLine,
|
|
} from '../../src/cli/doctor.js';
|
|
import { setCliLanguage, type SupportedCliLanguage } from '../../src/cli/i18n/index.js';
|
|
import type { NativeCheckResult } from '../../src/core/lbug/native-check.js';
|
|
|
|
const nativeProbeState = vi.hoisted(() => ({
|
|
vectorLoaded: true,
|
|
fts: { loaded: true } as {
|
|
loaded: boolean;
|
|
suppressed?: boolean;
|
|
reason?: string;
|
|
},
|
|
}));
|
|
|
|
vi.mock('../../src/core/lbug/native-check.js', () => ({
|
|
checkLbugNative: () => ({ ok: true, binaryPath: '/synthetic/lbugjs.node' }),
|
|
ftsAvailabilityLabel: (probe: { loaded: boolean; suppressed?: boolean }) =>
|
|
probe.loaded ? 'available' : probe.suppressed ? 'suppressed' : 'unavailable',
|
|
probeFtsExtensionLoad: async () => nativeProbeState.fts,
|
|
probeVectorExtensionLoad: async () =>
|
|
nativeProbeState.vectorLoaded
|
|
? { loaded: true }
|
|
: { loaded: false, reason: 'synthetic VECTOR load failure' },
|
|
}));
|
|
|
|
describe('doctor output formatting', () => {
|
|
it('keeps ASCII padding equivalent to String.padEnd', () => {
|
|
expect(displayWidth('OS:')).toBe(3);
|
|
expect(padDisplayEnd('OS:', 10)).toBe('OS:'.padEnd(10));
|
|
});
|
|
|
|
it('pads CJK labels by terminal display width, not code-unit length', () => {
|
|
const padded = padDisplayEnd('系统:', 10);
|
|
|
|
expect(displayWidth('系统:')).toBe(6);
|
|
expect(displayWidth(padded)).toBe(10);
|
|
expect(padded).toBe('系统: ');
|
|
});
|
|
|
|
it('does not truncate labels that are already wider than the target width', () => {
|
|
expect(padDisplayEnd('图存储:', 4)).toBe('图存储:');
|
|
});
|
|
});
|
|
|
|
describe('doctor VECTOR capability claims', () => {
|
|
const ENV_KEYS = [
|
|
'GITNEXUS_EMBEDDING_URL',
|
|
'GITNEXUS_EMBEDDING_MODEL',
|
|
'GITNEXUS_EMBEDDING_DIMS',
|
|
] as const;
|
|
const savedEnv = Object.fromEntries(ENV_KEYS.map((key) => [key, process.env[key]]));
|
|
|
|
const renderDoctor = async (
|
|
vectorLoaded: boolean,
|
|
language: SupportedCliLanguage = 'en',
|
|
): Promise<string> => {
|
|
nativeProbeState.vectorLoaded = vectorLoaded;
|
|
setCliLanguage(language);
|
|
process.env.GITNEXUS_EMBEDDING_URL = 'http://127.0.0.1:9/v1';
|
|
process.env.GITNEXUS_EMBEDDING_MODEL = 'synthetic-doctor';
|
|
process.env.GITNEXUS_EMBEDDING_DIMS = '384';
|
|
const log = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
|
|
await doctorCommand();
|
|
|
|
return log.mock.calls.map((args) => args.map(String).join(' ')).join('\n');
|
|
};
|
|
|
|
afterEach(() => {
|
|
nativeProbeState.vectorLoaded = true;
|
|
nativeProbeState.fts = { loaded: true };
|
|
setCliLanguage(null);
|
|
vi.restoreAllMocks();
|
|
for (const key of ENV_KEYS) {
|
|
if (savedEnv[key] === undefined) delete process.env[key];
|
|
else process.env[key] = savedEnv[key];
|
|
}
|
|
});
|
|
|
|
it('reports a loaded extension without claiming a repository index exists', async () => {
|
|
const output = await renderDoctor(true);
|
|
|
|
expect(output).toContain('VECTOR extension: available');
|
|
expect(output).toContain(
|
|
'Semantic support: vector-index capable (repository index not checked)',
|
|
);
|
|
expect(output).not.toContain('VECTOR index:');
|
|
expect(output).not.toContain('Semantic mode:');
|
|
});
|
|
|
|
it('reports exact-scan capability when the extension cannot load', async () => {
|
|
const output = await renderDoctor(false);
|
|
|
|
expect(output).toContain('VECTOR extension: unavailable');
|
|
expect(output).toContain('Semantic support: exact-scan only (VECTOR extension unavailable)');
|
|
});
|
|
|
|
it('keeps the corrected capability claims localized', async () => {
|
|
const output = await renderDoctor(true, 'zh-CN');
|
|
|
|
expect(output).toContain('VECTOR 扩展:');
|
|
expect(output).toContain('语义支持:');
|
|
expect(output).toContain('支持向量索引(未检查仓库索引)');
|
|
});
|
|
});
|
|
|
|
describe('doctor FTS policy claims (U12)', () => {
|
|
afterEach(() => {
|
|
nativeProbeState.fts = { loaded: true };
|
|
setCliLanguage(null);
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('reports suppressed-by-policy, not unavailable, when the probe is suppressed', async () => {
|
|
nativeProbeState.fts = {
|
|
loaded: false,
|
|
suppressed: true,
|
|
reason: 'suppressed by policy GITNEXUS_LBUG_EXTENSION_INSTALL=never',
|
|
};
|
|
const log = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
await doctorCommand();
|
|
const output = log.mock.calls.map((args) => args.map(String).join(' ')).join('\n');
|
|
|
|
expect(output).toMatch(/Full-text search:\s+suppressed/);
|
|
expect(output).toContain('suppressed by policy GITNEXUS_LBUG_EXTENSION_INSTALL=never');
|
|
expect(output).not.toMatch(/Full-text search:\s+unavailable/);
|
|
});
|
|
|
|
it('prints both serve/query and analyze extension install policies', async () => {
|
|
const log = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
await doctorCommand();
|
|
const output = log.mock.calls.map((args) => args.map(String).join(' ')).join('\n');
|
|
|
|
expect(output).toMatch(/serve\/query=/);
|
|
expect(output).toMatch(/analyze=/);
|
|
});
|
|
});
|
|
|
|
describe('doctor embedding-runtime support status', () => {
|
|
it('flags local embeddings as unavailable on macOS Intel (darwin/x64)', () => {
|
|
const { status, detail } = localEmbeddingDoctorStatus({
|
|
httpMode: false,
|
|
platform: 'darwin',
|
|
arch: 'x64',
|
|
});
|
|
expect(status).toBe('✗ local embeddings unavailable on darwin/x64');
|
|
expect(detail).not.toBeNull();
|
|
expect(detail).toMatch(/macOS Intel/);
|
|
expect(detail).toMatch(/native binding/i);
|
|
});
|
|
|
|
it('reports local embeddings as supported on darwin/arm64, linux/x64, and win32/x64', () => {
|
|
for (const [platform, arch] of [
|
|
['darwin', 'arm64'],
|
|
['linux', 'x64'],
|
|
['win32', 'x64'],
|
|
] as Array<[NodeJS.Platform, NodeJS.Architecture]>) {
|
|
const { status, detail } = localEmbeddingDoctorStatus({
|
|
httpMode: false,
|
|
platform,
|
|
arch,
|
|
resolution: { source: 'package' },
|
|
});
|
|
expect(status).toBe('✓ local embeddings supported');
|
|
expect(detail).toBeNull();
|
|
}
|
|
});
|
|
|
|
it('reports HTTP backend as configured and never blocks on platform', () => {
|
|
const { status, detail } = localEmbeddingDoctorStatus({
|
|
httpMode: true,
|
|
platform: 'darwin',
|
|
arch: 'x64',
|
|
});
|
|
expect(status).toBe('✓ http endpoint configured');
|
|
expect(detail).toBeNull();
|
|
});
|
|
|
|
it('flags a missing local embedding stack with install guidance', () => {
|
|
const { status, detail } = localEmbeddingDoctorStatus({
|
|
httpMode: false,
|
|
platform: 'linux',
|
|
arch: 'x64',
|
|
resolution: null,
|
|
});
|
|
expect(status).toBe('✗ local embedding stack not installed');
|
|
expect(detail).toContain('gitnexus embeddings install');
|
|
expect(detail).not.toContain('ONNXRUNTIME_NODE_INSTALL=skip');
|
|
});
|
|
|
|
it('reports a package-sourced stack as supported regardless of Node loadability', () => {
|
|
const { status, detail } = localEmbeddingDoctorStatus({
|
|
httpMode: false,
|
|
platform: 'linux',
|
|
arch: 'x64',
|
|
resolution: { source: 'package' },
|
|
prefixLoadable: false,
|
|
});
|
|
expect(status).toBe('✓ local embeddings supported');
|
|
expect(detail).toBeNull();
|
|
});
|
|
|
|
it('flags a prefix-sourced stack that this Node cannot load (#2372)', () => {
|
|
const { status, detail } = localEmbeddingDoctorStatus({
|
|
httpMode: false,
|
|
platform: 'linux',
|
|
arch: 'x64',
|
|
resolution: { source: 'runtime-prefix' },
|
|
prefixLoadable: false,
|
|
});
|
|
expect(status).toBe('✗ embedding stack installed in the prefix but not loadable on this Node');
|
|
expect(detail).toContain('module.registerHooks');
|
|
});
|
|
|
|
it('reports a prefix-sourced stack as supported when this Node can load it', () => {
|
|
const { status, detail } = localEmbeddingDoctorStatus({
|
|
httpMode: false,
|
|
platform: 'linux',
|
|
arch: 'x64',
|
|
resolution: { source: 'runtime-prefix' },
|
|
prefixLoadable: true,
|
|
});
|
|
expect(status).toBe('✓ local embeddings supported');
|
|
expect(detail).toBeNull();
|
|
});
|
|
|
|
it('prefers the platform blocker over the missing-stack report on macOS Intel', () => {
|
|
const { status } = localEmbeddingDoctorStatus({
|
|
httpMode: false,
|
|
platform: 'darwin',
|
|
arch: 'x64',
|
|
resolution: null,
|
|
});
|
|
expect(status).toBe('✗ local embeddings unavailable on darwin/x64');
|
|
});
|
|
|
|
it('never reports a missing stack in HTTP mode', () => {
|
|
const { status, detail } = localEmbeddingDoctorStatus({
|
|
httpMode: true,
|
|
resolution: null,
|
|
});
|
|
expect(status).toBe('✓ http endpoint configured');
|
|
expect(detail).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('doctor page-size lines (#1231, #2424 review)', () => {
|
|
it('warns on a non-4K page size with a pre-0.18.0 @ladybugdb/core', () => {
|
|
const lines = pageSizeDoctorLines(16384, '0.17.1');
|
|
expect(lines).toHaveLength(2);
|
|
expect(lines[0]).toBe(` ${padDisplayEnd('page size', 10)}16384`);
|
|
// Byte-identical to the pre-extraction inline rendering — guards the
|
|
// helper extraction against output drift.
|
|
expect(lines[1]).toBe(
|
|
` ${padDisplayEnd('', 10)}⚠ non-4K page size with @ladybugdb/core < 0.18.0 — ` +
|
|
`'gitnexus analyze' may fail during COPY (#1231). Upgrade gitnexus (npm install -g gitnexus@latest).`,
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
['page-size-aware LadybugDB', 16384, '0.18.0'],
|
|
['a 4 KiB page size', 4096, '0.17.1'],
|
|
])('prints the page size without a warning for %s', (_label, pageSize, version) => {
|
|
const lines = pageSizeDoctorLines(pageSize, version);
|
|
expect(lines).toHaveLength(1);
|
|
expect(lines[0]).toContain('page size');
|
|
expect(lines[0]).toContain(String(pageSize));
|
|
});
|
|
|
|
it('prints nothing when the page size is unknown', () => {
|
|
expect(pageSizeDoctorLines(undefined, '0.17.1')).toHaveLength(0);
|
|
});
|
|
|
|
it('names an unknown version instead of asserting "< 0.18.0" about it', () => {
|
|
const lines = pageSizeDoctorLines(16384, undefined);
|
|
expect(lines).toHaveLength(2);
|
|
expect(lines[1]).toContain('an unknown @ladybugdb/core version (may predate 0.18.0)');
|
|
expect(lines[1]).not.toContain('with @ladybugdb/core < 0.18.0');
|
|
expect(lines[1]).toContain('npm install -g gitnexus@latest');
|
|
});
|
|
});
|
|
|
|
describe('doctor pool-size line (#2631)', () => {
|
|
const MiB = 1024 * 1024;
|
|
|
|
it('prints the hintless pool in MiB with no env note when the env var is unset', () => {
|
|
expect(poolSizeDoctorLine(2048 * MiB, undefined)).toBe(
|
|
` ${padDisplayEnd('pool size', 10)}2048 MiB`,
|
|
);
|
|
});
|
|
|
|
it('marks an operator-supplied absolute value as an env override, with no scaling suffix', () => {
|
|
expect(poolSizeDoctorLine(4096 * MiB, String(4096 * MiB))).toBe(
|
|
` ${padDisplayEnd('pool size', 10)}4096 MiB (env override)`,
|
|
);
|
|
});
|
|
|
|
it('labels the 0 sentinel as the native default instead of "0 MiB"', () => {
|
|
expect(poolSizeDoctorLine(0, '0')).toBe(
|
|
` ${padDisplayEnd('pool size', 10)}native 80% of RAM (env override)`,
|
|
);
|
|
});
|
|
});
|
|
|
|
// #2672: every failed check used to print "lbugjs.node missing", including the
|
|
// glibc case where the binary is present and merely unloadable — contradicting
|
|
// the detail printed directly beneath it and sending users to reinstall a file
|
|
// they already had.
|
|
describe('doctor native status line (#2672)', () => {
|
|
const nativeStatusCases: ReadonlyArray<readonly [string, NativeCheckResult, string]> = [
|
|
['a loaded binary', { ok: true, binaryPath: '/x/lbugjs.node' }, '✓ lbugjs.node loaded'],
|
|
[
|
|
'an uninstalled package',
|
|
{ ok: false, kind: 'package_missing', message: 'x' },
|
|
'✗ @ladybugdb/core not installed',
|
|
],
|
|
[
|
|
'an absent binary',
|
|
{ ok: false, kind: 'binary_missing', binaryPath: '/x/lbugjs.node', message: 'x' },
|
|
'✗ lbugjs.node missing',
|
|
],
|
|
[
|
|
'a present-but-unloadable binary (glibc too old, truncated download)',
|
|
{ ok: false, kind: 'load_failed', binaryPath: '/x/lbugjs.node', message: 'x' },
|
|
'✗ lbugjs.node present but failed to load',
|
|
],
|
|
[
|
|
'a prebuilt that exists but could not be copied into a read-only node_modules',
|
|
{ ok: false, kind: 'binary_unwritable', binaryPath: '/x/lbugjs.node', message: 'x' },
|
|
'✗ lbugjs.node not installed (node_modules not writable)',
|
|
],
|
|
['a failure with no kind recorded', { ok: false, message: 'x' }, '✗ lbugjs.node missing'],
|
|
];
|
|
|
|
it.each(nativeStatusCases)('reports %s', (_name, check, expected) => {
|
|
expect(nativeStatusLine(check)).toBe(` ${padDisplayEnd('native', 10)}${expected}`);
|
|
});
|
|
});
|
|
|
|
describe('doctor survives a malformed GITNEXUS_EMBEDDING_DIMS (#2385)', () => {
|
|
const ENV_KEYS = [
|
|
'GITNEXUS_EMBEDDING_URL',
|
|
'GITNEXUS_EMBEDDING_MODEL',
|
|
'GITNEXUS_EMBEDDING_DIMS',
|
|
] as const;
|
|
const savedEnv = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]]));
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
for (const key of ENV_KEYS) {
|
|
if (savedEnv[key] === undefined) {
|
|
delete process.env[key];
|
|
} else {
|
|
process.env[key] = savedEnv[key];
|
|
}
|
|
}
|
|
});
|
|
|
|
it('does not crash at the unguarded isHttpMode() call sites', async () => {
|
|
process.env.GITNEXUS_EMBEDDING_URL = 'http://test:8080/v1';
|
|
process.env.GITNEXUS_EMBEDDING_MODEL = 'test-model';
|
|
process.env.GITNEXUS_EMBEDDING_DIMS = '1024abc';
|
|
vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
|
|
// Before the isHttpMode() root-cause fix (#2385) this threw at doctor.ts:167
|
|
// (isHttpMode -> readConfig -> throw on the malformed DIMS); now the presence
|
|
// probe never throws, so `gitnexus doctor` completes and reports the backend.
|
|
await expect(doctorCommand()).resolves.toBeUndefined();
|
|
});
|
|
});
|