mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-01 02:01:24 +00:00
* fix(lbug): pin Ladybug core so Dependabot cannot ship a skewed FTS artifact The extension version is a separate upstream constant. Ignore daily core bumps and fail the pairing gate when the committed manifest does not name the installed core. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): make doctor and CI FTS gates resolve the packaged artifact Doctor and the REQUIRE_FTS file gates still treated an empty ~/.lbdb as unavailable, which would turn three CI jobs red once analyze stops installing into that tree. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): name native-abort and tuple-missing so analyze cannot mis-advise The CLI summary's trailing else treated every unknown skip reason as a missing extension. New crash and platform causes must get their own remedies, not a network-install hint. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): delete the dead read-path FTS index create ensureFTSIndex had no production callers and swallowed read-only CREATE_FTS_INDEX failures, which hid the only signal that a reader tried to write. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): vendor per-platform FTS artifacts so analyze needs no host install Keyword search depended on a CDN fetch into ~/.lbdb. Shipping the five published tuples inside the package makes air-gapped and ignore-scripts installs load the same artifact the publish gate checksums. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): load the packaged FTS artifact before any network install Analyze still required a CDN fetch into ~/.lbdb even when the package already shipped the file. FTS now path-loads the vendored tuple first and records source labels so a later truncated home copy cannot steal the diagnosis. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): diagnose a core/extension version skew instead of a missing runtime A structurally valid FTS artifact whose path version disagrees with the packaged pin must name both versions, not prescribe VC++ or OpenSSL. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): stamp an FTS phase so repair stays usable after an in-place abort A native CREATE_FTS_INDEX abort leaves no skip reason; the next run infers it from the dirty flag, and --repair-fts must not treat that phase as a half-written graph. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): park an in-place FTS crash WAL without wiping the graph An FTS abort after a successful checkpoint must reopen the live index on macOS, Windows, and Linux. Staging never parks the live WAL; readers keep today's large-WAL refusal. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): refuse read-only opens of an FTS-poisoned WAL MCP and serve cannot repair a leftover in-place abort. Fail before the native open and name --repair-fts, on macOS, Windows, and Linux. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): name a vendor-neutral Windows OpenSSL prerequisite OQ1 is unanswered here so GitNexus does not ship OpenSSL DLLs. Windows FTS now asks for a system OpenSSL 3 runtime instead of Git Bash PATH. Co-authored-by: Cursor <cursoragent@cursor.com> * test(lbug): inject the FTS vendor root and redact it on HTTP and MCP Path-loaded artifacts no longer vary with HOME. Tests pass an injected vendor tree and assert search warnings never leak a filesystem path. Co-authored-by: Cursor <cursoragent@cursor.com> * docs(lbug): document load-only as the global FTS install default Analyze still overrides to auto. Packaged per-platform artifacts load before any network install on macOS, Windows, and Linux. Co-authored-by: Cursor <cursoragent@cursor.com> * docs(lbug): format the FTS install-policy README table Prettier does not run on Markdown in pre-commit, so the U10 table wrap needs its own formatting commit. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): skip FTS CREATE after a persisted native abort A recovered analyze run was retrying CREATE_FTS_INDEX from skipReason alone. Keep that skip until --repair-fts, fail closed on unsupported tuples, and honor the checkpoint warrant for park/repair. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): honor checkpoint flushed warrant and align FTS tests with packaged vendor A no-op CHECKPOINT must not satisfy the FTS park warrant, and CI still asserted HOME-only FTS isolation after analyze started path-LOADing the packaged artifact. Co-authored-by: Cursor <cursoragent@cursor.com> * test(lbug): accept a nonempty incremental write set in the #2790 recovery check FTS-phase recovery can incremental-add files (changed=0, added=1). That is not the #2790 empty-diff wipe skip. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): compare FTS home versions to the core pin and tighten the publish filename gate Ladybug's ~/.lbdb/extension directory is the runtime/core version; treating it as the artifact version false-diagnosed skew. The publish guard now rejects a path-escaping filename the same way the fetch script does. Co-authored-by: Cursor <cursoragent@cursor.com> * test(lbug): seed FTS e2e fixtures from the packaged vendor artifact A machine with no ~/.lbdb copy should still run the vendor-survivorship cases; the seed no longer depends on HOME or a network install. Co-authored-by: Cursor <cursoragent@cursor.com> * Address PR review feedback (#3274) Keep in-place FTS abort evidence after persist so a second CREATE abort cannot fail-open readers, and close the CLI, loader, embed, and e2e gaps the review called out. Note: full npm test hit Ladybug worker-pool startup failures under memory pressure; tsc and 180 targeted unit tests passed. Co-authored-by: Cursor <cursoragent@cursor.com> * Address PR review feedback (#3274) Run the vendored-path symlink guard on the OS matrix, put e2e HOME fixtures on Ladybug's real extension layout, pin the embed crash-WAL gate before the writable open, and let analyze writers park through missing-shadow recovery. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(lbug): keep --repair-fts CI green after vendored-first FTS Never-installed warning fixtures must not inspect a packaged vendor binary, and a failed dirty restamp must not abort an otherwise successful --repair-fts run. Co-authored-by: Cursor <cursoragent@cursor.com> * test(cli): give the #1169 analyze e2e the same 90s Windows budget as its sibling The first #1169 persist-meta case was still on a 60s spawn/it budget and was killed banner-only on windows-latest after the FTS warning fixture no longer failed the shard first. Co-authored-by: Cursor <cursoragent@cursor.com> * test(ci): reweight Windows shards after the FTS e2e grew Vendored-first HOME fixtures pushed fts-extension-e2e to ~6 minutes on windows-latest, so the old 146s weight packed it with skills-e2e and blew the 20-minute watchdog. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
421 lines
18 KiB
TypeScript
421 lines
18 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,
|
|
usesClassifiedLoadRemedy,
|
|
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);
|
|
expect(remedy).toMatch(/system runtime/);
|
|
expect(remedy).not.toMatch(/Git Bash|mingw64|Program Files\\Git/i);
|
|
// 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/);
|
|
expect(remedy).toMatch(/OpenSSL 3/);
|
|
expect(remedy).not.toMatch(/Git Bash|mingw64|Program Files\\Git/i);
|
|
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' });
|
|
});
|
|
|
|
it('a valid artifact with mismatched versions is version_skew, not missing_dependency (U3)', () => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'ext-diag-skew-'));
|
|
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: <localized>`;
|
|
const { kind, remedy } = diagnoseExtensionLoad(reason, 'FTS', file, {
|
|
expected: '0.18.1',
|
|
found: '0.17.0',
|
|
});
|
|
expect(kind).toBe('version_skew');
|
|
expect(remedy).toContain('0.18.1');
|
|
expect(remedy).toContain('0.17.0');
|
|
expect(remedy).not.toMatch(/VC\+\+|OpenSSL|vcredist/i);
|
|
expect(remedy).not.toContain(file);
|
|
expect(usesClassifiedLoadRemedy(kind)).toBe(true);
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('matching versions plus a Windows 126 signature stay missing_dependency', () => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'ext-diag-match-'));
|
|
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: The specified module could not be found.`;
|
|
expect(
|
|
diagnoseExtensionLoad(reason, 'FTS', file, { expected: '0.18.1', found: '0.18.1' }),
|
|
).toMatchObject({ kind: 'missing_dependency' });
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('a header-valid file the loader calls "file too short" stays corrupt_file even when versions also differ', () => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'ext-diag-valid-trunc-skew-'));
|
|
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: file too short`;
|
|
expect(
|
|
diagnoseExtensionLoad(reason, 'FTS', file, { expected: '0.18.1', found: '0.17.0' }),
|
|
).toMatchObject({ kind: 'corrupt_file' });
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('a truncated artifact stays corrupt even when versions also differ', () => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'ext-diag-trunc-skew-'));
|
|
const file = join(dir, 'libfts.lbug_extension');
|
|
writeFileSync(file, Buffer.from('short'));
|
|
try {
|
|
const reason = `Failed to load library: ${file} which is needed by extension: fts. Error: file too short`;
|
|
expect(
|
|
diagnoseExtensionLoad(reason, 'FTS', file, { expected: '0.18.1', found: '0.17.0' }),
|
|
).toMatchObject({ kind: 'corrupt_file' });
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|