mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-07 02:58:02 +00:00
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
* 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>
380 lines
14 KiB
TypeScript
380 lines
14 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import {
|
|
ExtensionManager,
|
|
getExtensionInstallChildProcessArgs,
|
|
getExtensionInstallPolicy,
|
|
getExtensionInstallTimeoutMs,
|
|
type ExtensionInstallResult,
|
|
} from '../../src/core/lbug/extension-loader.js';
|
|
|
|
const okInstall: ExtensionInstallResult = {
|
|
success: true,
|
|
timedOut: false,
|
|
message: 'installed',
|
|
};
|
|
const failedInstall: ExtensionInstallResult = {
|
|
success: false,
|
|
timedOut: false,
|
|
message: 'install failed',
|
|
};
|
|
const timedOutInstall: ExtensionInstallResult = {
|
|
success: false,
|
|
timedOut: true,
|
|
message: 'INSTALL vector timed out after 10ms',
|
|
};
|
|
|
|
const noopWarn = (): void => {};
|
|
|
|
describe('ExtensionManager — LOAD-first behavior', () => {
|
|
it('uses LOAD only and never invokes INSTALL when the extension is already available', async () => {
|
|
const installExtension = vi.fn();
|
|
const manager = new ExtensionManager({ policy: 'auto', installExtension });
|
|
const query = vi.fn().mockResolvedValue({});
|
|
|
|
await expect(manager.ensure(query, 'fts', 'FTS')).resolves.toBe(true);
|
|
|
|
expect(query.mock.calls.map(([sql]) => sql)).toEqual(['LOAD EXTENSION fts']);
|
|
expect(installExtension).not.toHaveBeenCalled();
|
|
expect(manager.getCapabilities()).toEqual([{ name: 'fts', loaded: true }]);
|
|
});
|
|
|
|
it('treats "already loaded" load errors as success', async () => {
|
|
const installExtension = vi.fn();
|
|
const manager = new ExtensionManager({ policy: 'auto', installExtension });
|
|
const query = vi.fn().mockRejectedValue(new Error('Extension fts is already loaded'));
|
|
|
|
await expect(manager.ensure(query, 'fts', 'FTS')).resolves.toBe(true);
|
|
expect(installExtension).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('ExtensionManager — install policies', () => {
|
|
it('runs bounded out-of-process INSTALL and retries LOAD when policy=auto', async () => {
|
|
const installExtension = vi.fn().mockResolvedValue(okInstall);
|
|
const manager = new ExtensionManager({ policy: 'auto', installExtension });
|
|
const query = vi
|
|
.fn()
|
|
.mockRejectedValueOnce(new Error('Extension "fts" not found'))
|
|
.mockResolvedValueOnce({});
|
|
|
|
await expect(manager.ensure(query, 'fts', 'FTS', { installTimeoutMs: 1234 })).resolves.toBe(
|
|
true,
|
|
);
|
|
|
|
// The LOAD failure reason is threaded to the installer so it can pick
|
|
// INSTALL vs FORCE INSTALL from the error class (#2374, PR #2375).
|
|
expect(installExtension).toHaveBeenCalledWith('fts', 1234, 'Extension "fts" not found');
|
|
expect(query.mock.calls.map(([sql]) => sql)).toEqual([
|
|
'LOAD EXTENSION fts',
|
|
'LOAD EXTENSION fts',
|
|
]);
|
|
expect(query.mock.calls.some(([sql]) => String(sql).startsWith('INSTALL '))).toBe(false);
|
|
});
|
|
|
|
it('skips INSTALL and warns when policy=load-only', async () => {
|
|
const installExtension = vi.fn();
|
|
const warn = vi.fn();
|
|
const manager = new ExtensionManager({ policy: 'load-only', installExtension, warn });
|
|
const query = vi.fn().mockRejectedValue(new Error('Extension "fts" not found'));
|
|
|
|
await expect(manager.ensure(query, 'fts', 'FTS')).resolves.toBe(false);
|
|
|
|
expect(installExtension).not.toHaveBeenCalled();
|
|
expect(warn).toHaveBeenCalledWith(expect.stringContaining('continuing without FTS features'));
|
|
expect(manager.getCapabilities()).toMatchObject([
|
|
{ name: 'fts', loaded: false, reason: expect.stringContaining('load-only') },
|
|
]);
|
|
});
|
|
|
|
it('short-circuits LOAD and INSTALL when policy=never', async () => {
|
|
const installExtension = vi.fn();
|
|
const warn = vi.fn();
|
|
const manager = new ExtensionManager({ policy: 'never', installExtension, warn });
|
|
const query = vi.fn();
|
|
|
|
await expect(manager.ensure(query, 'vector', 'VECTOR')).resolves.toBe(false);
|
|
|
|
expect(query).not.toHaveBeenCalled();
|
|
expect(installExtension).not.toHaveBeenCalled();
|
|
expect(warn).toHaveBeenCalledWith(
|
|
expect.stringContaining('continuing without VECTOR features'),
|
|
);
|
|
});
|
|
|
|
it('per-call options override manager defaults', async () => {
|
|
const installExtension = vi.fn().mockResolvedValue(okInstall);
|
|
const manager = new ExtensionManager({
|
|
policy: 'auto',
|
|
installExtension,
|
|
warn: noopWarn,
|
|
});
|
|
const query = vi.fn().mockRejectedValue(new Error('Extension "vector" not found'));
|
|
|
|
await expect(manager.ensure(query, 'vector', 'VECTOR', { policy: 'load-only' })).resolves.toBe(
|
|
false,
|
|
);
|
|
|
|
expect(installExtension).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('returns false and warns when bounded install times out', async () => {
|
|
const installExtension = vi.fn().mockResolvedValue(timedOutInstall);
|
|
const warn = vi.fn();
|
|
const manager = new ExtensionManager({ policy: 'auto', installExtension, warn });
|
|
const query = vi.fn().mockRejectedValue(new Error('Extension "vector" not found'));
|
|
|
|
await expect(manager.ensure(query, 'vector', 'VECTOR', { installTimeoutMs: 10 })).resolves.toBe(
|
|
false,
|
|
);
|
|
|
|
expect(query.mock.calls.map(([sql]) => sql)).toEqual(['LOAD EXTENSION vector']);
|
|
expect(warn).toHaveBeenCalledWith(
|
|
expect.stringContaining('continuing without VECTOR features'),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('ExtensionManager — reason strings carry the real LOAD error (#2374)', () => {
|
|
it('load-only failure reason includes the underlying LadybugDB error, collapsed to one line', async () => {
|
|
const warn = vi.fn();
|
|
const manager = new ExtensionManager({ policy: 'load-only', warn });
|
|
const query = vi
|
|
.fn()
|
|
.mockRejectedValue(
|
|
new Error(
|
|
'IO exception: Failed to load library: /x/libfts.lbug_extension.\ninvalid ELF header',
|
|
),
|
|
);
|
|
|
|
await expect(manager.ensure(query, 'fts', 'FTS')).resolves.toBe(false);
|
|
|
|
expect(manager.getCapabilities()).toMatchObject([
|
|
{
|
|
name: 'fts',
|
|
loaded: false,
|
|
reason: expect.stringContaining(
|
|
'LOAD fts failed: IO exception: Failed to load library: /x/libfts.lbug_extension. invalid ELF header',
|
|
),
|
|
},
|
|
]);
|
|
expect(warn).toHaveBeenCalledWith(expect.stringContaining('invalid ELF header'));
|
|
});
|
|
|
|
it('failed-install reason includes both the install message and the original LOAD error', async () => {
|
|
const installExtension = vi.fn().mockResolvedValue(failedInstall);
|
|
const manager = new ExtensionManager({ policy: 'auto', installExtension, warn: noopWarn });
|
|
const query = vi.fn().mockRejectedValue(new Error('Extension "fts" not found'));
|
|
|
|
await expect(manager.ensure(query, 'fts', 'FTS')).resolves.toBe(false);
|
|
|
|
expect(manager.getCapabilities()).toMatchObject([
|
|
{
|
|
name: 'fts',
|
|
loaded: false,
|
|
reason: 'install failed; LOAD fts had failed: Extension "fts" not found',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('post-install LOAD failure reason includes the retry error', async () => {
|
|
const installExtension = vi.fn().mockResolvedValue(okInstall);
|
|
const manager = new ExtensionManager({ policy: 'auto', installExtension, warn: noopWarn });
|
|
const query = vi
|
|
.fn()
|
|
.mockRejectedValue(new Error('version mismatch: extension built for 0.17.0'));
|
|
|
|
await expect(manager.ensure(query, 'fts', 'FTS')).resolves.toBe(false);
|
|
|
|
expect(manager.getCapabilities()).toMatchObject([
|
|
{
|
|
name: 'fts',
|
|
loaded: false,
|
|
reason:
|
|
'LOAD fts failed after successful INSTALL: version mismatch: extension built for 0.17.0',
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('ExtensionManager — caching', () => {
|
|
it('caches install attempt outcome to avoid retrying within the same process', async () => {
|
|
const installExtension = vi.fn().mockResolvedValue(timedOutInstall);
|
|
const manager = new ExtensionManager({
|
|
policy: 'auto',
|
|
installExtension,
|
|
warn: noopWarn,
|
|
});
|
|
const query = vi.fn().mockRejectedValue(new Error('Extension "vector" not found'));
|
|
|
|
await expect(manager.ensure(query, 'vector', 'VECTOR')).resolves.toBe(false);
|
|
await expect(manager.ensure(query, 'vector', 'VECTOR')).resolves.toBe(false);
|
|
|
|
expect(installExtension).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('reset() clears capability and install state so install is retried', async () => {
|
|
const installExtension = vi.fn().mockResolvedValue(failedInstall);
|
|
const manager = new ExtensionManager({
|
|
policy: 'auto',
|
|
installExtension,
|
|
warn: noopWarn,
|
|
});
|
|
const query = vi.fn().mockRejectedValue(new Error('Extension "fts" not found'));
|
|
|
|
await manager.ensure(query, 'fts', 'FTS');
|
|
expect(manager.getCapabilities()).toHaveLength(1);
|
|
|
|
manager.reset();
|
|
expect(manager.getCapabilities()).toEqual([]);
|
|
|
|
await manager.ensure(query, 'fts', 'FTS');
|
|
expect(installExtension).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|
|
|
|
describe('ExtensionManager — observability', () => {
|
|
it('exposes per-extension capability snapshot', async () => {
|
|
const manager = new ExtensionManager({ policy: 'load-only', warn: noopWarn });
|
|
const okQuery = vi.fn().mockResolvedValue({});
|
|
const failQuery = vi.fn().mockRejectedValue(new Error('Extension "vector" not found'));
|
|
|
|
await manager.ensure(okQuery, 'fts', 'FTS');
|
|
await manager.ensure(failQuery, 'vector', 'VECTOR');
|
|
|
|
expect(manager.getCapabilities()).toMatchObject([
|
|
{ name: 'fts', loaded: true },
|
|
{ name: 'vector', loaded: false, reason: expect.stringContaining('load-only') },
|
|
]);
|
|
});
|
|
|
|
it('warns at most once per (extension, reason) pair', async () => {
|
|
const installExtension = vi.fn();
|
|
const warn = vi.fn();
|
|
const manager = new ExtensionManager({ policy: 'load-only', installExtension, warn });
|
|
const query = vi.fn().mockRejectedValue(new Error('Extension "fts" not found'));
|
|
|
|
await manager.ensure(query, 'fts', 'FTS');
|
|
await manager.ensure(query, 'fts', 'FTS');
|
|
|
|
expect(warn).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe('ExtensionManager — input validation', () => {
|
|
it('rejects extension names that are not bare identifiers', async () => {
|
|
const manager = new ExtensionManager({ policy: 'auto' });
|
|
const query = vi.fn();
|
|
|
|
await expect(manager.ensure(query, 'fts; DROP TABLE x', 'FTS')).rejects.toThrow(/Invalid/);
|
|
expect(query).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('installDuckDbExtensionOutOfProcess child process', () => {
|
|
it('spawns the stable packaged installer script instead of inline -e code', () => {
|
|
const args = getExtensionInstallChildProcessArgs('fts');
|
|
|
|
expect(args).not.toContain('-e');
|
|
expect(args).not.toContain('--input-type=module');
|
|
expect(args[0]).toContain('scripts');
|
|
expect(args[0]).toContain('install-duckdb-extension.mjs');
|
|
expect(args[1]).toBe('fts');
|
|
expect(Number(args[2])).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('passes the resolved LadybugDB max DB size to the installer child', () => {
|
|
expect(getExtensionInstallChildProcessArgs('fts', 1234).at(-1)).toBe('1234');
|
|
});
|
|
});
|
|
|
|
describe('getExtensionInstallPolicy', () => {
|
|
it('defaults to load-only when env var is unset', () => {
|
|
const original = process.env.GITNEXUS_LBUG_EXTENSION_INSTALL;
|
|
delete process.env.GITNEXUS_LBUG_EXTENSION_INSTALL;
|
|
try {
|
|
expect(getExtensionInstallPolicy()).toBe('load-only');
|
|
} finally {
|
|
if (original === undefined) {
|
|
delete process.env.GITNEXUS_LBUG_EXTENSION_INSTALL;
|
|
} else {
|
|
process.env.GITNEXUS_LBUG_EXTENSION_INSTALL = original;
|
|
}
|
|
}
|
|
});
|
|
|
|
it('returns auto when env var is set to auto', () => {
|
|
const original = process.env.GITNEXUS_LBUG_EXTENSION_INSTALL;
|
|
process.env.GITNEXUS_LBUG_EXTENSION_INSTALL = 'auto';
|
|
try {
|
|
expect(getExtensionInstallPolicy()).toBe('auto');
|
|
} finally {
|
|
if (original === undefined) {
|
|
delete process.env.GITNEXUS_LBUG_EXTENSION_INSTALL;
|
|
} else {
|
|
process.env.GITNEXUS_LBUG_EXTENSION_INSTALL = original;
|
|
}
|
|
}
|
|
});
|
|
|
|
it('returns never when env var is set to never', () => {
|
|
const original = process.env.GITNEXUS_LBUG_EXTENSION_INSTALL;
|
|
process.env.GITNEXUS_LBUG_EXTENSION_INSTALL = 'never';
|
|
try {
|
|
expect(getExtensionInstallPolicy()).toBe('never');
|
|
} finally {
|
|
if (original === undefined) {
|
|
delete process.env.GITNEXUS_LBUG_EXTENSION_INSTALL;
|
|
} else {
|
|
process.env.GITNEXUS_LBUG_EXTENSION_INSTALL = original;
|
|
}
|
|
}
|
|
});
|
|
|
|
it('falls back to load-only for invalid env var values', () => {
|
|
const original = process.env.GITNEXUS_LBUG_EXTENSION_INSTALL;
|
|
process.env.GITNEXUS_LBUG_EXTENSION_INSTALL = 'bogus';
|
|
try {
|
|
expect(getExtensionInstallPolicy()).toBe('load-only');
|
|
} finally {
|
|
if (original === undefined) {
|
|
delete process.env.GITNEXUS_LBUG_EXTENSION_INSTALL;
|
|
} else {
|
|
process.env.GITNEXUS_LBUG_EXTENSION_INSTALL = original;
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('getExtensionInstallTimeoutMs', () => {
|
|
it('reads a positive override from the environment', () => {
|
|
const original = process.env.GITNEXUS_LBUG_EXTENSION_INSTALL_TIMEOUT_MS;
|
|
process.env.GITNEXUS_LBUG_EXTENSION_INSTALL_TIMEOUT_MS = '42';
|
|
try {
|
|
expect(getExtensionInstallTimeoutMs()).toBe(42);
|
|
} finally {
|
|
if (original === undefined) {
|
|
delete process.env.GITNEXUS_LBUG_EXTENSION_INSTALL_TIMEOUT_MS;
|
|
} else {
|
|
process.env.GITNEXUS_LBUG_EXTENSION_INSTALL_TIMEOUT_MS = original;
|
|
}
|
|
}
|
|
});
|
|
|
|
it('falls back to the default when the env var is missing or invalid', () => {
|
|
const original = process.env.GITNEXUS_LBUG_EXTENSION_INSTALL_TIMEOUT_MS;
|
|
delete process.env.GITNEXUS_LBUG_EXTENSION_INSTALL_TIMEOUT_MS;
|
|
try {
|
|
expect(getExtensionInstallTimeoutMs()).toBe(15_000);
|
|
process.env.GITNEXUS_LBUG_EXTENSION_INSTALL_TIMEOUT_MS = 'notanumber';
|
|
expect(getExtensionInstallTimeoutMs()).toBe(15_000);
|
|
process.env.GITNEXUS_LBUG_EXTENSION_INSTALL_TIMEOUT_MS = '0';
|
|
expect(getExtensionInstallTimeoutMs()).toBe(15_000);
|
|
} finally {
|
|
if (original === undefined) {
|
|
delete process.env.GITNEXUS_LBUG_EXTENSION_INSTALL_TIMEOUT_MS;
|
|
} else {
|
|
process.env.GITNEXUS_LBUG_EXTENSION_INSTALL_TIMEOUT_MS = original;
|
|
}
|
|
}
|
|
});
|
|
});
|