mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-24 00:51:53 +00:00
* fix(deps): upgrade @ladybugdb/core to 0.16.0 to resolve native segfaults Resolves the SIGSEGV / access-violation (0xC0000005) / exit-139 crashes that have been reported widely since 1.6.3. The native crashes originate in @ladybugdb/core 0.15.x — primarily during FTS index creation, VECTOR extension load, and concurrent query teardown — and are reproducible on Linux, macOS and Windows. The maintainer-confirmed fix is to bump the runtime to 0.16.0, which ships nodejs async + memory-management fixes, extension ABI bump, and macOS Intel binaries. Adopting 0.16.0 cleanly required three supporting changes; without them the upgrade itself regresses other paths: 1. maxDBSize must be passed explicitly. 0.16.0 keeps the upstream JSDoc note that the default 0 is "introduced temporarily for now to get around with the default 8 TB mmap address space limit some environment". Constrained CI runners and laptops cannot reserve 8 TB and crash with "Buffer manager exception: Mmap for size 8796093022208 failed." A new gitnexus/src/core/lbug/lbug-config.ts centralises a 16 GiB default (overridable via GITNEXUS_LBUG_MAX_DB_SIZE) and every Database() construction site now passes it. 2. enableCompression default flipped from false to true in 0.16.0. Every Database() call site is updated to pass false explicitly so existing GitNexus indexes keep the same wire format. 3. Bridge DB sidecar files (.wal, .shadow). 0.16.0 enforces a database-id check on .wal / .shadow sidecars and rejects opens whose sidecars belong to a different base name. writeBridge now (a) cleans the full sidecar set when removing the tmp slot, (b) renames .wal / .shadow alongside the main file during the atomic .tmp -> .lbug swap, and (c) wraps openBridgeDbReadOnly in a bounded retry on transient Win32-Error-33 lock errors. Eager db.init() / conn.init() forces the lazy native handle to surface lock contention at the retry site. Known limitation (not a regression): on Windows the 0.16.0 native binary does not release the OS file lock until the process exits, so the close-then-reopen-same-process pattern raises Error 33 after the first close. Production paths (analyze / serve / mcp each open the DB exactly once per process) are unaffected, but eight tests that exercise the pattern are guarded with a process.platform === 'win32' skip; CI's Linux + macOS shards exercise them as before. Tracking upstream: kuzudb/kuzu#3872 / #3883 / #4730. Closes #1136 #1154 #1160 #1162 #1178 #1195 #1196 #1199 #1204 #1206 Refs #1209 (supersedes — Dependabot bump without the supporting fixes) Made-with: Cursor * fix(test): isolate LadybugDB native test state Use per-suite LadybugDB databases in integration helpers so test forks do not reopen a database created by Vitest global setup, and centralize Windows-tolerant native temp cleanup for bridge tests. * fix(lbug): avoid bridge existence reopen Reuse the built LadybugDB config in the extension installer and avoid native close/reopen cycles when checking bridge existence on Windows. Made-with: Cursor * chore(docs): exclude local lbug plan Keep the refactor planning note out of the PR while leaving the ignored local copy on disk. Made-with: Cursor * refactor(lbug): centralize database construction Route LadybugDB opens through shared helpers so native constructor defaults stay consistent across core, pool, bridge, and extension install paths. Made-with: Cursor --------- Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
190 lines
6.2 KiB
TypeScript
190 lines
6.2 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import fsp from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import os from 'node:os';
|
|
import { cleanupTempDir } from '../../helpers/test-db.js';
|
|
import {
|
|
writeBridge,
|
|
openBridgeDbReadOnly,
|
|
queryBridge,
|
|
closeBridgeDb,
|
|
} from '../../../src/core/group/bridge-db.js';
|
|
import type { CrossLink } from '../../../src/core/group/types.js';
|
|
import { makeContract } from './fixtures.js';
|
|
|
|
/**
|
|
* See bridge-db.test.ts header for context: LadybugDB 0.16.0 cannot release
|
|
* Windows file locks until process exit, so any close-then-reopen pattern
|
|
* (writeBridge → openBridgeDbReadOnly) raises Win32 Error 33 in-process.
|
|
* Production paths are unaffected (single open per process).
|
|
*/
|
|
const itLbugReopen = process.platform === 'win32' ? it.skip : it;
|
|
|
|
describe('bridge-db edge cases', () => {
|
|
let tmpDir: string;
|
|
|
|
beforeEach(async () => {
|
|
tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'bridge-edge-'));
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await cleanupTempDir(tmpDir);
|
|
});
|
|
|
|
it('test_openBridgeDbReadOnly_version_gate_returns_null_for_incompatible', async () => {
|
|
// Create a dummy bridge.lbug file so the access check passes
|
|
await fsp.writeFile(path.join(tmpDir, 'bridge.lbug'), 'dummy');
|
|
// Write meta.json with an incompatible version (999)
|
|
await fsp.writeFile(
|
|
path.join(tmpDir, 'meta.json'),
|
|
JSON.stringify({ version: 999, generatedAt: '', missingRepos: [] }),
|
|
);
|
|
|
|
const handle = await openBridgeDbReadOnly(tmpDir);
|
|
expect(handle).toBeNull();
|
|
});
|
|
|
|
itLbugReopen('test_openBridgeDbReadOnly_bak_recovery_restores_bridge', async () => {
|
|
// Write a valid bridge
|
|
await writeBridge(tmpDir, {
|
|
contracts: [makeContract()],
|
|
crossLinks: [],
|
|
repoSnapshots: {},
|
|
missingRepos: [],
|
|
});
|
|
// Move bridge.lbug → bridge.lbug.bak (simulating interrupted swap)
|
|
const dbPath = path.join(tmpDir, 'bridge.lbug');
|
|
const bakPath = path.join(tmpDir, 'bridge.lbug.bak');
|
|
await fsp.rename(dbPath, bakPath);
|
|
|
|
// openBridgeDbReadOnly should auto-recover from .bak
|
|
const handle = await openBridgeDbReadOnly(tmpDir);
|
|
expect(handle).not.toBeNull();
|
|
const rows = await queryBridge<{ repo: string }>(
|
|
handle!,
|
|
'MATCH (c:Contract) RETURN c.repo AS repo',
|
|
);
|
|
expect(rows).toHaveLength(1);
|
|
await closeBridgeDb(handle!);
|
|
});
|
|
|
|
itLbugReopen('test_writeBridge_crossLink_with_missing_to_node_silently_skipped', async () => {
|
|
const provider = makeContract({ repo: 'backend', role: 'provider' });
|
|
const consumer = makeContract({
|
|
repo: 'frontend',
|
|
role: 'consumer',
|
|
symbolRef: { filePath: 'src/api.ts', name: 'fetchUsers' },
|
|
symbolName: 'fetchUsers',
|
|
});
|
|
// CrossLink referencing a 'to' endpoint that doesn't match any contract node
|
|
const link: CrossLink = {
|
|
from: {
|
|
repo: 'frontend',
|
|
symbolUid: '',
|
|
symbolRef: { filePath: 'src/api.ts', name: 'fetchUsers' },
|
|
},
|
|
to: {
|
|
repo: 'nonexistent-repo',
|
|
symbolUid: 'uid-missing',
|
|
symbolRef: { filePath: 'src/missing.ts', name: 'missingFn' },
|
|
},
|
|
type: 'http',
|
|
contractId: 'http::GET::/api/users',
|
|
matchType: 'exact',
|
|
confidence: 1.0,
|
|
};
|
|
|
|
// Should not throw — the link is silently skipped
|
|
await writeBridge(tmpDir, {
|
|
contracts: [provider, consumer],
|
|
crossLinks: [link],
|
|
repoSnapshots: {},
|
|
missingRepos: [],
|
|
});
|
|
|
|
const handle = await openBridgeDbReadOnly(tmpDir);
|
|
expect(handle).not.toBeNull();
|
|
// No cross-links should exist since 'to' node was missing
|
|
const rows = await queryBridge<{ matchType: string }>(
|
|
handle!,
|
|
'MATCH (a:Contract)-[l:ContractLink]->(b:Contract) RETURN l.matchType AS matchType',
|
|
);
|
|
expect(rows).toHaveLength(0);
|
|
// But contracts should still be present
|
|
const contractRows = await queryBridge<{ repo: string }>(
|
|
handle!,
|
|
'MATCH (c:Contract) RETURN c.repo AS repo',
|
|
);
|
|
expect(contractRows).toHaveLength(2);
|
|
await closeBridgeDb(handle!);
|
|
});
|
|
|
|
itLbugReopen(
|
|
'test_writeBridge_manifest_grpc_link_with_symbol_uids_persists_queryable_contract_edge',
|
|
async () => {
|
|
const provider = makeContract({
|
|
contractId: 'grpc::auth.AuthService/Login',
|
|
type: 'grpc',
|
|
role: 'provider',
|
|
repo: 'platform/auth',
|
|
symbolUid: 'uid-auth-login',
|
|
symbolRef: { filePath: 'src/auth.proto', name: 'Login' },
|
|
symbolName: 'auth.AuthService/Login',
|
|
});
|
|
const consumer = makeContract({
|
|
contractId: 'grpc::auth.AuthService/Login',
|
|
type: 'grpc',
|
|
role: 'consumer',
|
|
repo: 'platform/orders',
|
|
symbolUid: 'uid-orders-client',
|
|
symbolRef: { filePath: 'src/client.ts', name: 'AuthServiceClient' },
|
|
symbolName: 'auth.AuthService/Login',
|
|
});
|
|
const link: CrossLink = {
|
|
from: {
|
|
repo: 'platform/orders',
|
|
symbolUid: 'uid-orders-client',
|
|
symbolRef: { filePath: 'src/client.ts', name: 'AuthServiceClient' },
|
|
},
|
|
to: {
|
|
repo: 'platform/auth',
|
|
symbolUid: 'uid-auth-login',
|
|
symbolRef: { filePath: 'src/auth.proto', name: 'Login' },
|
|
},
|
|
type: 'grpc',
|
|
contractId: 'grpc::auth.AuthService/Login',
|
|
matchType: 'manifest',
|
|
confidence: 1.0,
|
|
};
|
|
|
|
await writeBridge(tmpDir, {
|
|
contracts: [provider, consumer],
|
|
crossLinks: [link],
|
|
repoSnapshots: {},
|
|
missingRepos: [],
|
|
});
|
|
|
|
const handle = await openBridgeDbReadOnly(tmpDir);
|
|
expect(handle).not.toBeNull();
|
|
const rows = await queryBridge<{
|
|
contractId: string;
|
|
matchType: string;
|
|
fromRepo: string;
|
|
toRepo: string;
|
|
}>(
|
|
handle!,
|
|
`MATCH (a:Contract)-[l:ContractLink]->(b:Contract)
|
|
RETURN l.contractId AS contractId, l.matchType AS matchType, l.fromRepo AS fromRepo, l.toRepo AS toRepo`,
|
|
);
|
|
expect(rows).toEqual([
|
|
{
|
|
contractId: 'grpc::auth.AuthService/Login',
|
|
matchType: 'manifest',
|
|
fromRepo: 'platform/orders',
|
|
toRepo: 'platform/auth',
|
|
},
|
|
]);
|
|
await closeBridgeDb(handle!);
|
|
},
|
|
);
|
|
});
|