GitNexus/gitnexus/test/unit/lazy-action.test.ts
Gergő Magyar 76f9f70183
fix(cli): LadybugDB native-load failures fail closed, incl. truncated-binary SIGBUS (#2441) (#2651)
* test(cli): cover analyzer lazy-action native-load failure (#2441)

createAnalyzerLbugLazyAction — the wrapper the `analyze` command uses — had
only a happy-path test; its native-load-failure branch was untested, so a
regression could silently reintroduce #2441 (analyze exiting 0 after a
LadybugDB native load failure, writing no index while reporting success).

Add a failure-path test asserting that when checkLbugNative() reports the
binary cannot load, the analyzer module is NOT imported, process.exitCode is
set to 1, and the repair message is written to stderr. Mirrors the existing
createLbugLazyAction failure test.

Verified discriminating: the test fails ("expected undefined to be 1") when
the exitCode guard is removed from the analyzer branch, and passes with it
restored.

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

* fix(cli): probe LadybugDB native load out-of-process so a truncated binary fails closed (#2441)

checkLbugNative() loaded lbugjs.node in-process to validate it. That catches
clean load failures (missing dylib, zero-byte, garbage -> "file too short"),
but a merely truncated/corrupted binary (valid header, missing pages) SIGBUSes
the dynamic loader mid-dlopen — a signal, not a catchable throw — taking the
whole CLI down with a raw exit 135 and no guidance.

Load the binary in a throwaway child process instead. Only a child that RAN and
failed (non-zero exit or a fatal signal) marks the binary bad; if the probe
itself could not run — a spawn error or timeout, e.g. a no-subprocess sandbox
or a non-Node execPath — the result is inconclusive and the command's own load
stays authoritative rather than condemning a healthy binary. The probe forces
ELECTRON_RUN_AS_NODE, removes the redundant in-process pre-load, and costs ~20ms.

Regression tests: truncated binary -> ok:false; unspawnable probe -> ok:true.

Verified: a 300KB-truncated native now exits 1 with the repair message
(previously exit 135 SIGBUS); zero-byte/garbage stay graceful; good native
still loads and indexes.

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-23 11:59:56 +01:00

135 lines
4.8 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import { createAnalyzerLbugLazyAction, createLazyAction } from '../../src/cli/lazy-action.js';
const { checkLbugNativeMock } = vi.hoisted(() => ({
checkLbugNativeMock: vi.fn(() => ({ ok: true })),
}));
vi.mock('../../src/core/lbug/native-check.js', () => ({
checkLbugNative: checkLbugNativeMock,
}));
describe('createLazyAction', () => {
it('does not import target module until invoked', async () => {
const loader = vi.fn(async () => ({
run: vi.fn(async () => 'ok'),
}));
const action = createLazyAction(loader, 'run');
expect(loader).not.toHaveBeenCalled();
await expect(action('arg-1')).resolves.toBeUndefined();
expect(loader).toHaveBeenCalledTimes(1);
});
it('throws a clear error when export is not a function', async () => {
const action = createLazyAction(async () => ({ notAFunction: 'string-value' }), 'notAFunction');
await expect(action()).rejects.toThrow('notAFunction');
});
});
describe('createLbugLazyAction', () => {
it('fails before importing the target module when LadybugDB native cannot load', async () => {
checkLbugNativeMock.mockReturnValueOnce({
ok: false,
message:
'LadybugDB native binary (lbugjs.node) exists but failed to load:\n' + ' dlopen failed',
});
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
process.exitCode = undefined;
const loader = vi.fn(async () => ({
run: vi.fn(async () => 'ok'),
}));
try {
const { createLbugLazyAction } = await import('../../src/cli/lazy-action.js');
const action = createLbugLazyAction(loader, 'run');
await expect(action('arg-1')).resolves.toBeUndefined();
expect(loader).not.toHaveBeenCalled();
expect(process.exitCode).toBe(1);
expect(stderrSpy).toHaveBeenCalledWith(
expect.stringContaining('LadybugDB native binary (lbugjs.node) exists but failed to load:'),
);
} finally {
stderrSpy.mockRestore();
process.exitCode = undefined;
}
});
});
describe('createAnalyzerLbugLazyAction', () => {
it('captures identity before probing native code or importing the analyzer graph', async () => {
const events: string[] = [];
const receipt = { schemaVersion: 4 };
const run = vi.fn(async () => undefined);
const identityLoader = vi.fn(async () => {
events.push('identity-module');
return {
captureAnalyzerIdentityBeforeLoad: async (_url: string, loader: () => Promise<unknown>) => {
events.push('receipt-captured');
const loaded = await loader();
return { runnerIdentity: receipt, loaded };
},
};
});
const analyzerLoader = vi.fn(async () => {
events.push('analyzer-module');
return { run };
});
const action = createAnalyzerLbugLazyAction(
identityLoader as never,
analyzerLoader,
'run',
'file:///fixture/dist/cli/index.js',
);
await action('repo', { force: true });
expect(events).toEqual(['identity-module', 'receipt-captured', 'analyzer-module']);
expect(run).toHaveBeenCalledWith(receipt, 'repo', { force: true });
});
it('sets exit code 1 and skips the analyzer import when native load fails', async () => {
// Regression guard for #2441: a LadybugDB native-load failure must fail
// closed — no analyzer import, no index write, non-zero exit — not the
// pre-fix "print help then exit 0" silent success. Mirrors the
// createLbugLazyAction failure test above for the analyze-only wrapper.
checkLbugNativeMock.mockReturnValueOnce({
ok: false,
message:
'LadybugDB native binary (lbugjs.node) exists but failed to load:\n' + ' dlopen failed',
});
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
process.exitCode = undefined;
const run = vi.fn(async () => undefined);
const analyzerLoader = vi.fn(async () => ({ run }));
const identityLoader = vi.fn(async () => ({
captureAnalyzerIdentityBeforeLoad: async (_url: string, loader: () => Promise<unknown>) => {
const loaded = await loader();
return { runnerIdentity: { schemaVersion: 4 }, loaded };
},
}));
const action = createAnalyzerLbugLazyAction(
identityLoader as never,
analyzerLoader,
'run',
'file:///fixture/dist/cli/index.js',
);
try {
await expect(action('repo', { force: true })).resolves.toBeUndefined();
expect(analyzerLoader).not.toHaveBeenCalled();
expect(run).not.toHaveBeenCalled();
expect(process.exitCode).toBe(1);
expect(stderrSpy).toHaveBeenCalledWith(
expect.stringContaining('LadybugDB native binary (lbugjs.node) exists but failed to load:'),
);
} finally {
stderrSpy.mockRestore();
process.exitCode = undefined;
}
});
});