import { afterEach, describe, expect, it, vi } from 'vitest'; import { execFileSync } from 'child_process'; import { _setOsPageSizeForTests, 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('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(() => { _setOsPageSizeForTests(undefined); 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); _setOsPageSizeForTests(undefined); getOsPageSize(); expect(execFileSyncSpy).toHaveBeenCalledTimes(2); }); });