mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-08 22:22:52 +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.
211 lines
7.9 KiB
TypeScript
211 lines
7.9 KiB
TypeScript
/**
|
|
* P0 Integration Tests: LadybugDB Connection Pool
|
|
*
|
|
* Tests: initLbug, executeQuery, executeParameterized, closeLbug lifecycle
|
|
* Covers hardening fixes: parameterized queries, query timeout,
|
|
* waiter queue timeout, idle eviction guards, stdout silencing race
|
|
*/
|
|
import { describe, it, expect, afterEach } from 'vitest';
|
|
import {
|
|
initLbug,
|
|
executeQuery,
|
|
executeParameterized,
|
|
closeLbug,
|
|
isLbugReady,
|
|
} from '../../src/mcp/core/lbug-adapter.js';
|
|
import { withTestLbugDB } from '../helpers/test-indexed-db.js';
|
|
|
|
const POOL_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)`,
|
|
];
|
|
|
|
// ─── Pool lifecycle tests — test the pool adapter API directly ───────
|
|
|
|
withTestLbugDB(
|
|
'lbug-pool',
|
|
(handle) => {
|
|
afterEach(async () => {
|
|
try {
|
|
await closeLbug('test-repo');
|
|
} catch {
|
|
/* best-effort */
|
|
}
|
|
try {
|
|
await closeLbug('repo1');
|
|
} catch {
|
|
/* best-effort */
|
|
}
|
|
try {
|
|
await closeLbug('repo2');
|
|
} catch {
|
|
/* best-effort */
|
|
}
|
|
try {
|
|
await closeLbug('');
|
|
} catch {
|
|
/* best-effort */
|
|
}
|
|
});
|
|
|
|
// ─── Lifecycle: init → query → close ─────────────────────────────────
|
|
|
|
describe('pool lifecycle', () => {
|
|
it('initLbug + executeQuery + closeLbug', async () => {
|
|
await initLbug('test-repo', handle.dbPath);
|
|
expect(isLbugReady('test-repo')).toBe(true);
|
|
|
|
const rows = await executeQuery('test-repo', 'MATCH (n:Function) RETURN n.name AS name');
|
|
expect(rows.length).toBeGreaterThanOrEqual(2);
|
|
const names = rows.map((r: any) => r.name);
|
|
expect(names).toContain('main');
|
|
expect(names).toContain('helper');
|
|
|
|
await closeLbug('test-repo');
|
|
expect(isLbugReady('test-repo')).toBe(false);
|
|
});
|
|
|
|
it('initLbug reuses existing pool entry', async () => {
|
|
await initLbug('test-repo', handle.dbPath);
|
|
await initLbug('test-repo', handle.dbPath); // second call should be no-op
|
|
expect(isLbugReady('test-repo')).toBe(true);
|
|
});
|
|
|
|
it('closeLbug is idempotent', async () => {
|
|
await initLbug('test-repo', handle.dbPath);
|
|
await closeLbug('test-repo');
|
|
await closeLbug('test-repo'); // second close should not throw
|
|
expect(isLbugReady('test-repo')).toBe(false);
|
|
});
|
|
|
|
it('closeLbug with no args closes all repos', async () => {
|
|
await initLbug('repo1', handle.dbPath);
|
|
await initLbug('repo2', handle.dbPath);
|
|
expect(isLbugReady('repo1')).toBe(true);
|
|
expect(isLbugReady('repo2')).toBe(true);
|
|
|
|
await closeLbug();
|
|
expect(isLbugReady('repo1')).toBe(false);
|
|
expect(isLbugReady('repo2')).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ─── Parameterized queries ───────────────────────────────────────────
|
|
|
|
describe('executeParameterized', () => {
|
|
it('works with parameterized query', async () => {
|
|
await initLbug('test-repo', handle.dbPath);
|
|
const rows = await executeParameterized(
|
|
'test-repo',
|
|
'MATCH (n:Function) WHERE n.name = $name RETURN n.name AS name',
|
|
{ name: 'main' },
|
|
);
|
|
expect(rows).toHaveLength(1);
|
|
expect(rows[0].name).toBe('main');
|
|
});
|
|
|
|
it('injection attempt is harmless with parameterized query', async () => {
|
|
await initLbug('test-repo', handle.dbPath);
|
|
const rows = await executeParameterized(
|
|
'test-repo',
|
|
'MATCH (n:Function) WHERE n.name = $name RETURN n.name AS name',
|
|
{ name: "' OR 1=1 --" }, // SQL/Cypher injection attempt
|
|
);
|
|
// Should return 0 rows, not all rows
|
|
expect(rows).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
// ─── Error handling ──────────────────────────────────────────────────
|
|
|
|
describe('error handling', () => {
|
|
it('throws when querying uninitialized repo', async () => {
|
|
await expect(executeQuery('nonexistent-repo', 'MATCH (n) RETURN n')).rejects.toThrow(
|
|
/not initialized/,
|
|
);
|
|
});
|
|
|
|
it('throws when db path does not exist', async () => {
|
|
await expect(initLbug('bad-repo', '/nonexistent/path/lbug')).rejects.toThrow();
|
|
});
|
|
|
|
it('read-only mode: write query throws', async () => {
|
|
await initLbug('test-repo', handle.dbPath);
|
|
await expect(
|
|
executeQuery(
|
|
'test-repo',
|
|
"CREATE (n:Function {id: 'new', name: 'new', filePath: '', startLine: 0, endLine: 0, isExported: false, content: '', description: ''})",
|
|
),
|
|
).rejects.toThrow();
|
|
});
|
|
});
|
|
|
|
// ─── Relationship queries ────────────────────────────────────────────
|
|
|
|
describe('relationship queries', () => {
|
|
it('can query relationships', async () => {
|
|
await initLbug('test-repo', handle.dbPath);
|
|
const rows = await executeQuery(
|
|
'test-repo',
|
|
`MATCH (a:Function)-[r:CodeRelation {type: 'CALLS'}]->(b:Function) RETURN a.name AS caller, b.name AS callee`,
|
|
);
|
|
expect(rows.length).toBeGreaterThanOrEqual(1);
|
|
const row = rows.find((r: any) => r.caller === 'main');
|
|
expect(row).toBeDefined();
|
|
expect(row.callee).toBe('helper');
|
|
});
|
|
});
|
|
|
|
// ─── Unhappy paths ──────────────────────────────────────────────────
|
|
|
|
describe('unhappy paths', () => {
|
|
it('executeParameterized throws when repo is not initialized', async () => {
|
|
await expect(executeParameterized('ghost-repo', 'MATCH (n) RETURN n', {})).rejects.toThrow(
|
|
/not initialized/,
|
|
);
|
|
});
|
|
|
|
it('executeQuery rejects invalid Cypher syntax', async () => {
|
|
await initLbug('test-repo', handle.dbPath);
|
|
await expect(executeQuery('test-repo', 'THIS IS NOT CYPHER')).rejects.toThrow();
|
|
});
|
|
|
|
it('executeParameterized rejects when referenced parameter is missing', async () => {
|
|
await initLbug('test-repo', handle.dbPath);
|
|
await expect(
|
|
executeParameterized('test-repo', 'MATCH (n:Function) WHERE n.name = $name RETURN n', {
|
|
wrong_param: 'main',
|
|
}),
|
|
).rejects.toThrow();
|
|
});
|
|
|
|
it('closeLbug with unknown repoId does not throw', async () => {
|
|
await expect(closeLbug('never-existed-repo')).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('isLbugReady returns false for unknown repoId', () => {
|
|
expect(isLbugReady('never-existed-repo')).toBe(false);
|
|
});
|
|
|
|
it('initLbug with empty string repoId stores entry under empty key', async () => {
|
|
await initLbug('', handle.dbPath);
|
|
expect(isLbugReady('')).toBe(true);
|
|
await closeLbug('');
|
|
expect(isLbugReady('')).toBe(false);
|
|
});
|
|
|
|
it('executeQuery with empty query string rejects', async () => {
|
|
await initLbug('test-repo', handle.dbPath);
|
|
await expect(executeQuery('test-repo', '')).rejects.toThrow();
|
|
});
|
|
});
|
|
},
|
|
{
|
|
seed: POOL_SEED_DATA,
|
|
poolAdapter: true,
|
|
},
|
|
);
|