mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-09 22:33:39 +00:00
* 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>
135 lines
4.8 KiB
TypeScript
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;
|
|
}
|
|
});
|
|
});
|