mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-08 22:22:52 +00:00
* docs(plans): add glibc-windows-fts-diagnostics plan Implementation plan for #2672 (glibc-too-old native-load misdiagnosis) and #2669 (Windows FTS prerequisites + Git Bash zero-install workaround). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(cli): stop prescribing a reinstall when the host glibc is too old (#2672) The LadybugDB prebuilt binary requires GLIBC_2.34 (dlopen/pthread_* at 2.34, fstat64/lstat at 2.33). On an older host the loader reports version `GLIBC_2.34' not found (required by .../lbugjs.node) and checkLbugNative answered with "truncated file, ABI mismatch, or wrong-platform binary" plus instructions to re-run install.js. That advice is actively wrong for this class: every download ships the same prebuilt binary, so the reinstall fails identically and the user loops. Add glibcTooOldMessage: match a GLIBC_<version> token on a "not found" line, report the highest required version (compared numerically, so 2.9 < 2.34) alongside this host's glibc from process.report, state that reinstalling will NOT help, and point at the real options. The branch sits on the arm where the probe actually ran and failed, so an unrunnable probe still fails open (#2441). The glibc read is local rather than analyzer-identity's detectLibcVariant: native-check is the dependency-light startup gate and must not statically pull in a module the CLI reaches through a dynamic import. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(lbug): name the Git Bash zero-install fix for Windows FTS load failures (#2669) The Windows error-126 remedy already refuses to prescribe a reinstall and names the VC++ redistributable and the OpenSSL 3 DLLs, but not where those DLLs already exist on the machine. #2669's reporter had the redistributable installed and still failed: the same command failed in PowerShell and succeeded in Git Bash, because Git for Windows puts libssl-3-x64.dll and libcrypto-3-x64.dll on PATH via C:\Program Files\Git\mingw64\bin. Add that hint to the Windows-126 and structural missing-dependency remedies through one shared const, following the VC_REDIST_INSTALL_HINT anti-drift pattern (#2383 F5). Placing it in the builders rather than at a call site is load-bearing: markUnavailable caches the whole diagnosis (#2383 F3) and ftsDegradedWarning replays that cached remedy, so a call-site fix would miss the MCP query and /api/search surfaces. The hint is a fixed system path, never a user-profile one — remedy text is not path-redacted, and fts-degraded-warning.test.ts asserts no C:\Users\ path ever reaches a user. Both touched tests now assert that property directly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(readme): document the Linux glibc floor and Windows FTS prerequisites (#2672, #2669) Requirements listed only Node and git, so neither runtime prerequisite that these two issues turn on was discoverable before hitting the failure. - Linux: the LadybugDB prebuilt binary needs glibc 2.34+; name the distro versions that clear it and state plainly that reinstalling does not help. - Windows: full-text search needs the VC++ 2015-2022 x64 redistributable AND OpenSSL 3 on PATH. The redistributable alone is not sufficient (#2669's reporter had it), and Git for Windows already ships the OpenSSL DLLs, so running from Git Bash or prepending mingw64\bin is a zero-install fix. Without them analyze still succeeds but the index carries no search tables. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: drop the plan document from version control docs/* is gitignored; the plan was force-added so it would travel with the work. It is working material, not a repository artifact — the code, tests and README carry the reasoning that matters. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(cli): stop doctor reporting a present-but-unloadable binary as missing (#2672) doctor printed "✗ lbugjs.node missing" for every failed native check — including the case this PR is about, where the binary is right there and merely fails to load because the host glibc is too old. It then wrote the real detail to stderr directly beneath, so the two lines contradicted each other and the headline sent users to reinstall a file they already had. It said the same for a truncated download and for an entirely absent @ladybugdb/core package. checkLbugNative already knows which of the three it found, so record it: a `kind` discriminator ('package_missing' | 'binary_missing' | 'load_failed') set at each failure return. doctor renders it through a new exported `nativeStatusLine`, following the existing pageSizeDoctorLines/poolSizeDoctorLine pure-helper pattern — which also makes the line testable, where before it had no coverage at all. An unrecognized or absent kind keeps the conservative "missing". Deriving this in doctor with a second existsSync would have re-stat'd a file the check had already inspected, and could disagree with what it actually observed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
358 lines
16 KiB
TypeScript
358 lines
16 KiB
TypeScript
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
classifyBinaryHeader,
|
|
classifyExtensionLoadError,
|
|
diagnoseExtensionLoad,
|
|
extractExtensionPath,
|
|
type ExtensionLoadErrorKind,
|
|
} from '../../src/core/lbug/extension-load-error.js';
|
|
|
|
// Minimal well-formed binary headers per format, for the structural check.
|
|
function buildELF(eMachine: number): Buffer {
|
|
const b = Buffer.alloc(64);
|
|
b[0] = 0x7f;
|
|
b[1] = 0x45;
|
|
b[2] = 0x4c;
|
|
b[3] = 0x46; // 0x7F E L F
|
|
b[4] = 2; // 64-bit
|
|
b[5] = 1; // little-endian
|
|
b.writeUInt16LE(eMachine, 18);
|
|
return b;
|
|
}
|
|
function buildPE(machine: number): Buffer {
|
|
const peOff = 0x80;
|
|
const b = Buffer.alloc(peOff + 8);
|
|
b[0] = 0x4d;
|
|
b[1] = 0x5a; // MZ
|
|
b.writeUInt32LE(peOff, 0x3c);
|
|
b[peOff] = 0x50;
|
|
b[peOff + 1] = 0x45; // PE\0\0
|
|
b.writeUInt16LE(machine, peOff + 4);
|
|
return b;
|
|
}
|
|
function buildMachO(cpuType: number): Buffer {
|
|
const b = Buffer.alloc(32);
|
|
b.writeUInt32LE(0xfeedfacf, 0); // MH_MAGIC_64 (little-endian file)
|
|
b.writeUInt32LE(cpuType, 4);
|
|
return b;
|
|
}
|
|
function buildHostValidBinary(): Buffer {
|
|
const arm = process.arch === 'arm64';
|
|
if (process.platform === 'win32') return buildPE(arm ? 0xaa64 : 0x8664);
|
|
if (process.platform === 'linux') return buildELF(arm ? 0xb7 : 0x3e);
|
|
if (process.platform === 'darwin') return buildMachO(arm ? 0x0100000c : 0x01000007);
|
|
return Buffer.alloc(64); // unknown host: classifyBinaryHeader returns 'valid' anyway
|
|
}
|
|
// Valid MZ, but e_lfanew points far past the bytes we read → header unprovable.
|
|
function buildPEBeyondWindow(): Buffer {
|
|
const b = Buffer.alloc(128); // > 0x40 so the MZ check passes
|
|
b[0] = 0x4d;
|
|
b[1] = 0x5a; // MZ
|
|
b.writeUInt32LE(4100, 0x3c); // e_lfanew far beyond the 128-byte buffer
|
|
return b;
|
|
}
|
|
// Valid MZ and an in-window e_lfanew, but no 'PE\0\0' signature there → corrupt.
|
|
function buildPEGarbageSignature(): Buffer {
|
|
const peOff = 0x80;
|
|
const b = Buffer.alloc(peOff + 8);
|
|
b[0] = 0x4d;
|
|
b[1] = 0x5a; // MZ
|
|
b.writeUInt32LE(peOff, 0x3c); // e_lfanew within the buffer, but bytes there stay 0x00
|
|
return b;
|
|
}
|
|
|
|
/**
|
|
* U1 (#2374): the string classifier. The precise en/zh 126 tail gets the definite
|
|
* runtime remedy; other Windows tails (127/5/1114) and the bare wrapper match only
|
|
* lbug's language-independent `Failed to load library` wrapper, so they fall to the
|
|
* HEDGED `missing_dependency` remedy (never a wrong confident instruction); an
|
|
* English corrupt/wrong-arch tail routes to `corrupt_file` first. The structural
|
|
* layer (below) refines corrupt-vs-valid from the binary itself, in any language.
|
|
*/
|
|
describe('classifyExtensionLoadError', () => {
|
|
const kindCases: ReadonlyArray<readonly [string, string, ExtensionLoadErrorKind]> = [
|
|
[
|
|
'Windows 126 (Chinese)',
|
|
'IO exception: Failed to load library: C:\\Users\\someone/.lbdb/extension/0.18.0/win_amd64/fts/libfts.lbug_extension which is needed by extension: fts. Error: 找不到指定的模块。',
|
|
'missing_dependency',
|
|
],
|
|
[
|
|
'Windows 126 (English)',
|
|
'Failed to load library: libfts.lbug_extension which is needed by extension: fts. Error: The specified module could not be found.',
|
|
'missing_dependency',
|
|
],
|
|
[
|
|
'Linux missing shared object',
|
|
'IO exception: Failed to load library: libfts.lbug_extension which is needed by extension: fts. Error: libcrypto.so.3: cannot open shared object file: No such file or directory',
|
|
'missing_dependency',
|
|
],
|
|
[
|
|
'macOS image not found',
|
|
'Failed to load library: Library not loaded: @rpath/libssl.3.dylib ... Reason: image not found',
|
|
'missing_dependency',
|
|
],
|
|
[
|
|
'missing file (never installed)',
|
|
'Extension "fts" is an official extension and has not been installed.',
|
|
'missing_file',
|
|
],
|
|
['corrupt: invalid ELF header', 'Binder exception: invalid ELF header', 'corrupt_file'],
|
|
['corrupt: file too short', 'IO exception: file too short', 'corrupt_file'],
|
|
[
|
|
'Windows 193 (not a valid Win32 application) → corrupt, not missing_dependency',
|
|
'Failed to load library: libfts.lbug_extension which is needed by extension: fts. Error: %1 is not a valid Win32 application.',
|
|
'corrupt_file',
|
|
],
|
|
[
|
|
'German 126 (localized) via the language-independent wrapper',
|
|
'Failed to load library: C:\\Users\\x\\.lbdb\\extension\\0.18.0\\win_amd64\\fts\\libfts.lbug_extension which is needed by extension: fts. Error: Das angegebene Modul wurde nicht gefunden.',
|
|
'missing_dependency',
|
|
],
|
|
[
|
|
'German 193 (corrupt, localized) → hedged (corruption not detectable in German)',
|
|
'Failed to load library: C:\\Users\\x\\.lbdb\\extension\\0.18.0\\win_amd64\\fts\\libfts.lbug_extension which is needed by extension: fts. Error: Die Datei ist keine zulässige Win32-Anwendung.',
|
|
'missing_dependency',
|
|
],
|
|
[
|
|
'Windows 127 (wrong symbol) → hedged missing_dependency via the wrapper',
|
|
'Failed to load library: libfts.lbug_extension which is needed by extension: fts. Error: The specified procedure could not be found.',
|
|
'missing_dependency',
|
|
],
|
|
[
|
|
'Windows 5 (access denied / AV lock) → hedged missing_dependency via the wrapper',
|
|
'Failed to load library: libfts.lbug_extension which is needed by extension: fts. Error: Access is denied.',
|
|
'missing_dependency',
|
|
],
|
|
[
|
|
'bare wrapper, no OS-error tail → hedged missing_dependency',
|
|
'Failed to load library: libfts.lbug_extension which is needed by extension: fts.',
|
|
'missing_dependency',
|
|
],
|
|
['unrelated/garbage (no wrapper) → unknown', 'something else entirely went wrong', 'unknown'],
|
|
['empty → unknown', '', 'unknown'],
|
|
];
|
|
|
|
it.each(kindCases)('classifies %s', (_name, reason, expectedKind) => {
|
|
expect(classifyExtensionLoadError(reason)).toMatchObject({ kind: expectedKind });
|
|
});
|
|
|
|
it('nullish reason does not throw and is unknown', () => {
|
|
expect(classifyExtensionLoadError(undefined)).toMatchObject({ kind: 'unknown' });
|
|
expect(classifyExtensionLoadError(null)).toMatchObject({ kind: 'unknown' });
|
|
});
|
|
|
|
it('Windows missing-dependency remedy leads with MSVC redist, names OpenSSL, and says reinstall will not help', () => {
|
|
const { remedy } = classifyExtensionLoadError(
|
|
'needed by extension: fts. Error: The specified module could not be found.',
|
|
);
|
|
expect(remedy).toMatch(/Visual C\+\+/);
|
|
expect(remedy).toMatch(/vc_redist\.x64\.exe/);
|
|
expect(remedy).toMatch(/OpenSSL 3/);
|
|
expect(remedy).toMatch(/will NOT help/);
|
|
// Must not resurrect the old, wrong "retry the network install" instruction.
|
|
expect(remedy).not.toMatch(/Retry with network access/i);
|
|
// #2669: the zero-install path — Git for Windows already ships those DLLs.
|
|
expect(remedy).toMatch(/Git Bash/);
|
|
expect(remedy).toMatch(/mingw64/);
|
|
// Never a user-profile path: remedy text reaches /api/search unredacted.
|
|
expect(remedy).not.toMatch(/C:\\Users\\/);
|
|
});
|
|
|
|
it('hedged fallback remedy points at the OS error and offers both branches (language-independent)', () => {
|
|
// A non-English localized Windows tail we do not enumerate — matched only via
|
|
// lbug's language-independent "Failed to load library" wrapper.
|
|
const { kind, remedy } = classifyExtensionLoadError(
|
|
'Failed to load library: libfts.lbug_extension which is needed by extension: fts. Error: <localized OS message>',
|
|
);
|
|
expect(kind).toBe('missing_dependency');
|
|
expect(remedy).toMatch(/"Error:"/); // tells the user to read their own localized error
|
|
expect(remedy).toMatch(/repair-fts/); // corrupt branch
|
|
expect(remedy).toMatch(/Visual C\+\+|OpenSSL/); // missing-runtime branch
|
|
// Hedged, distinct from the definite 126 remedy — "usually will not help".
|
|
expect(remedy).toMatch(/usually will not help/);
|
|
});
|
|
|
|
it('POSIX missing-dependency remedy points at the named library, not a reinstall', () => {
|
|
const { remedy } = classifyExtensionLoadError('libcrypto.so.3: cannot open shared object file');
|
|
expect(remedy).toMatch(/shared library/i);
|
|
expect(remedy).toMatch(/will NOT help/i);
|
|
});
|
|
|
|
it('missing-file remedy routes to the network install', () => {
|
|
const { remedy } = classifyExtensionLoadError('has not been installed');
|
|
expect(remedy).toMatch(/--repair-fts|GITNEXUS_LBUG_EXTENSION_INSTALL=auto/);
|
|
});
|
|
});
|
|
|
|
/**
|
|
* The language-independent structural layer: it decides corrupt-vs-valid from the
|
|
* binary's own header (PE/ELF/Mach-O magic + architecture), never from a localized
|
|
* OS-error string.
|
|
*/
|
|
describe('classifyBinaryHeader', () => {
|
|
const cases: ReadonlyArray<
|
|
readonly [string, Buffer, NodeJS.Platform, string, 'valid' | 'corrupt' | 'indeterminate']
|
|
> = [
|
|
['linux x64 valid ELF', buildELF(0x3e), 'linux', 'x64', 'valid'],
|
|
['linux arm64 valid ELF', buildELF(0xb7), 'linux', 'arm64', 'valid'],
|
|
['linux: arm64 ELF on x64 host → corrupt', buildELF(0xb7), 'linux', 'x64', 'corrupt'],
|
|
[
|
|
'linux: non-ELF bytes → corrupt',
|
|
Buffer.from('this is definitely not an ELF binary'),
|
|
'linux',
|
|
'x64',
|
|
'corrupt',
|
|
],
|
|
['win x64 valid PE', buildPE(0x8664), 'win32', 'x64', 'valid'],
|
|
['win: arm64 PE on x64 host → corrupt', buildPE(0xaa64), 'win32', 'x64', 'corrupt'],
|
|
['win: ELF file on a Windows host → corrupt', buildELF(0x3e), 'win32', 'x64', 'corrupt'],
|
|
['darwin x64 valid Mach-O', buildMachO(0x01000007), 'darwin', 'x64', 'valid'],
|
|
['darwin arm64 valid Mach-O', buildMachO(0x0100000c), 'darwin', 'arm64', 'valid'],
|
|
[
|
|
'darwin: x86_64 Mach-O on arm64 host → corrupt',
|
|
buildMachO(0x01000007),
|
|
'darwin',
|
|
'arm64',
|
|
'corrupt',
|
|
],
|
|
[
|
|
'unknown host → valid (never claim corrupt)',
|
|
Buffer.from('whatever'),
|
|
'sunos' as NodeJS.Platform,
|
|
'x64',
|
|
'valid',
|
|
],
|
|
// #2383 F1-secondary: a valid PE whose header sits past the read window is not
|
|
// provably corrupt — return indeterminate so the caller defers to the loader.
|
|
[
|
|
'win: PE header beyond read window → indeterminate',
|
|
buildPEBeyondWindow(),
|
|
'win32',
|
|
'x64',
|
|
'indeterminate',
|
|
],
|
|
// Arch we don't map on a known platform: never claim corrupt (documents KTD5).
|
|
['linux: valid ELF, unmapped arch → valid', buildELF(0x3e), 'linux', 'mips', 'valid'],
|
|
// Valid MZ but garbage where PE\0\0 should be, within the window → genuinely corrupt.
|
|
[
|
|
'win: valid MZ but no PE signature → corrupt',
|
|
buildPEGarbageSignature(),
|
|
'win32',
|
|
'x64',
|
|
'corrupt',
|
|
],
|
|
];
|
|
|
|
it.each(cases)('%s', (_name, buf, platform, arch, expected) => {
|
|
expect(classifyBinaryHeader(buf, buf.length, platform, arch)).toBe(expected);
|
|
});
|
|
});
|
|
|
|
describe('extractExtensionPath', () => {
|
|
const cases: ReadonlyArray<readonly [string, string, string | null]> = [
|
|
[
|
|
'real lbug wrapper (Windows, spaces + mixed separators)',
|
|
'Failed to load library: C:\\Users\\a b\\.lbdb\\extension\\0.18.0\\win_amd64\\fts\\libfts.lbug_extension which is needed by extension: fts. Error: x',
|
|
'C:\\Users\\a b\\.lbdb\\extension\\0.18.0\\win_amd64\\fts\\libfts.lbug_extension',
|
|
],
|
|
[
|
|
'quoted variant',
|
|
"Failed to load library '/home/u/.lbdb/extension/0.18.0/linux_amd64/fts/libfts.lbug_extension': invalid ELF header",
|
|
'/home/u/.lbdb/extension/0.18.0/linux_amd64/fts/libfts.lbug_extension',
|
|
],
|
|
['no path (never installed)', 'Extension "fts" ... has not been installed.', null],
|
|
['no .lbug_extension token', 'some unrelated error', null],
|
|
];
|
|
|
|
it.each(cases)('%s', (_name, reason, expected) => {
|
|
expect(extractExtensionPath(reason)).toBe(expected);
|
|
});
|
|
});
|
|
|
|
describe('diagnoseExtensionLoad (structural, language-independent)', () => {
|
|
it('a valid host binary that still failed to load → missing_dependency', () => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'ext-diag-valid-'));
|
|
const file = join(dir, 'libfts.lbug_extension');
|
|
writeFileSync(file, buildHostValidBinary());
|
|
try {
|
|
// A localized tail we do NOT enumerate — structural check decides it anyway.
|
|
const reason = `Failed to load library: ${file} which is needed by extension: fts. Error: <localized>`;
|
|
expect(diagnoseExtensionLoad(reason)).toMatchObject({ kind: 'missing_dependency' });
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('a header-valid file the loader calls "file too short" → corrupt_file, not missing_dependency (#2383 F1)', () => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'ext-diag-trunc-'));
|
|
const file = join(dir, 'libfts.lbug_extension');
|
|
// Intact host header, but the loader reports a body-truncated download.
|
|
writeFileSync(file, buildHostValidBinary());
|
|
try {
|
|
const reason = `Failed to load library: ${file} which is needed by extension: fts. Error: file too short`;
|
|
expect(diagnoseExtensionLoad(reason)).toMatchObject({ kind: 'corrupt_file' });
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('a header-valid file the loader calls "not a valid Win32 application" (error 193) → corrupt_file', () => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'ext-diag-win193-'));
|
|
const file = join(dir, 'libfts.lbug_extension');
|
|
writeFileSync(file, buildHostValidBinary());
|
|
try {
|
|
const reason = `Failed to load library: ${file} which is needed by extension: fts. Error: %1 is not a valid Win32 application.`;
|
|
expect(diagnoseExtensionLoad(reason)).toMatchObject({ kind: 'corrupt_file' });
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('a valid file with an unrecognized loader tail → structural remedy carrying the shared VC++ hint', () => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'ext-diag-struct-'));
|
|
const file = join(dir, 'libfts.lbug_extension');
|
|
writeFileSync(file, buildHostValidBinary());
|
|
try {
|
|
// Wrapper present (so the path extracts) with a tail that maps to neither
|
|
// corrupt_file nor missing_dependency — exercises the STRUCTURAL remedy branch,
|
|
// and asserts it carries the same vc_redist URL as the Windows-126 remedy (#2383 F5).
|
|
const reason = `Failed to load library: ${file}. has not been installed`;
|
|
const { kind, remedy } = diagnoseExtensionLoad(reason);
|
|
expect(kind).toBe('missing_dependency');
|
|
expect(remedy).toMatch(/vc_redist\.x64\.exe/);
|
|
// #2669: the structural remedy carries the same zero-install hint.
|
|
expect(remedy).toMatch(/Git Bash/);
|
|
expect(remedy).not.toMatch(/C:\\Users\\/);
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('a malformed host binary → corrupt_file regardless of the (localized) error text', () => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'ext-diag-corrupt-'));
|
|
const file = join(dir, 'libfts.lbug_extension');
|
|
writeFileSync(file, Buffer.from('not a shared library'));
|
|
try {
|
|
const reason = `Failed to load library: ${file} which is needed by extension: fts. Error: Die Datei ist beschädigt.`;
|
|
expect(diagnoseExtensionLoad(reason)).toMatchObject({ kind: 'corrupt_file' });
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('no readable file → falls back to the string classifier', () => {
|
|
// No path in the reason (never installed) → string classifier → missing_file.
|
|
expect(
|
|
diagnoseExtensionLoad('Extension "fts" is an official extension and has not been installed.'),
|
|
).toMatchObject({ kind: 'missing_file' });
|
|
// Path present but absent on disk → defer to the string classifier (hedged here).
|
|
expect(
|
|
diagnoseExtensionLoad(
|
|
'Failed to load library: /nope/libfts.lbug_extension which is needed by extension: fts. Error: xyz',
|
|
),
|
|
).toMatchObject({ kind: 'missing_dependency' });
|
|
});
|
|
});
|