mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-05 02:43:32 +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>
188 lines
8.1 KiB
TypeScript
188 lines
8.1 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import os from 'os';
|
|
import path from 'path';
|
|
import fs from 'fs/promises';
|
|
import { checkLbugNative, glibcTooOldMessage } from '../../src/core/lbug/native-check.js';
|
|
|
|
describe('checkLbugNative', () => {
|
|
it('returns ok:true when the real @ladybugdb/core binary is present', () => {
|
|
const result = checkLbugNative();
|
|
expect(result.ok).toBe(true);
|
|
expect(result.binaryPath).toBeDefined();
|
|
expect(result.message).toBeUndefined();
|
|
});
|
|
|
|
it('returns ok:false with repair instructions when lbugjs.node is missing', async () => {
|
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'lbug-check-'));
|
|
try {
|
|
await fs.writeFile(path.join(tmpDir, 'install.js'), '');
|
|
|
|
const result = checkLbugNative(tmpDir);
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.kind).toBe('binary_missing');
|
|
expect(result.message).toContain('missing');
|
|
expect(result.message).toContain('install.js');
|
|
expect(result.message).toContain('trustedDependencies');
|
|
expect(result.message).toContain('ignore-scripts');
|
|
expect(result.message).toContain('--allow-build=@ladybugdb/core');
|
|
expect(result.message).toContain('pnpm add -g --allow-build=@ladybugdb/core');
|
|
const allowBuildIdx = result.message!.indexOf('--allow-build=@ladybugdb/core');
|
|
const dlxIdx = result.message!.indexOf('dlx gitnexus');
|
|
expect(allowBuildIdx).toBeGreaterThanOrEqual(0);
|
|
expect(dlxIdx).toBeGreaterThan(allowBuildIdx);
|
|
} finally {
|
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('returns ok:false when lbugjs.node exists but is unloadable (zero-byte)', async () => {
|
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'lbug-check-'));
|
|
try {
|
|
await fs.writeFile(path.join(tmpDir, 'lbugjs.node'), Buffer.alloc(0));
|
|
|
|
const result = checkLbugNative(tmpDir);
|
|
|
|
expect(result.ok).toBe(false);
|
|
// Present but unloadable — doctor must not call this "missing" (#2672).
|
|
expect(result.kind).toBe('load_failed');
|
|
expect(result.message).toContain('failed to load');
|
|
expect(result.message).toContain('install.js');
|
|
} finally {
|
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('returns ok:false when lbugjs.node is truncated (loader crashes with a signal)', async () => {
|
|
// A partially written .node (valid header, missing pages) SIGBUSes dlopen — a
|
|
// signal, not a catchable throw. The out-of-process probe must observe the
|
|
// crash and report it, instead of the whole process dying with exit 135 (#2441).
|
|
const realPath = checkLbugNative().binaryPath;
|
|
expect(realPath).toBeDefined();
|
|
const truncated = (await fs.readFile(realPath!)).subarray(0, 300_000);
|
|
|
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'lbug-check-'));
|
|
try {
|
|
await fs.writeFile(path.join(tmpDir, 'install.js'), '');
|
|
await fs.writeFile(path.join(tmpDir, 'lbugjs.node'), truncated);
|
|
|
|
const result = checkLbugNative(tmpDir);
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.message).toContain('failed to load');
|
|
expect(result.message).toContain('install.js');
|
|
} finally {
|
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('does not prescribe a reinstall when the host glibc is too old (#2672)', async () => {
|
|
// The generic failure text ("truncated / ABI mismatch / re-run install.js")
|
|
// is actively wrong for this class: the reinstall re-downloads the identical
|
|
// prebuilt binary and fails identically. Guard the whole assembled message,
|
|
// not just the helper, so the branch stays wired into checkLbugNative.
|
|
const message = glibcTooOldMessage(
|
|
"Error: /lib64/libc.so.6: version `GLIBC_2.34' not found (required by " +
|
|
'/usr/lib/node_modules/gitnexus/node_modules/@ladybugdb/core/lbugjs.node)',
|
|
);
|
|
|
|
expect(message).toContain('glibc 2.34 or newer');
|
|
expect(message).toContain('will NOT help');
|
|
expect(message).not.toContain('install.js');
|
|
expect(message).not.toContain('trustedDependencies');
|
|
expect(message).not.toContain('--allow-build');
|
|
});
|
|
|
|
// POSIX-only: the fake "node" is a shebang script, which Windows cannot exec.
|
|
// The real Windows path has no glibc, so this class cannot occur there anyway.
|
|
it.skipIf(process.platform === 'win32')(
|
|
'checkLbugNative routes a glibc load failure to that message, not the reinstall text',
|
|
async () => {
|
|
// Proves the branch is WIRED, not merely present: the probe child is
|
|
// replaced by a script that emits the loader error the #2672 reporter saw.
|
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'lbug-check-glibc-'));
|
|
const originalExecPath = process.execPath;
|
|
try {
|
|
await fs.writeFile(path.join(tmpDir, 'lbugjs.node'), Buffer.from('content-irrelevant'));
|
|
await fs.writeFile(path.join(tmpDir, 'install.js'), '');
|
|
const fakeNode = path.join(tmpDir, 'fake-node');
|
|
await fs.writeFile(
|
|
fakeNode,
|
|
'#!/bin/sh\n' +
|
|
'echo "Error: /lib64/libc.so.6: version \\`GLIBC_2.34\' not found' +
|
|
' (required by /x/@ladybugdb/core/lbugjs.node)" >&2\n' +
|
|
'exit 1\n',
|
|
);
|
|
await fs.chmod(fakeNode, 0o755);
|
|
process.execPath = fakeNode;
|
|
|
|
const result = checkLbugNative(tmpDir);
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.kind).toBe('load_failed');
|
|
expect(result.message).toContain('glibc 2.34 or newer');
|
|
expect(result.message).toContain('will NOT help');
|
|
expect(result.message).not.toContain('install.js');
|
|
} finally {
|
|
process.execPath = originalExecPath;
|
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
}
|
|
},
|
|
);
|
|
|
|
it('names this host glibc alongside the required one', () => {
|
|
const message = glibcTooOldMessage("version `GLIBC_2.34' not found");
|
|
// Never pin the runner's own glibc — assert the line is populated either way.
|
|
expect(message).toMatch(/this host: (glibc \d+\.\d+|glibc version could not be determined)/);
|
|
});
|
|
|
|
const requiredVersionCases: ReadonlyArray<readonly [string, string, string]> = [
|
|
['reports the single required version', "version `GLIBC_2.34' not found", '2.34'],
|
|
[
|
|
'reports the highest of several required versions',
|
|
"version `GLIBC_2.29' not found\nversion `GLIBC_2.34' not found",
|
|
'2.34',
|
|
],
|
|
[
|
|
'orders versions numerically, not lexically',
|
|
"version `GLIBC_2.9' not found\nversion `GLIBC_2.34' not found",
|
|
'2.34',
|
|
],
|
|
];
|
|
|
|
it.each(requiredVersionCases)('%s', (_name, stderr, expected) => {
|
|
expect(glibcTooOldMessage(stderr)).toContain(`glibc ${expected} or newer`);
|
|
});
|
|
|
|
const nonGlibcCases: ReadonlyArray<readonly [string, string]> = [
|
|
['an unrelated loader error', 'Error: invalid ELF header'],
|
|
['empty stderr', ''],
|
|
['a GLIBC token with no not-found line', 'linked against GLIBC_2.34 successfully'],
|
|
];
|
|
|
|
it.each(nonGlibcCases)('returns null for %s, leaving the generic message', (_name, stderr) => {
|
|
expect(glibcTooOldMessage(stderr)).toBeNull();
|
|
});
|
|
|
|
it('returns ok:true when the load probe cannot be spawned (inconclusive, not a broken binary)', async () => {
|
|
// The binary is present, but the child probe cannot launch — a sandbox that
|
|
// forbids subprocesses, or a non-Node execPath. We could not test the binary,
|
|
// so a healthy one must not be condemned; the command's own load stays
|
|
// authoritative. (Binary content is irrelevant here — the probe never runs.)
|
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'lbug-check-'));
|
|
const originalExecPath = process.execPath;
|
|
try {
|
|
await fs.writeFile(path.join(tmpDir, 'lbugjs.node'), Buffer.from('content-irrelevant'));
|
|
await fs.writeFile(path.join(tmpDir, 'install.js'), '');
|
|
process.execPath = path.join(tmpDir, 'definitely-not-node');
|
|
|
|
const result = checkLbugNative(tmpDir);
|
|
|
|
expect(result.ok).toBe(true);
|
|
expect(result.message).toBeUndefined();
|
|
} finally {
|
|
process.execPath = originalExecPath;
|
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|