mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
* feat: configure prettier with pre-commit hook integration Add prettier, lint-staged, and prettier-plugin-tailwindcss at the repo root with husky pre-commit hook integration. Moves husky from gitnexus/ to root package.json for reliable hook installation. - Root package.json with prepare/format/format:check scripts - .prettierrc with endOfLine:lf and tailwindStylesheet for TW v4 - .prettierignore excluding fixtures, vendor, generated, *.d.ts, *.md - .gitattributes enforcing LF line endings for Windows consistency - Pre-commit hook uses direct node_modules/.bin/ paths (no npx) * style: apply prettier formatting to entire codebase One-time bulk format. No logic changes. Use .git-blame-ignore-revs to skip this commit in git blame. * chore: add .git-blame-ignore-revs for prettier format commit * perf: pre-commit hook runs only tests related to staged files Use vitest --related to scope test execution to tests that import the changed files, instead of running the full suite on every commit. * perf: remove vitest from pre-commit hook, keep in CI only Pre-commit now runs lint-staged + tsc only. Tests run in CI (ci-tests.yml) where they belong — keeps commits fast. * ci: add prettier format check to quality workflow PRs will now fail if code isn't formatted with prettier.
180 lines
7.1 KiB
TypeScript
180 lines
7.1 KiB
TypeScript
/**
|
|
* Integration Tests: LadybugDB Connection Pool — Parallel Stability
|
|
*
|
|
* Tests concurrency fixes from PR #349:
|
|
* - Pre-warmed pool handles max parallel queries
|
|
* - Waiter queue drains under overload
|
|
* - Concurrent initLbug deduplication
|
|
* - stdout.write restoration after parallel operations
|
|
* - No connection leaks over sequential workloads
|
|
* - Atomic pool entry visibility (pool.set last)
|
|
* - Mixed query types at full concurrency
|
|
*
|
|
* Connection budget: LadybugDB's native mmap budget caps at ~56
|
|
* simultaneous Connection objects per process. Tests that only need
|
|
* a ready pool share a single 'shared-repo' init (8 connections) to
|
|
* stay well under the limit.
|
|
*/
|
|
import { it, expect, afterAll } from 'vitest';
|
|
import {
|
|
initLbug,
|
|
executeQuery,
|
|
executeParameterized,
|
|
closeLbug,
|
|
isLbugReady,
|
|
} from '../../src/mcp/core/lbug-adapter.js';
|
|
import { withTestLbugDB } from '../helpers/test-indexed-db.js';
|
|
|
|
const SEED_DATA = [
|
|
`CREATE (f:File {id: 'file:index.ts', name: 'index.ts', filePath: 'src/index.ts', content: ''})`,
|
|
`CREATE (fn:Function {id: 'func:main', name: 'main', filePath: 'src/index.ts', startLine: 1, endLine: 10, isExported: true, content: '', description: ''})`,
|
|
`CREATE (fn2:Function {id: 'func:helper', name: 'helper', filePath: 'src/utils.ts', startLine: 1, endLine: 5, isExported: true, content: '', description: ''})`,
|
|
`MATCH (a:Function), (b:Function)
|
|
WHERE a.id = 'func:main' AND b.id = 'func:helper'
|
|
CREATE (a)-[:CodeRelation {type: 'CALLS', confidence: 1.0, reason: 'direct', step: 0}]->(b)`,
|
|
];
|
|
|
|
// ─── Shared-pool tests: reuse one init across 5 tests (8 connections total) ──
|
|
|
|
withTestLbugDB(
|
|
'pool-stability',
|
|
(handle) => {
|
|
const REPO = 'shared-par';
|
|
let inited = false;
|
|
|
|
const ensurePool = async () => {
|
|
if (!inited) {
|
|
await initLbug(REPO, handle.dbPath);
|
|
inited = true;
|
|
}
|
|
};
|
|
|
|
afterAll(async () => {
|
|
try {
|
|
await closeLbug(REPO);
|
|
} catch {
|
|
/* best-effort */
|
|
}
|
|
});
|
|
|
|
it('8 simultaneous queries complete without crashes', async () => {
|
|
await ensurePool();
|
|
const queries = Array.from({ length: 8 }, () =>
|
|
executeQuery(REPO, 'MATCH (n:Function) RETURN n.name AS name'),
|
|
);
|
|
const results = await Promise.all(queries);
|
|
expect(results).toHaveLength(8);
|
|
for (const rows of results) {
|
|
expect(rows.length).toBeGreaterThanOrEqual(2);
|
|
}
|
|
});
|
|
|
|
it('12 parallel queries overflow into waiter queue and all complete', async () => {
|
|
await ensurePool();
|
|
const queries = Array.from({ length: 12 }, () =>
|
|
executeQuery(REPO, 'MATCH (n:Function) RETURN n.name AS name'),
|
|
);
|
|
const results = await Promise.all(queries);
|
|
expect(results).toHaveLength(12);
|
|
for (const rows of results) {
|
|
expect(rows.length).toBeGreaterThanOrEqual(2);
|
|
}
|
|
});
|
|
|
|
it('process.stdout.write is functional after init and parallel queries', async () => {
|
|
await ensurePool();
|
|
const queries = Array.from({ length: 8 }, () =>
|
|
executeQuery(REPO, 'MATCH (n:Function) RETURN n.name AS name'),
|
|
);
|
|
await Promise.all(queries);
|
|
// stdout.write should be the real function, not the silenced no-op
|
|
expect(typeof process.stdout.write).toBe('function');
|
|
});
|
|
|
|
it('50 sequential queries do not leak connections', async () => {
|
|
await ensurePool();
|
|
for (let i = 0; i < 50; i++) {
|
|
await executeQuery(REPO, 'MATCH (n:Function) RETURN n.name AS name');
|
|
}
|
|
// If connections leaked, these 8 parallel queries would exhaust the pool
|
|
const queries = Array.from({ length: 8 }, () =>
|
|
executeQuery(REPO, 'MATCH (n:Function) RETURN n.name AS name'),
|
|
);
|
|
const results = await Promise.all(queries);
|
|
expect(results).toHaveLength(8);
|
|
});
|
|
|
|
it('mixed executeQuery + executeParameterized at full concurrency', async () => {
|
|
await ensurePool();
|
|
const queries = [
|
|
executeQuery(REPO, 'MATCH (n:Function) RETURN n.name AS name'),
|
|
executeParameterized(
|
|
REPO,
|
|
'MATCH (n:Function) WHERE n.name = $name RETURN n.name AS name',
|
|
{ name: 'main' },
|
|
),
|
|
executeQuery(REPO, 'MATCH (n:Function) RETURN n.name AS name'),
|
|
executeParameterized(
|
|
REPO,
|
|
'MATCH (n:Function) WHERE n.name = $name RETURN n.name AS name',
|
|
{ name: 'helper' },
|
|
),
|
|
executeQuery(REPO, 'MATCH (n:Function) RETURN n.name AS name'),
|
|
executeParameterized(
|
|
REPO,
|
|
'MATCH (n:Function) WHERE n.name = $name RETURN n.name AS name',
|
|
{ name: 'main' },
|
|
),
|
|
executeQuery(REPO, 'MATCH (n:Function) RETURN n.name AS name'),
|
|
executeQuery(REPO, 'MATCH (n:Function) RETURN n.name AS name'),
|
|
];
|
|
const results = await Promise.all(queries);
|
|
expect(results).toHaveLength(8);
|
|
});
|
|
|
|
// ─── Regression tests for #308 / #314 / #347 ──────────────────────────
|
|
// The impact command's enrichment phase runs 3 concurrent queries via
|
|
// Promise.all (local-backend.ts:1415). Before PR #349's pool pre-warming,
|
|
// this triggered lazy createConnection → silenceStdout → SIGSEGV.
|
|
|
|
it('3 concurrent queries via Promise.all (impact enrichment pattern, #308/#314/#347)', async () => {
|
|
await ensurePool();
|
|
// Mirrors the exact Promise.all pattern from local-backend.ts:1415
|
|
// that caused SIGSEGV in issues #308, #314, #347 before pool pre-warming.
|
|
const [r1, r2, r3] = await Promise.all([
|
|
executeQuery(REPO, 'MATCH (n:Function) RETURN n.name AS name LIMIT 5').catch(() => []),
|
|
executeQuery(REPO, 'MATCH (n:Function) RETURN n.id AS id LIMIT 5').catch(() => []),
|
|
executeQuery(REPO, 'MATCH ()-[r:CodeRelation]->() RETURN r.type AS type LIMIT 5').catch(
|
|
() => [],
|
|
),
|
|
]);
|
|
expect(r1.length).toBeGreaterThanOrEqual(1);
|
|
expect(r2.length).toBeGreaterThanOrEqual(1);
|
|
expect(r3.length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
// ─── Fresh-state tests: need their own init ──────────────────────────
|
|
|
|
it('concurrent initLbug calls for the same repoId deduplicate', async () => {
|
|
const promises = Array.from({ length: 6 }, () => initLbug('dedup-repo', handle.dbPath));
|
|
await Promise.all(promises);
|
|
expect(isLbugReady('dedup-repo')).toBe(true);
|
|
const rows = await executeQuery('dedup-repo', 'MATCH (n:Function) RETURN n.name AS name');
|
|
expect(rows.length).toBeGreaterThanOrEqual(2);
|
|
await closeLbug('dedup-repo');
|
|
});
|
|
|
|
it('pool entry is not visible until initLbug fully resolves', async () => {
|
|
const initPromise = initLbug('atomic-repo', handle.dbPath);
|
|
// Synchronously, pool should NOT be ready (pool.set is the last operation)
|
|
expect(isLbugReady('atomic-repo')).toBe(false);
|
|
await initPromise;
|
|
expect(isLbugReady('atomic-repo')).toBe(true);
|
|
await closeLbug('atomic-repo');
|
|
});
|
|
},
|
|
{
|
|
seed: SEED_DATA,
|
|
poolAdapter: true,
|
|
},
|
|
);
|