GitNexus/gitnexus/test/unit/extension-load-error.test.ts
Gergő Magyar 21a52af1d4
fix(lbug): ship FTS per-platform and recover in-place native aborts (#3274)
* 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>
2026-09-14 08:52:24 +01:00

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 });
}
});
});