GitNexus/gitnexus/test/unit/lbug-config-pagesize.test.ts
VL 117587d543
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(cli): actionable diagnostics for non-4K page-size buffer manager failures (#2424)
2026-07-10 19:12:53 +01:00

162 lines
6.3 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
import { execFileSync } from 'child_process';
import {
_resetOsPageSizeCacheForTest,
getOsPageSize,
isLbugPageSizeFrameError,
isPageSizeAwareLadybug,
} from '../../src/core/lbug/lbug-config.js';
// Pass-through spy on the bare 'child_process' specifier (lbug-config.ts
// imports execFileSync from 'child_process', not 'node:child_process').
// Real behavior is preserved by default, so the live-probe test below still
// exercises the host getconf — including the real 16 KiB pages on the
// macos-arm64 CI matrix — while failure-path tests override single calls via
// mockImplementationOnce. Recipe: sibling-clone-drift.test.ts.
vi.mock('child_process', async () => {
const actual = await vi.importActual<typeof import('child_process')>('child_process');
return { ...actual, execFileSync: vi.fn(actual.execFileSync) };
});
const execFileSyncSpy = vi.mocked(execFileSync);
// getOsPageSize short-circuits before any exec on win32, so every
// exec-asserting test below is POSIX-only (skipIf pairs, no if-branching).
const onWindows = process.platform === 'win32';
// ─── #1231: non-4K page-size frame-release matcher ──────────────────────────
describe('isLbugPageSizeFrameError', () => {
it.each([
[
'exact Raspberry Pi 5 failure (issue #1231)',
'Buffer manager exception: Releasing physical memory associated with a frame failed with error code -1: Invalid argument.',
],
[
'wrapped by the node-COPY error path',
'COPY failed for File: Buffer manager exception: Releasing physical memory associated with a frame failed with error code -1: Invalid argument.',
],
[
'0.18.0 residual guard',
'Buffer manager exception: Unsupported page size combination: frame size 4096, discard granule size 65536, frame group size 16384.',
],
])('matches %s', (_label, msg) => {
expect(isLbugPageSizeFrameError(msg)).toBe(true);
expect(isLbugPageSizeFrameError(new Error(msg))).toBe(true);
});
it.each([
[
'buffer pool exhaustion (a sizing problem, not page size)',
'Buffer manager exception: Unable to allocate memory! The buffer pool is full and no memory could be freed!',
],
['8TB mmap failure (#785)', 'Buffer manager exception: Mmap for size 8796093022208 failed.'],
['WAL corruption', 'Runtime exception: Corrupted wal file. Read out invalid WAL record type.'],
['lock contention', 'Could not set lock on file : /path/to/db'],
['generic', 'Query failed'],
])('does NOT match %s', (_label, msg) => {
expect(isLbugPageSizeFrameError(msg)).toBe(false);
});
it('handles non-string input', () => {
expect(isLbugPageSizeFrameError(undefined)).toBe(false);
expect(isLbugPageSizeFrameError(null)).toBe(false);
expect(isLbugPageSizeFrameError(42)).toBe(false);
});
});
// ─── #1231: page-size-aware LadybugDB version gate ──────────────────────────
describe('isPageSizeAwareLadybug', () => {
it.each([
['0.18.0', true],
['0.18.0-dev.20260708', true],
['0.19.2', true],
['1.0.0', true],
['0.17.1', false],
['0.16.0', false],
['0.15.4', false],
])('%s -> %s', (version, expected) => {
expect(isPageSizeAwareLadybug(version)).toBe(expected);
});
it('returns false for unknown/unparseable versions (err on showing the upgrade hint)', () => {
expect(isPageSizeAwareLadybug(undefined)).toBe(false);
expect(isPageSizeAwareLadybug('')).toBe(false);
expect(isPageSizeAwareLadybug('unknown')).toBe(false);
expect(isPageSizeAwareLadybug('v0.18.0')).toBe(false);
});
});
// ─── #1231: OS page-size probe ───────────────────────────────────────────────
describe('getOsPageSize', () => {
afterEach(() => {
_resetOsPageSizeCacheForTest();
execFileSyncSpy.mockClear();
});
it.skipIf(onWindows)('returns a positive power-of-two page size on POSIX platforms', () => {
const pageSize = getOsPageSize();
expect(pageSize).toBeDefined();
expect(Number.isInteger(pageSize)).toBe(true);
// Every real page size is a power of two (4K, 16K, 64K, ...). log2 of a
// power of two is an integer; `?? 0` narrows without a cast or branch and
// Math.log2(0) is -Infinity, which fails isInteger.
expect(Number.isInteger(Math.log2(pageSize ?? 0))).toBe(true);
});
it.skipIf(!onWindows)('returns undefined on Windows without forking', () => {
expect(getOsPageSize()).toBeUndefined();
expect(execFileSyncSpy).not.toHaveBeenCalled();
});
it.skipIf(onWindows)('returns undefined when the probe cannot exec', () => {
execFileSyncSpy.mockImplementationOnce(() => {
throw new Error('spawnSync getconf ENOENT');
});
expect(getOsPageSize()).toBeUndefined();
});
it.skipIf(onWindows)('returns undefined on non-numeric probe output', () => {
execFileSyncSpy.mockImplementationOnce(() => 'unlimited');
expect(getOsPageSize()).toBeUndefined();
});
it.skipIf(onWindows)('returns undefined on empty probe output', () => {
execFileSyncSpy.mockImplementationOnce(() => '');
expect(getOsPageSize()).toBeUndefined();
});
it.skipIf(onWindows)('execs getconf with a SIGKILL-hardened 2s timeout', () => {
getOsPageSize();
expect(execFileSyncSpy).toHaveBeenCalledWith(
'getconf',
['PAGE_SIZE'],
expect.objectContaining({ timeout: 2000, killSignal: 'SIGKILL' }),
);
});
it.skipIf(onWindows)('returns undefined when the probe times out', () => {
// execFileSync's timeout kill surfaces as a throw with `signal` set and
// `status` null — the fail-safe must hold for the kill path too.
execFileSyncSpy.mockImplementationOnce(() => {
const err = new Error('spawnSync getconf ETIMEDOUT') as Error & {
signal: string;
status: null;
};
err.signal = 'SIGKILL';
err.status = null;
throw err;
});
expect(getOsPageSize()).toBeUndefined();
});
it.skipIf(onWindows)('probes at most once per process (cached)', () => {
expect(getOsPageSize()).toBe(getOsPageSize());
expect(execFileSyncSpy).toHaveBeenCalledTimes(1);
_resetOsPageSizeCacheForTest();
getOsPageSize();
expect(execFileSyncSpy).toHaveBeenCalledTimes(2);
});
});