mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-07 08:26:11 +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
151 lines
6.7 KiB
TypeScript
151 lines
6.7 KiB
TypeScript
/**
|
|
* Tests for the LadybugDB wipe-failure path in the `analyzeCommand` CLI
|
|
* (#2409, tri-review 4669518496 P2-4).
|
|
*
|
|
* When `wipeLbugDbFiles` cannot verify the DB file family is gone (another
|
|
* process holds the index open — MCP server, serve worker, antivirus scan),
|
|
* analyze rejects with a typed `LbugWipeError`. The CLI must render the
|
|
* dedicated recovery-hint branch — classified by error TYPE, the repo norm
|
|
* from #2385 — and must NOT fall through to the raw-stack
|
|
* `writeFatalToStderr` fallback.
|
|
*
|
|
* Mirrors analyze-http-endpoint-error.test.ts:
|
|
* - vi.mock the heavy dependencies so no real DB / git is touched
|
|
* - drive `analyzeCommand` with a mocked `runFullAnalysis` that rejects
|
|
* - assert on process.exitCode and the captured logger records
|
|
*/
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const runFullAnalysisMock = vi.fn();
|
|
|
|
const resolveEmbeddingRuntimeMock = vi.fn<() => { source: string } | null>(() => ({
|
|
source: 'package',
|
|
}));
|
|
const isPrefixRuntimeLoadableMock = vi.fn(() => true);
|
|
const installEmbeddingRuntimeMock = vi.fn(async () => undefined);
|
|
vi.mock('../../src/core/embeddings/runtime-install.js', async (importOriginal) => ({
|
|
...(await importOriginal<typeof import('../../src/core/embeddings/runtime-install.js')>()),
|
|
resolveEmbeddingRuntime: () => resolveEmbeddingRuntimeMock(),
|
|
isPrefixRuntimeLoadable: () => isPrefixRuntimeLoadableMock(),
|
|
// Unlike the analyze-http-endpoint-error model, no args are forwarded: the
|
|
// zero-arg mock's signature makes a spread call a type error (TS2556).
|
|
installEmbeddingRuntime: () => installEmbeddingRuntimeMock(),
|
|
getEmbeddingRuntimeDir: () => '/fake/embedding-runtime',
|
|
}));
|
|
|
|
vi.mock('../../src/core/run-analyze.js', () => ({
|
|
runFullAnalysis: runFullAnalysisMock,
|
|
}));
|
|
|
|
// Preserve the REAL LbugWipeError (the CLI branch classifies by instanceof,
|
|
// so the test must throw the very class `cli/analyze.ts` imports); only the
|
|
// lifecycle functions are stubbed so no native DB is opened or closed.
|
|
vi.mock('../../src/core/lbug/lbug-adapter.js', async (importOriginal) => ({
|
|
...(await importOriginal<typeof import('../../src/core/lbug/lbug-adapter.js')>()),
|
|
closeLbug: vi.fn(async () => undefined),
|
|
closeLbugBeforeExit: vi.fn(async () => undefined),
|
|
isLbugReady: vi.fn(() => false),
|
|
}));
|
|
|
|
vi.mock('../../src/storage/repo-manager.js', () => ({
|
|
getStoragePaths: vi.fn(() => ({ storagePath: '.gitnexus', lbugPath: '.gitnexus/lbug' })),
|
|
getGlobalRegistryPath: vi.fn(() => 'registry.json'),
|
|
RegistryNameCollisionError: class RegistryNameCollisionError extends Error {},
|
|
AnalysisNotFinalizedError: class AnalysisNotFinalizedError extends Error {},
|
|
assertAnalysisFinalized: vi.fn(async () => undefined),
|
|
}));
|
|
|
|
vi.mock('../../src/storage/git.js', () => ({
|
|
getGitRoot: vi.fn(() => '/repo'),
|
|
hasGitDir: vi.fn(() => true),
|
|
}));
|
|
|
|
vi.mock('../../src/core/ingestion/utils/max-file-size.js', () => ({
|
|
getMaxFileSizeBannerMessage: vi.fn(() => null),
|
|
}));
|
|
|
|
// analyze.ts imports isHfDownloadFailure from hf-env.js. Mock it to break the
|
|
// transitive gitnexus-shared chain (never claims the wipe error either way).
|
|
vi.mock('../../src/core/embeddings/hf-env.js', () => ({
|
|
isHfDownloadFailure: vi.fn(() => false),
|
|
}));
|
|
|
|
describe('analyzeCommand LadybugDB wipe-failure handling (#2409, tri-review 4669518496)', () => {
|
|
// Capture the host's NODE_OPTIONS once so afterEach can restore it cleanly.
|
|
// Without the restore, beforeEach's append accumulated duplicate
|
|
// --max-old-space-size tokens across tests (analyze-worker-pool-size.test.ts
|
|
// pattern; #2424 review).
|
|
const ORIGINAL_NODE_OPTIONS = process.env.NODE_OPTIONS;
|
|
|
|
beforeEach(() => {
|
|
vi.resetModules();
|
|
runFullAnalysisMock.mockReset();
|
|
resolveEmbeddingRuntimeMock.mockReset().mockReturnValue({ source: 'package' });
|
|
isPrefixRuntimeLoadableMock.mockReset().mockReturnValue(true);
|
|
installEmbeddingRuntimeMock.mockReset().mockResolvedValue(undefined);
|
|
process.exitCode = undefined;
|
|
process.env.NODE_OPTIONS = `${process.env.NODE_OPTIONS ?? ''} --max-old-space-size=8192`.trim();
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (ORIGINAL_NODE_OPTIONS === undefined) {
|
|
delete process.env.NODE_OPTIONS;
|
|
} else {
|
|
process.env.NODE_OPTIONS = ORIGINAL_NODE_OPTIONS;
|
|
}
|
|
});
|
|
|
|
it('routes a wipe failure to the dedicated recovery hint, not the raw-stack fallback', async () => {
|
|
// Install the stderr spy BEFORE importing analyze.js: the module binds
|
|
// `realStderrWrite = process.stderr.write.bind(...)` at load time, so a
|
|
// later spy would miss the writeFatalToStderr fallback this test rules out.
|
|
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
try {
|
|
const { LbugWipeError } = await import('../../src/core/lbug/lbug-adapter.js');
|
|
const survivor = '/repo/.gitnexus/lbug.wal';
|
|
runFullAnalysisMock.mockRejectedValue(new LbugWipeError([survivor]));
|
|
|
|
const { _captureLogger } = await import('../../src/core/logger.js');
|
|
const cap = _captureLogger();
|
|
const { analyzeCommand } = await import('../../src/cli/analyze.js');
|
|
|
|
await analyzeCommand(undefined, { embeddings: true });
|
|
|
|
expect(process.exitCode).toBe(1);
|
|
const record = cap.records().find((r) => r.recoveryHint === 'lbug-wipe-failed');
|
|
expect(record).toBeDefined();
|
|
const text = typeof record?.msg === 'string' ? record.msg : '';
|
|
// The self-contained LbugWipeError message is surfaced verbatim:
|
|
// survivor path + the shared stop-MCP/AV-exclusion/re-run guidance
|
|
// (lbugLockRemediation, this shipping review FIX 7).
|
|
expect(text).toContain(survivor);
|
|
expect(text).toMatch(/stop any GitNexus MCP or serve process/i);
|
|
// The typed branch returns before writeFatalToStderr — the raw-stack
|
|
// fallback header must never hit stderr for this error class.
|
|
const stderrText = stderrSpy.mock.calls.map((call) => String(call[0])).join('');
|
|
expect(stderrText).not.toContain('Analysis failed');
|
|
cap.restore();
|
|
} finally {
|
|
stderrSpy.mockRestore();
|
|
}
|
|
});
|
|
|
|
it('does not claim unrelated analyze failures for the wipe branch', async () => {
|
|
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
try {
|
|
runFullAnalysisMock.mockRejectedValue(new Error('LadybugDB write failed'));
|
|
|
|
const { _captureLogger } = await import('../../src/core/logger.js');
|
|
const cap = _captureLogger();
|
|
const { analyzeCommand } = await import('../../src/cli/analyze.js');
|
|
|
|
await analyzeCommand(undefined, { embeddings: true });
|
|
|
|
expect(process.exitCode).toBe(1);
|
|
expect(cap.records().some((r) => r.recoveryHint === 'lbug-wipe-failed')).toBe(false);
|
|
cap.restore();
|
|
} finally {
|
|
stderrSpy.mockRestore();
|
|
}
|
|
});
|
|
});
|