GitNexus/gitnexus/test/unit/extension-load-error.test.ts
Gergő Magyar 76a1c90b02
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
fix(fts): diagnose Windows FTS missing-dependency load failures (#2374, Phase 1) (#2383)
* feat(lbug): classify FTS extension load errors with Windows missing-dependency guard (#2374)

Add classifyExtensionLoadError() — a pure-string, lbug-free four-way
classifier (missing_file / corrupt_file / missing_dependency / unknown).
The Windows catch-all guard keys missing_dependency strictly on the
error-126 signal, never LadybugDB's generic 'Failed to load library …
needed by extension' wrapper, so 127/5/1114 and truncated (193) files
route correctly.

* feat(fts): surface classified missing-dependency remedy in doctor, repair-fts, and degrade warnings (#2374)

Route the FTS load reason through classifyExtensionLoadError at all four
surfaces (doctor, --repair-fts error, analyze degrade log, ftsDegradedWarning).
For the Windows missing-dependency class, emit the runtime-install remedy
(VC++ redist, then OpenSSL) instead of the wrong reinstall-over-network
guidance; other classes keep their existing routing. Path redaction preserved
on the client-facing warning.

* test(fts): assert doctor surfaces the classified remedy end-to-end (#2374)

Extend the broken-file e2e: doctor now prints the corrupt-file re-download
remedy through the real CLI, and the Windows missing-dependency remedy
(VC++/OpenSSL) must not misfire on a corrupt file — the catch-all guard,
verified end-to-end. Also assert the repair path does not misfire.

* style(fts): apply prettier formatting to #2374 diagnosis files

* feat(fts): language-independent hedged fallback for Windows load failures (#2374)

The Windows OS-error tail is localized, so matching only en/zh 126 text left
other locales on the generic 'run doctor' remedy. lbug's 'Failed to load
library' wrapper is English on every platform and present for all load
failures, so use it as a fallback: when the localized tail matches no specific
class, emit a hedged remedy that points the user at their own OS error and
offers both branches (install runtime / --repair-fts) without prescribing the
wrong single fix. Precise en/zh 126 keeps its definite remedy.

* feat(fts): language-independent structural classifier via binary inspection (#2374)

Add diagnoseExtensionLoad: pull the extension's file path out of lbug's own
English wrapper and inspect the binary header (PE/ELF/Mach-O magic + arch)
directly, so corrupt-vs-valid is decided by the file itself, not the localized
OS-error tail. A valid binary that still failed to load ⇒ missing_dependency
(runtime dep), decided in any OS display language and on all three platforms.
Falls back to the string classifier (with its hedged fallback) when the file
can't be read. Wire all four surfaces to it. Event Viewer / GetLastError-via-FFI
were dead ends (lbug catches the failure — no crash event; no native FFI dep).

* test(fts): exercise the structural classifier on real binaries (#2374)

Add an integration suite that runs inspectExtensionBinary/diagnoseExtensionLoad
against genuine binaries — the running node executable, the real lbugjs.node
addon, and the installed FTS extension (valid); a truncated real binary and a
real text file (corrupt). Registered in cross-platform-tests PLATFORM_LOGIC so
it runs on the Windows + macOS matrix, proving the PE and Mach-O header parsing
on real PE/Mach-O files (ubuntu covers ELF).

* fix(fts): honor a corrupt_file verdict over a structurally-valid header (#2374)

The structural probe in diagnoseExtensionLoad inspects only the first 4 KB, so a
download truncated after its header reads 'valid' and was routed to the "install
VC++, reinstalling will NOT help" remedy — the exact loop #2374 exists to kill,
for the truncated-download case the module docstring claims it handles. Honor the
loader's own corruption report ("file too short" / Windows error 193
"not a valid Win32 application") before defaulting to the dependency remedy;
localized corrupt tails stay hedged missing_dependency, preserving
language-independence.

Addresses PR #2383 review finding F1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(fts): return indeterminate for a PE header beyond the read window (#2374)

The structural probe reads only BINARY_HEADER_BYTES (4 KB). A valid PE with a
large DOS stub whose e_lfanew points past that window was wrongly called
'corrupt', routing a fine DLL to "re-download". A garbage e_lfanew from a truly
corrupt file is indistinguishable from here, so widen the header verdict with
'indeterminate' and return it in that case; the caller then defers to the
loader's own report instead of asserting a false verdict. Fat Mach-O stays valid
(LadybugDB ships thin per-arch binaries). Also covers the unmapped-arch and
garbage-PE-signature branches.

Addresses PR #2383 review finding F1-secondary.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(fts): drop contradictory reinstall guidance from the analyze degrade log (#2374)

For a missing runtime dependency the extension file is present, so appending
FTS_UNAVAILABLE_MESSAGE (which tells the user to install it "with network
access") to the remedy ("reinstalling will NOT help") produced self-contradictory
guidance on the main analyze surface. Lead the missing_dependency degrade log
with the class-neutral sentence (FTS_UNAVAILABLE_LEAD) and append only the
classified remedy; other classes keep FTS_UNAVAILABLE_MESSAGE unchanged.

Addresses PR #2383 review finding F2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* perf(fts): cache the load diagnosis so the degraded warning does no per-request I/O (#2374)

ftsDegradedWarning() runs on every degraded /api/search response and MCP query,
and it was calling diagnoseExtensionLoad — a synchronous openSync/readSync of the
extension file — on every call. Compute the diagnosis once at mark-unavailable
time (the single load-failure sink, run per Database not per request), cache it on
ExtensionCapability, and have the warning read the cached result (falling back to
the pure, no-I/O string classifier if it is absent). Loader capability-shape
assertions relax from toEqual to toMatchObject for the new optional field.

Addresses PR #2383 review finding F3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(fts): cover the missing_dependency remedy on the --repair-fts path (#2374)

The repair-fts error interpolates the classified remedy, but no test reached the
missing_dependency branch — only the corrupt/invalid-ELF path. Add a Windows
error-126 case asserting the thrown error carries the VC++ redistributable remedy
and omits the old "retry the network install" tail, and that no index is dropped.

Addresses PR #2383 review finding F6a (--repair-fts surface).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(fts): share the VC++ redistributable install hint (#2374)

The Microsoft Visual C++ redistributable name and aka.ms URL were duplicated
verbatim in WINDOWS_MISSING_DEPENDENCY_REMEDY and
STRUCTURAL_MISSING_DEPENDENCY_REMEDY. Factor a single VC_REDIST_INSTALL_HINT
constant so the pointer cannot drift between them; the composed remedy strings are
byte-identical (existing exact-text assertions unchanged). Also adds a test
covering the previously-unexercised structural remedy branch.

Addresses PR #2383 review finding F5a.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(fts): guard FILE_CORRUPTION_SIGNATURES parity with the installer script (#2374)

The corruption-signature list is deliberately duplicated between
extension-load-error.ts and scripts/install-duckdb-extension.mjs (the .mjs cannot
import the .ts), with nothing guarding against drift — a one-sided edit would
desync the FORCE-INSTALL verb from remedy classification. Export the array from
both and add a parity test that compares regex source + flags element-wise.

Addresses PR #2383 review finding F5b.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(test): run extension-binary-real in the sequential lbug-db vitest project (#2374)

extension-binary-real.test.ts imports @ladybugdb/core but ran in the parallel
`default` project, contrary to TESTING.md's rule that native-LadybugDB tests live
in the sequential `lbug-db` project. Add it to the lbug-db include list and the
default exclude list; it now runs under lbug-db and no longer under default.

Addresses PR #2383 review finding F6c.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(fts): fail loud, not silent-skip, on missing FTS artifacts under REQUIRE_FTS=1 (#2374)

The real-binary structural tests gated on raw .skipIf(!lbugNative) /
.skipIf(!installedFts), so under GITNEXUS_REQUIRE_FTS=1 a missing artifact would
silently vanish from a green CI run (the #2299 trap). These tests inspect the
extension file directly and need its path, not a loaded connection — so
skipUnlessFtsAvailable (which needs an initialized LadybugDB) does not fit. Add
requireFtsResourceOrSkip: skip gracefully offline, throw under REQUIRE_FTS=1. The
always-on process.execPath assertion still runs everywhere.

Addresses PR #2383 review finding F6d.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* style(fts): apply prettier formatting to the #2383 fix files (#2374)

Line-wrapping only; the quality/format CI check flagged three files. No behavior
change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 21:27:37 +01:00

350 lines
15 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);
});
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/);
} 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' });
});
});