mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-23 00:41:36 +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
* fix(embeddings): guard local ONNX runtime on macOS Intel before transformers.js import macOS Intel (darwin/x64) crashed on `gitnexus analyze --embeddings` with a raw `Cannot find module .../bin/napi-v6/darwin/x64/onnxruntime_binding.node`: both embedders imported @huggingface/transformers at module scope, which loads onnxruntime-node and resolves the (unshipped) native binding before any backend could be selected. ONNX_WEB_BACKEND=wasm could not help (#1516). - Add a native-free runtime-support guard (getLocalEmbeddingRuntimeBlocker) that returns a clear, actionable message on darwin/x64 and null elsewhere. - Convert both the core and MCP embedders to type-only transformers imports plus a guarded lazy `await import()`; throw the blocker in initEmbedder before any transformers.js / onnxruntime-node resolution. HTTP mode is unaffected. - Surface the blocker cleanly in the analyze CLI instead of the misleading "installation may be corrupt" module-not-found hint. - Add unit tests: guard DI, lazy-import timing, core+MCP darwin/x64 rejection, and HTTP mode not blocked. Refs #1515, #1516 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(doctor): surface macOS Intel local-embedding limitation `gitnexus doctor` now reports whether the local embedding runtime can load on the current platform. macOS Intel (darwin/x64) users see up front that local embeddings are unavailable — plus the recommended alternatives — instead of only discovering it when `analyze --embeddings` fails (#1515). The Embeddings section gains a "Support" line; on a blocked platform the full guidance (reused from getLocalEmbeddingRuntimeBlocker, single source of truth) is written to stderr. doctor stays import-safe — it never loads transformers.js or onnxruntime-node, so it runs cleanly on macOS Intel. Refs #1515 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(embeddings): close #1515 guard coverage gaps + PR #1987 review polish Resolves the maintainer tri-review feedback on PR #1987: - Add the analyze error-branch test (new analyze-local-embedding-error.test.ts): a darwin/x64 blocker routes to the clean local-embedding-unsupported message (exit 1), not the module-not-found "installation may be corrupt" branch, and wins over isHfDownloadFailure even when both match (guards the reorder below). - Cover the MCP embedQuery darwin/x64 paths — HTTP bypass via httpEmbedQuery without importing transformers, and local-mode rejection before the import. - Make the "defaults platform/arch" guard test falsifiable by stubbing the platform, instead of asserting null === null on the CI host. - analyze.ts: evaluate the blocker-message branch before the network-heuristic isHfDownloadFailure branch so the explicit platform message takes priority. - runtime-support.ts: the blocker message now also notes GITNEXUS_EMBEDDING_DEVICE =wasm/cpu cannot help, not only ONNX_WEB_BACKEND=wasm. - doctor.ts: resolve platform/arch once instead of re-resolving after the guard. Refs #1515, #1516 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
151 lines
6 KiB
TypeScript
151 lines
6 KiB
TypeScript
/**
|
|
* Tests for the local-embedding-runtime blocker error path in the
|
|
* `analyzeCommand` CLI (#1515 / #1987 review follow-up).
|
|
*
|
|
* On macOS Intel (darwin/x64) `initEmbedder` throws a GitNexus-authored blocker
|
|
* before importing transformers.js. The analyze error handler must route that
|
|
* message to a clean `local-embedding-unsupported` message (exit 1) — not the
|
|
* generic MODULE_NOT_FOUND "installation may be corrupt" hint, and not the
|
|
* network-heuristic HF-download branch — so the explicit platform message wins.
|
|
*
|
|
* Mirrors the shape of analyze-wal-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 { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { getLocalEmbeddingRuntimeBlocker } from '../../src/core/embeddings/runtime-support.js';
|
|
|
|
const runFullAnalysisMock = vi.fn();
|
|
// Controllable so the dual-match scenario can force the network heuristic to
|
|
// also match the blocker error and prove the blocker branch still wins.
|
|
const isHfDownloadFailureMock = vi.fn(() => false);
|
|
|
|
vi.mock('../../src/core/run-analyze.js', () => ({
|
|
runFullAnalysis: runFullAnalysisMock,
|
|
}));
|
|
|
|
vi.mock('../../src/core/lbug/lbug-adapter.js', () => ({
|
|
closeLbug: vi.fn(async () => undefined),
|
|
}));
|
|
|
|
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, which transitively
|
|
// pulls gitnexus-shared. Mock it to break the chain and to drive the
|
|
// blocker-vs-HF ordering test below. isLocalEmbeddingRuntimeBlockerMessage
|
|
// (runtime-support.js) is intentionally NOT mocked — the real branch must fire.
|
|
vi.mock('../../src/core/embeddings/hf-env.js', () => ({
|
|
isHfDownloadFailure: isHfDownloadFailureMock,
|
|
}));
|
|
|
|
const blockerMessage = getLocalEmbeddingRuntimeBlocker({
|
|
platform: 'darwin',
|
|
arch: 'x64',
|
|
}) as string;
|
|
|
|
describe('analyzeCommand local-embedding-runtime error handling', () => {
|
|
beforeEach(() => {
|
|
vi.resetModules();
|
|
runFullAnalysisMock.mockReset();
|
|
isHfDownloadFailureMock.mockReset();
|
|
isHfDownloadFailureMock.mockReturnValue(false);
|
|
process.exitCode = undefined;
|
|
// Ensure ensureHeap() short-circuits (heap already at target size)
|
|
process.env.NODE_OPTIONS = `${process.env.NODE_OPTIONS ?? ''} --max-old-space-size=8192`.trim();
|
|
});
|
|
|
|
it('routes the blocker to a clean local-embedding-unsupported message (exit 1)', async () => {
|
|
runFullAnalysisMock.mockRejectedValue(new Error(blockerMessage));
|
|
|
|
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 records = cap.records();
|
|
const blockerRecord = records.find((r) => r.recoveryHint === 'local-embedding-unsupported');
|
|
expect(blockerRecord).toBeDefined();
|
|
expect(typeof blockerRecord?.msg === 'string' && blockerRecord.msg).toMatch(/macOS Intel/);
|
|
|
|
cap.restore();
|
|
});
|
|
|
|
it('does NOT fall through to the module-not-found "installation may be corrupt" hint', async () => {
|
|
runFullAnalysisMock.mockRejectedValue(new Error(blockerMessage));
|
|
|
|
const { _captureLogger } = await import('../../src/core/logger.js');
|
|
const cap = _captureLogger();
|
|
const { analyzeCommand } = await import('../../src/cli/analyze.js');
|
|
|
|
await analyzeCommand(undefined, { embeddings: true });
|
|
|
|
const records = cap.records();
|
|
const corruptRecord = records.find(
|
|
(r) => typeof r.msg === 'string' && r.msg.includes('installation may be corrupt'),
|
|
);
|
|
expect(corruptRecord).toBeUndefined();
|
|
|
|
cap.restore();
|
|
});
|
|
|
|
it('wins over the HF-download branch even when isHfDownloadFailure also matches (R4 ordering)', async () => {
|
|
// Force the network heuristic to claim the blocker error too. Because the
|
|
// blocker check is ordered before isHfDownloadFailure, the blocker branch
|
|
// must still win — this is the only scenario that falsifies a wrong order.
|
|
isHfDownloadFailureMock.mockReturnValue(true);
|
|
runFullAnalysisMock.mockRejectedValue(new Error(blockerMessage));
|
|
|
|
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 records = cap.records();
|
|
expect(records.some((r) => r.recoveryHint === 'local-embedding-unsupported')).toBe(true);
|
|
// The HF-download branch must NOT have fired.
|
|
expect(records.some((r) => r.recoveryHint === 'hf-endpoint-unreachable')).toBe(false);
|
|
|
|
cap.restore();
|
|
});
|
|
|
|
it('does NOT route unrelated errors through the local-embedding branch', async () => {
|
|
runFullAnalysisMock.mockRejectedValue(
|
|
new Error('Some unexpected failure unrelated to embeddings'),
|
|
);
|
|
|
|
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 records = cap.records();
|
|
expect(records.some((r) => r.recoveryHint === 'local-embedding-unsupported')).toBe(false);
|
|
|
|
cap.restore();
|
|
});
|
|
});
|