mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-09 03:17:54 +00:00
* fix(lbug): resolve non-ASCII paths to 8.3 short form on Windows (#1811) KuzuDB's native C++ layer uses ANSI file APIs (fopen) on Windows. When the repo path contains CJK or other non-ASCII characters, the UTF-8 bytes from Node.js are misinterpreted as the system's Active Code Page (e.g. GBK), producing a garbled path — "Error 3: The system cannot find the path specified." Add `toNativeSafePath()` which converts non-ASCII paths to their Windows 8.3 short-name form (all-ASCII) before passing them to the native layer. Applied to both the database open path and the COPY CSV paths. No-ops on non-Windows and on all-ASCII paths. Closes #1811 * test(lbug): add unit + integration tests for non-ASCII path handling (#1811) - Unit tests for toNativeSafePath: ASCII passthrough, non-Windows no-op, Windows short-path conversion, nonexistent-path fallback - Integration test: full initLbug + loadGraphToLbug round-trip with CJK characters in the storage path — runs on all platforms - Fix toNativeSafePath to reject cmd.exe output containing '?' chars (replacement for unrepresentable Unicode in the console code page) - Register integration test in vitest lbug-db project and cross-platform-tests.ts matrix * chore(autofix): apply prettier + eslint fixes via /autofix command * feat(lbug): junction fallback, tmpdir CSV staging, pool-adapter coverage (#1811) U1+U4: toNativeSafePath now tries 8.3 short path → NTFS junction fallback → diagnostic warning. Junctions target path.dirname(p) and reconstruct the leaf. Handles EEXIST races. Registers cleanup on exit/SIGTERM/SIGINT. Orphan scan on first call removes stale junctions from prior crashes. U2: loadGraphToLbug redirects csvDir to os.tmpdir() when storagePath contains non-ASCII on Windows, avoiding non-ASCII characters in COPY FROM paths entirely. U3: All 4 createLbugDatabase call sites in pool-adapter.ts now wrap dbPath with toNativeSafePath. * fix(test): fix CI failures from toNativeSafePath addition (#1811) - Fix lbug-non-ascii-path integration test: use CodeRelation (actual relationship table name) instead of CALLS - Add toNativeSafePath to lbug-config.js mocks in pool-wal-recovery and lbug-pool-win-fts-probe tests — pool-adapter now imports it * fix(lbug): sanitize path before cmd.exe shell expansion (CodeQL) Reject paths containing cmd.exe metacharacters (" % | & < > ^) before interpolating into the `for %I` short-path command. Prevents command injection via crafted path names. * fix(lbug): address code review findings in non-ASCII path implementation - U1: Use process.exit(0) on Windows instead of process.kill re-raise (SIGTERM forcefully kills on Windows, handlers never fire) - U2: Pass safePath to openWithLockRetry so sidecar sweep targets the path KuzuDB actually opened, not the original non-ASCII path - U3: Skip junction creation in worker threads (isMainThread guard) to prevent junction leaks from pool-adapter workers - U4: Replace existsSync with lstatSync in orphan scan to avoid 30s blocking on unreachable UNC network targets * chore(autofix): apply prettier + eslint fixes via /autofix command * fix(lbug): correct SIGTERM exit code and run Prettier (#1811) - Use exit code 143 (SIGTERM) / 130 (SIGINT) on Windows instead of 0 so termination is not masked as success - Run Prettier to fix formatting (CI Gate blocker) * fix(lbug): eliminate CodeQL command-injection taint in tryShortPath Pass the path via GITNEXUS_SP environment variable instead of interpolating it into the cmd.exe command string. The FOR loop reads %GITNEXUS_SP% from the environment, so the command text is entirely static — no user-controlled data in the shell command. Also removes CMD_UNSAFE_RE since the env var approach makes character-level sanitization unnecessary. --------- Co-authored-by: Test <test@example.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
133 lines
5.6 KiB
TypeScript
133 lines
5.6 KiB
TypeScript
/**
|
|
* 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(),
|
|
toNativeSafePath: vi.fn((p: string) => p),
|
|
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);
|
|
});
|
|
});
|