mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-23 00:41:36 +00:00
Merge branch 'main' into feat/Desktop-app
This commit is contained in:
commit
d88092c85c
3 changed files with 194 additions and 12 deletions
6
eval/uv.lock
generated
6
eval/uv.lock
generated
|
|
@ -760,11 +760,11 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
version = "3.15"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@
|
|||
*/
|
||||
|
||||
import fs from 'fs/promises';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import lbug from '@ladybugdb/core';
|
||||
import { isReadOnlyDbError, loadFTSExtension } from './lbug-adapter.js';
|
||||
import {
|
||||
|
|
@ -24,6 +26,42 @@ import {
|
|||
WAL_RECOVERY_SUGGESTION,
|
||||
} from './lbug-config.js';
|
||||
|
||||
/**
|
||||
* Probe whether a Windows FTS extension binary is locally installed under
|
||||
* ~/.lbdb/extension/<any-version>/win_amd64/fts/. Returns true on the first
|
||||
* version dir whose libfts.lbug_extension exists on disk; false if the
|
||||
* extension root is missing or contains no FTS binary.
|
||||
*
|
||||
* Gates the Windows skip-FTS-load guard below so we only skip the load
|
||||
* when no extension binary is present. When at least one binary exists,
|
||||
* loadFTSExtension is called with policy: 'load-only' — LadybugDB resolves
|
||||
* LOAD EXTENSION fts to its version-specific path internally, and the
|
||||
* ExtensionManager's tryLoad try/catch handles version-mismatch errors
|
||||
* cleanly without ever attempting dlopen of a stale binary. The install
|
||||
* path that the #1199/#1217 SIGSEGV documented is never exercised at
|
||||
* query time.
|
||||
*
|
||||
* Exported so unit tests can exercise the probe directly against a
|
||||
* temp-dir plus spied `os.homedir()` — see lbug-pool-win-fts-probe.test.ts.
|
||||
*/
|
||||
export async function hasLocalWinFtsExtension(): Promise<boolean> {
|
||||
try {
|
||||
const extRoot = path.join(os.homedir(), '.lbdb', 'extension');
|
||||
const versions = await fs.readdir(extRoot);
|
||||
for (const v of versions) {
|
||||
try {
|
||||
await fs.stat(path.join(extRoot, v, 'win_amd64', 'fts', 'libfts.lbug_extension'));
|
||||
return true;
|
||||
} catch {
|
||||
/* missing for this version, keep looking */
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* no .lbdb/extension dir */
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Per-repo pool: one Database, many Connections */
|
||||
interface PoolEntry {
|
||||
db: lbug.Database;
|
||||
|
|
@ -423,14 +461,24 @@ async function doInitLbug(repoId: string, dbPath: string): Promise<void> {
|
|||
// install; analyze owns extension installation. If LOAD fails, search
|
||||
// features degrade gracefully and the user-facing query path proceeds.
|
||||
if (!shared.ftsLoaded) {
|
||||
// Windows guard: LOAD EXTENSION fts crashes with SIGSEGV on Windows when
|
||||
// the FTS extension binary is not installed locally (@ladybugdb/core native
|
||||
// bug — the extension loader hits an unhandled error path that signals SIGSEGV
|
||||
// rather than throwing a JS exception, so try/catch cannot protect here).
|
||||
// Skip the load on Windows; bm25-index.js catches the resulting Kuzu catalog
|
||||
// errors and returns empty BM25 results gracefully. Graph queries are unaffected.
|
||||
// Windows guard: LOAD EXTENSION fts crashes with SIGSEGV on Windows during
|
||||
// *install* — the @ladybugdb/core out-of-process installer hits an unhandled
|
||||
// error path that signals SIGSEGV instead of throwing (see #1199, #1217).
|
||||
// The previous unconditional skip was over-broad: it also disabled FTS on
|
||||
// hosts where the binary was already on disk and only needed LOAD, leaving
|
||||
// BM25 silently degraded with no error path (see #1690).
|
||||
//
|
||||
// Probe ~/.lbdb/extension/*/win_amd64/fts/ first. If any binary is on disk
|
||||
// we run loadFTSExtension(..., 'load-only'); the install path is never
|
||||
// exercised, and LadybugDB's version-specific resolution + ExtensionManager
|
||||
// try/catch handle stale/zero-byte siblings cleanly (verified empirically
|
||||
// on Win10 + Node 22.19 + gitnexus 1.6.5 + @ladybugdb/core 0.16.1). With
|
||||
// no binary at all, we fall back to the upstream skip so install-time
|
||||
// SIGSEGV continues to be avoided.
|
||||
if (process.platform === 'win32') {
|
||||
shared.ftsLoaded = true;
|
||||
shared.ftsLoaded = (await hasLocalWinFtsExtension())
|
||||
? await loadFTSExtension(available[0], { policy: 'load-only' })
|
||||
: true;
|
||||
} else {
|
||||
shared.ftsLoaded = await loadFTSExtension(available[0], { policy: 'load-only' });
|
||||
}
|
||||
|
|
@ -497,10 +545,12 @@ export async function initLbugWithDb(
|
|||
// Load FTS extension if not already loaded on this Database.
|
||||
// policy: 'load-only' — same contract as initLbug above; the read pool
|
||||
// must not block on a network install during query execution.
|
||||
// Windows guard: same SIGSEGV risk as doInitLbug above — skip on Windows.
|
||||
// Windows guard: same probe-then-load policy as doInitLbug above.
|
||||
if (!shared.ftsLoaded) {
|
||||
if (process.platform === 'win32') {
|
||||
shared.ftsLoaded = true;
|
||||
shared.ftsLoaded = (await hasLocalWinFtsExtension())
|
||||
? await loadFTSExtension(available[0], { policy: 'load-only' })
|
||||
: true;
|
||||
} else {
|
||||
shared.ftsLoaded = await loadFTSExtension(available[0], { policy: 'load-only' });
|
||||
}
|
||||
|
|
|
|||
132
gitnexus/test/unit/lbug-pool-win-fts-probe.test.ts
Normal file
132
gitnexus/test/unit/lbug-pool-win-fts-probe.test.ts
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
/**
|
||||
* Unit tests for the Windows FTS probe in pool-adapter.ts.
|
||||
*
|
||||
* Covers `hasLocalWinFtsExtension()` — the helper that gates the
|
||||
* Windows-only skip of `loadFTSExtension` in `doInitLbug` and
|
||||
* `initLbugWithDb`. Issue #1690 / PR #1692.
|
||||
*
|
||||
* The probe is exercised against a real temp filesystem with
|
||||
* `os.homedir()` spied to point at the tempdir. This tests the
|
||||
* actual fs surface (readdir/stat semantics, missing-dir behavior,
|
||||
* zero-byte file handling) rather than mocking fs internals.
|
||||
*
|
||||
* The Windows-branch conditional in `doInitLbug` / `initLbugWithDb`
|
||||
* is intentionally not unit-tested in isolation: those functions
|
||||
* require a fully constructed `lbug.Database` + `Connection` pool
|
||||
* and are exercised end-to-end by `test/integration/lbug-pool*.test.ts`
|
||||
* on the `windows-latest` matrix. The conditional itself is a single
|
||||
* expression — `(await hasLocalWinFtsExtension()) ? load : true` —
|
||||
* whose correctness reduces to the probe being correctly tested here.
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import fs from 'fs/promises';
|
||||
|
||||
// Stub out the LadybugDB native loader and its transitive importers so that
|
||||
// importing pool-adapter.ts in this unit test does not pull in the .node binary
|
||||
// (which is built by the postinstall script and is not always present in the
|
||||
// dev install used for unit tests).
|
||||
vi.mock('@ladybugdb/core', () => ({
|
||||
default: { Database: vi.fn(), Connection: vi.fn() },
|
||||
}));
|
||||
vi.mock('../../src/core/lbug/lbug-adapter.js', () => ({
|
||||
isReadOnlyDbError: vi.fn(() => false),
|
||||
loadFTSExtension: vi.fn(),
|
||||
}));
|
||||
vi.mock('../../src/core/lbug/lbug-config.js', () => ({
|
||||
createLbugDatabase: vi.fn(),
|
||||
isWalCorruptionError: vi.fn(() => false),
|
||||
WAL_RECOVERY_SUGGESTION: '',
|
||||
}));
|
||||
|
||||
import { hasLocalWinFtsExtension } from '../../src/core/lbug/pool-adapter.js';
|
||||
|
||||
describe('hasLocalWinFtsExtension', () => {
|
||||
let tmpHome: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tmpHome = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-fts-probe-'));
|
||||
vi.spyOn(os, 'homedir').mockReturnValue(tmpHome);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
await fs.rm(tmpHome, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('returns false when ~/.lbdb/extension does not exist', async () => {
|
||||
// tmpHome is empty; the probe should swallow the readdir ENOENT and return false.
|
||||
await expect(hasLocalWinFtsExtension()).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when ~/.lbdb/extension exists but has no version dirs', async () => {
|
||||
await fs.mkdir(path.join(tmpHome, '.lbdb', 'extension'), { recursive: true });
|
||||
await expect(hasLocalWinFtsExtension()).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when a single version dir contains the FTS binary', async () => {
|
||||
const ftsDir = path.join(tmpHome, '.lbdb', 'extension', '0.16.0', 'win_amd64', 'fts');
|
||||
await fs.mkdir(ftsDir, { recursive: true });
|
||||
await fs.writeFile(path.join(ftsDir, 'libfts.lbug_extension'), Buffer.from('mock-binary'));
|
||||
await expect(hasLocalWinFtsExtension()).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when the binary is a zero-byte stub (LOAD failure handled downstream)', async () => {
|
||||
// Empirically verified in #1690 thread: LadybugDB resolves LOAD EXTENSION fts to a
|
||||
// version-specific path internally and the ExtensionManager's tryLoad try/catch
|
||||
// catches the resulting load error cleanly. Probe is intentionally generous here;
|
||||
// safety lives in the loader, not the probe.
|
||||
const ftsDir = path.join(tmpHome, '.lbdb', 'extension', '0.16.0', 'win_amd64', 'fts');
|
||||
await fs.mkdir(ftsDir, { recursive: true });
|
||||
await fs.writeFile(path.join(ftsDir, 'libfts.lbug_extension'), '');
|
||||
await expect(hasLocalWinFtsExtension()).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when multiple version dirs exist and only one carries the binary', async () => {
|
||||
const versions = ['0.15.0', '0.16.0', '0.17.0'];
|
||||
for (const v of versions) {
|
||||
await fs.mkdir(path.join(tmpHome, '.lbdb', 'extension', v, 'win_amd64', 'fts'), {
|
||||
recursive: true,
|
||||
});
|
||||
}
|
||||
// Only 0.16.0 has the binary; the probe should keep iterating past empty siblings.
|
||||
await fs.writeFile(
|
||||
path.join(
|
||||
tmpHome,
|
||||
'.lbdb',
|
||||
'extension',
|
||||
'0.16.0',
|
||||
'win_amd64',
|
||||
'fts',
|
||||
'libfts.lbug_extension',
|
||||
),
|
||||
Buffer.from('mock-binary'),
|
||||
);
|
||||
await expect(hasLocalWinFtsExtension()).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when version dirs exist but none contain the binary', async () => {
|
||||
// Adversarial topology raised by #1690 review: tree exists (Nix store, Bazel
|
||||
// sandbox seeding, corporate MDM-prepopulated user dirs) but the actual
|
||||
// libfts.lbug_extension file is absent. Probe must distinguish file from dir.
|
||||
const versions = ['0.15.0', '0.16.0', '0.17.0'];
|
||||
for (const v of versions) {
|
||||
await fs.mkdir(path.join(tmpHome, '.lbdb', 'extension', v, 'win_amd64', 'fts'), {
|
||||
recursive: true,
|
||||
});
|
||||
}
|
||||
await expect(hasLocalWinFtsExtension()).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when fs.readdir throws (e.g. permission denied on the extension root)', async () => {
|
||||
// Cover the outer try/catch — any fs error walking the extension root is
|
||||
// treated as "no binary present", matching the upstream skip-guard intent.
|
||||
const eaccess = Object.assign(new Error('EACCES: permission denied'), {
|
||||
code: 'EACCES',
|
||||
}) as NodeJS.ErrnoException;
|
||||
vi.spyOn(fs, 'readdir').mockRejectedValue(eaccess);
|
||||
await expect(hasLocalWinFtsExtension()).resolves.toBe(false);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue