mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-08 22:22:52 +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
265 lines
9.8 KiB
TypeScript
265 lines
9.8 KiB
TypeScript
/**
|
|
* Tests for non-4K page-size buffer-manager error handling in `analyzeCommand`
|
|
* (#1231).
|
|
*
|
|
* On kernels with 16 KiB pages (Raspberry Pi 5, Asahi Linux) a
|
|
* @ladybugdb/core < 0.18.0 buffer manager fails to release evicted frames
|
|
* (madvise EINVAL) and analyze aborts mid-COPY with a raw native message.
|
|
* The CLI must catch that shape before the generic error path and render an
|
|
* actionable message: what the OS page size is, and whether the fix is
|
|
* upgrading (@ladybugdb/core < 0.18.0) or reporting (>= 0.18.0).
|
|
*
|
|
* Mirrors the mock shape of analyze-wal-error.test.ts.
|
|
*/
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import type { RuntimeFingerprint } from '../../src/core/platform/capabilities.js';
|
|
|
|
const runFullAnalysisMock = vi.fn();
|
|
|
|
vi.mock('../../src/core/run-analyze.js', () => ({
|
|
runFullAnalysis: runFullAnalysisMock,
|
|
}));
|
|
|
|
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, which in turn imports
|
|
// from gitnexus-shared (not linked in dev). Mock the module to break the chain.
|
|
vi.mock('../../src/core/embeddings/hf-env.js', () => ({
|
|
isHfDownloadFailure: vi.fn(() => false),
|
|
}));
|
|
|
|
// Pin the fingerprint so assertions do not depend on the dev environment's
|
|
// installed @ladybugdb/core.
|
|
const fingerprintMock = vi.fn(
|
|
(): RuntimeFingerprint => ({
|
|
platform: process.platform,
|
|
arch: process.arch,
|
|
node: process.version,
|
|
gitnexus: 'test',
|
|
ladybugdb: '0.17.1',
|
|
}),
|
|
);
|
|
vi.mock('../../src/core/platform/capabilities.js', async (importOriginal) => ({
|
|
...(await importOriginal<typeof import('../../src/core/platform/capabilities.js')>()),
|
|
getRuntimeFingerprint: fingerprintMock,
|
|
}));
|
|
|
|
// Pin the OS page size so the "Detected OS page size" line is deterministic
|
|
// (on 4 KiB dev/CI hosts the real probe would render nothing). The spread is
|
|
// load-bearing: analyze.ts also takes isWalCorruptionError /
|
|
// isLbugCheckpointIoError / isLbugPageSizeFrameError from this module, and
|
|
// the WAL/checkpoint branches run BEFORE the page-size branch in the same
|
|
// catch — a non-spread mock would stub them and reroute the test errors.
|
|
const getOsPageSizeMock = vi.fn((): number | undefined => 16384);
|
|
vi.mock('../../src/core/lbug/lbug-config.js', async (importOriginal) => ({
|
|
...(await importOriginal<typeof import('../../src/core/lbug/lbug-config.js')>()),
|
|
getOsPageSize: getOsPageSizeMock,
|
|
}));
|
|
|
|
const PI5_COPY_ERROR =
|
|
'COPY failed for File: Buffer manager exception: Releasing physical memory ' +
|
|
'associated with a frame failed with error code -1: Invalid argument.';
|
|
|
|
describe('analyzeCommand non-4K page-size error handling (#1231)', () => {
|
|
// 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();
|
|
getOsPageSizeMock.mockReset();
|
|
getOsPageSizeMock.mockReturnValue(16384);
|
|
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('recommends upgrading when @ladybugdb/core < 0.18.0 hits the frame-release error', async () => {
|
|
fingerprintMock.mockReturnValue({
|
|
platform: process.platform,
|
|
arch: process.arch,
|
|
node: process.version,
|
|
gitnexus: 'test',
|
|
ladybugdb: '0.17.1',
|
|
});
|
|
runFullAnalysisMock.mockRejectedValue(new Error(PI5_COPY_ERROR));
|
|
|
|
const { _captureLogger } = await import('../../src/core/logger.js');
|
|
const cap = _captureLogger();
|
|
const { analyzeCommand } = await import('../../src/cli/analyze.js');
|
|
|
|
await analyzeCommand(undefined, {});
|
|
|
|
expect(process.exitCode).toBe(1);
|
|
const records = cap.records();
|
|
const hint = records.find(
|
|
(r) => typeof r.msg === 'string' && r.msg.includes('failed to release frame memory'),
|
|
);
|
|
expect(hint).toBeDefined();
|
|
expect(hint?.msg).toContain('0.18.0');
|
|
expect(hint?.msg).toContain('npm install -g gitnexus@latest');
|
|
// The raw native error text is embedded so users can attach it to reports
|
|
// (#2424 review P2) — and the page-size line renders the mocked probe.
|
|
expect(hint?.msg).toContain(PI5_COPY_ERROR);
|
|
expect(hint?.msg).toContain('Detected OS page size: 16384 bytes');
|
|
// Structured fields flow to log aggregation (mirror analyze-wipe-error).
|
|
expect(hint).toMatchObject({
|
|
recoveryHint: 'lbug-page-size',
|
|
pageSize: 16384,
|
|
ladybugVersion: '0.17.1',
|
|
});
|
|
// Raw stack trace must NOT appear via cliError
|
|
const stackRecord = records.find(
|
|
(r) => typeof r.msg === 'string' && r.msg.includes('at analyzeCommand'),
|
|
);
|
|
expect(stackRecord).toBeUndefined();
|
|
|
|
cap.restore();
|
|
});
|
|
|
|
it('asks for a bug report when @ladybugdb/core >= 0.18.0 still hits the error', async () => {
|
|
fingerprintMock.mockReturnValue({
|
|
platform: process.platform,
|
|
arch: process.arch,
|
|
node: process.version,
|
|
gitnexus: 'test',
|
|
ladybugdb: '0.18.0',
|
|
});
|
|
runFullAnalysisMock.mockRejectedValue(new Error(PI5_COPY_ERROR));
|
|
|
|
const { _captureLogger } = await import('../../src/core/logger.js');
|
|
const cap = _captureLogger();
|
|
const { analyzeCommand } = await import('../../src/cli/analyze.js');
|
|
|
|
await analyzeCommand(undefined, {});
|
|
|
|
expect(process.exitCode).toBe(1);
|
|
const records = cap.records();
|
|
const hint = records.find(
|
|
(r) => typeof r.msg === 'string' && r.msg.includes('failed to release frame memory'),
|
|
);
|
|
expect(hint).toBeDefined();
|
|
expect(hint?.msg).toContain('issues/1231');
|
|
expect(hint?.msg).not.toContain('npm install -g gitnexus@latest');
|
|
// The report-a-bug path asks for "the full error message above" — the
|
|
// embedded raw text is what makes that instruction fulfillable.
|
|
expect(hint?.msg).toContain(PI5_COPY_ERROR);
|
|
expect(hint).toMatchObject({
|
|
recoveryHint: 'lbug-page-size',
|
|
pageSize: 16384,
|
|
ladybugVersion: '0.18.0',
|
|
});
|
|
|
|
cap.restore();
|
|
});
|
|
|
|
it.each([
|
|
['a 4 KiB host', 4096],
|
|
['an unavailable probe', undefined],
|
|
])('omits the page-size line on %s', async (_label, probed) => {
|
|
getOsPageSizeMock.mockReturnValue(probed);
|
|
runFullAnalysisMock.mockRejectedValue(new Error(PI5_COPY_ERROR));
|
|
|
|
const { _captureLogger } = await import('../../src/core/logger.js');
|
|
const cap = _captureLogger();
|
|
const { analyzeCommand } = await import('../../src/cli/analyze.js');
|
|
|
|
await analyzeCommand(undefined, {});
|
|
|
|
const hint = cap
|
|
.records()
|
|
.find((r) => typeof r.msg === 'string' && r.msg.includes('failed to release frame memory'));
|
|
expect(hint).toBeDefined();
|
|
expect(hint?.msg).not.toContain('Detected OS page size');
|
|
expect(hint?.msg).toContain(PI5_COPY_ERROR);
|
|
|
|
cap.restore();
|
|
});
|
|
|
|
it('names the unknown version instead of asserting facts about it', async () => {
|
|
fingerprintMock.mockReturnValue({
|
|
platform: process.platform,
|
|
arch: process.arch,
|
|
node: process.version,
|
|
gitnexus: 'test',
|
|
});
|
|
runFullAnalysisMock.mockRejectedValue(new Error(PI5_COPY_ERROR));
|
|
|
|
const { _captureLogger } = await import('../../src/core/logger.js');
|
|
const cap = _captureLogger();
|
|
const { analyzeCommand } = await import('../../src/cli/analyze.js');
|
|
|
|
await analyzeCommand(undefined, {});
|
|
|
|
expect(process.exitCode).toBe(1);
|
|
const hint = cap
|
|
.records()
|
|
.find((r) => typeof r.msg === 'string' && r.msg.includes('failed to release frame memory'));
|
|
expect(hint).toBeDefined();
|
|
// Unknown version must not read "(unknown) assumes 4 KiB pages" (#2424
|
|
// review R2) — name the unknown state, keep the upgrade instruction.
|
|
expect(hint?.msg).toContain('version is unknown');
|
|
expect(hint?.msg).not.toContain('(unknown) assumes');
|
|
expect(hint?.msg).toContain('npm install -g gitnexus@latest');
|
|
|
|
cap.restore();
|
|
});
|
|
|
|
it('does NOT route buffer-pool exhaustion through the page-size handler', async () => {
|
|
runFullAnalysisMock.mockRejectedValue(
|
|
new Error(
|
|
'COPY failed for File: Buffer manager exception: Unable to allocate memory! ' +
|
|
'The buffer pool is full and no memory could be freed!',
|
|
),
|
|
);
|
|
|
|
const { _captureLogger } = await import('../../src/core/logger.js');
|
|
const cap = _captureLogger();
|
|
const { analyzeCommand } = await import('../../src/cli/analyze.js');
|
|
|
|
await analyzeCommand(undefined, {});
|
|
|
|
expect(process.exitCode).toBe(1);
|
|
const records = cap.records();
|
|
expect(
|
|
records.some(
|
|
(r) => typeof r.msg === 'string' && r.msg.includes('failed to release frame memory'),
|
|
),
|
|
).toBe(false);
|
|
|
|
cap.restore();
|
|
});
|
|
});
|