mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-10 03:27:59 +00:00
Some checks are pending
CI / quality (push) Waiting to run
CI / tests (push) Waiting to run
CI / e2e (push) Waiting to run
CI / scope-parity (push) Waiting to run
CI / Save PR Metadata (push) Blocked by required conditions
CI / CI Gate (push) Blocked by required conditions
Release Candidate / Check if release candidate should run (push) Waiting to run
Release Candidate / ci (push) Blocked by required conditions
Release Candidate / Publish release candidate to npm (push) Blocked by required conditions
Release Candidate / Build & Push RC Docker images (push) Blocked by required conditions
* Initial plan * feat: detect sibling-clone graph drift via remote URL fingerprint Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/e5decb67-7fec-40e7-b2a1-b5e94a0d393f Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> * test: address review feedback — fake commit, same-commit case, regex docs Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/e5decb67-7fec-40e7-b2a1-b5e94a0d393f Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> * fix(mcp): address review feedback — CI green, perf, dead branch, one-shot test Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/cc2259f7-94e4-4243-aaa9-e03b7c632d32 * Merge branch 'main' into copilot/fix-single-path-indexing-issue Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/5840b3dd-e879-4854-a067-d1622bec2634 Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> * Merge branch 'main' into copilot/fix-single-path-indexing-issue Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/9025262f-4dd4-4774-8f32-e14434100004 Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> * style: prettier format run-analyze.ts after merge with main Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/a7be18dd-102f-4a7b-ac56-53fbd414fe3b Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> * test: realpath both sides of cwdGitRoot assertion for Windows 8.3 short-name compat Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/b2a1c6a3-e454-4b87-b0e4-69d7c0d9a51b Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> * fix(test): use path-agnostic assertion for cwdGitRoot on Windows (#1015) git rev-parse --show-toplevel returns long path names on Windows while os.tmpdir() returns 8.3 short names. fs.realpathSync does not expand short names, so exact path comparison always fails on Windows CI runners. Replace with behavioral assertions instead. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <copilot-swe-agent[bot]@users.noreply.github.com> Co-authored-by: Gergő Magyar <gergomagyar@icloud.com> Co-authored-by: evolution <wjc163@sina.cn>
239 lines
8.8 KiB
TypeScript
239 lines
8.8 KiB
TypeScript
/**
|
|
* E2E Tests: Stale Data Detection + Sequential Enrichment Stability
|
|
*
|
|
* Validates the fixes in PR #396:
|
|
* 1. Sequential enrichment: impact() enrichment queries run without
|
|
* SIGSEGV on arm64 macOS (sequential on arm64, parallel elsewhere)
|
|
* 2. Consecutive tool stability: MCP server stays alive after 10+
|
|
* consecutive tool calls (no stdout corruption)
|
|
* 3. Watchdog guard: activeQueryCount prevents premature stdout restore
|
|
* 4. Stale data detection: ensureInitialized() detects meta.json changes
|
|
*
|
|
* All tests share one withTestLbugDB lifecycle to avoid cross-block
|
|
* DB closure issues (LadybugDB's shared global DB in a single fork).
|
|
*
|
|
* Issues: #285, #290, #292, #297
|
|
*/
|
|
import { describe, it, expect, afterAll } from 'vitest';
|
|
import fs from 'fs/promises';
|
|
import path from 'path';
|
|
import { initLbug, executeQuery, closeLbug } from '../../src/mcp/core/lbug-adapter.js';
|
|
import { withTestLbugDB } from '../helpers/test-indexed-db.js';
|
|
import {
|
|
LOCAL_BACKEND_SEED_DATA,
|
|
LOCAL_BACKEND_FTS_INDEXES,
|
|
} from '../fixtures/local-backend-seed.js';
|
|
import { LocalBackend } from '../../src/mcp/local/local-backend.js';
|
|
import { listRegisteredRepos } from '../../src/storage/repo-manager.js';
|
|
import { vi } from 'vitest';
|
|
|
|
vi.mock('../../src/storage/repo-manager.js', () => ({
|
|
listRegisteredRepos: vi.fn().mockResolvedValue([]),
|
|
cleanupOldKuzuFiles: vi.fn().mockResolvedValue({ found: false, needsReindex: false }),
|
|
findSiblingClones: vi.fn().mockResolvedValue([]),
|
|
}));
|
|
|
|
withTestLbugDB(
|
|
'staleness-and-stability',
|
|
(handle) => {
|
|
let backend: LocalBackend;
|
|
let storagePath: string;
|
|
|
|
// ─── Setup ─────────────────────────────────────────────────────────
|
|
describe('setup', () => {
|
|
it('initialize backend', async () => {
|
|
const ext = handle as typeof handle & { _backend?: LocalBackend };
|
|
if (!ext._backend) throw new Error('LocalBackend not initialized');
|
|
backend = ext._backend;
|
|
storagePath = handle.tmpHandle.dbPath;
|
|
});
|
|
});
|
|
|
|
// ─── Block 1: Sequential enrichment queries (#285, #290, #292) ─────
|
|
describe('impact enrichment queries run without crashes', () => {
|
|
it('impact with enrichment completes without SIGSEGV', async () => {
|
|
const result = await backend.callTool('impact', {
|
|
target: 'validate',
|
|
direction: 'upstream',
|
|
});
|
|
expect(result).not.toHaveProperty('error');
|
|
expect(result.impactedCount).toBeGreaterThanOrEqual(1);
|
|
expect(result).toHaveProperty('affected_processes');
|
|
expect(result).toHaveProperty('affected_modules');
|
|
});
|
|
|
|
it('impact with large maxDepth completes without crash', async () => {
|
|
const result = await backend.callTool('impact', {
|
|
target: 'login',
|
|
direction: 'downstream',
|
|
maxDepth: 5,
|
|
});
|
|
expect(result).toBeDefined();
|
|
expect(result).not.toHaveProperty('error');
|
|
});
|
|
});
|
|
|
|
// ─── Block 2: Consecutive tool call stability ──────────────────────
|
|
describe('MCP server stays alive after 10+ consecutive tool calls', () => {
|
|
it('10 consecutive cypher calls complete without stdout corruption', async () => {
|
|
for (let i = 0; i < 10; i++) {
|
|
const result = await backend.callTool('cypher', {
|
|
query: `MATCH (n:Function) RETURN n.name AS name LIMIT ${i + 1}`,
|
|
});
|
|
expect(result).toHaveProperty('row_count');
|
|
expect(result.row_count).toBeGreaterThanOrEqual(1);
|
|
}
|
|
});
|
|
|
|
it('mixed tool calls: context → impact → query → cypher cycle', async () => {
|
|
for (let i = 0; i < 3; i++) {
|
|
const ctx = await backend.callTool('context', { name: 'login' });
|
|
expect(ctx.status).toBe('found');
|
|
|
|
const imp = await backend.callTool('impact', {
|
|
target: 'validate',
|
|
direction: 'upstream',
|
|
});
|
|
expect(imp).not.toHaveProperty('error');
|
|
|
|
const qry = await backend.callTool('query', { query: 'login' });
|
|
expect(qry).not.toHaveProperty('error');
|
|
|
|
const cyp = await backend.callTool('cypher', {
|
|
query: 'MATCH (n:Function) RETURN COUNT(n) AS cnt',
|
|
});
|
|
expect(cyp).toHaveProperty('row_count');
|
|
}
|
|
});
|
|
|
|
it('stdout.write is still a function after all calls', () => {
|
|
expect(typeof process.stdout.write).toBe('function');
|
|
});
|
|
});
|
|
|
|
// ─── Block 3: Watchdog / activeQueryCount ──────────────────────────
|
|
describe('watchdog does not restore stdout during active queries', () => {
|
|
const REPO = 'watchdog-test';
|
|
let poolInited = false;
|
|
|
|
const ensurePool = async () => {
|
|
if (!poolInited) {
|
|
await initLbug(REPO, handle.dbPath);
|
|
poolInited = true;
|
|
}
|
|
};
|
|
|
|
afterAll(async () => {
|
|
try {
|
|
await closeLbug(REPO);
|
|
} catch {
|
|
/* best-effort */
|
|
}
|
|
});
|
|
|
|
it('parallel queries complete and stdout is restored', async () => {
|
|
await ensurePool();
|
|
const queries = Array.from({ length: 4 }, (_, i) =>
|
|
executeQuery(REPO, `MATCH (n:Function) RETURN n.name AS name LIMIT ${i + 1}`),
|
|
);
|
|
const results = await Promise.all(queries);
|
|
expect(results).toHaveLength(4);
|
|
for (const r of results) {
|
|
expect(r.length).toBeGreaterThanOrEqual(1);
|
|
}
|
|
});
|
|
|
|
it('sequential queries still work', async () => {
|
|
await ensurePool();
|
|
for (let i = 0; i < 5; i++) {
|
|
const rows = await executeQuery(REPO, 'MATCH (n:Function) RETURN n.name');
|
|
expect(rows.length).toBeGreaterThanOrEqual(1);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── Block 4: Stale data detection (#297) ──────────────────────────
|
|
// LAST: triggers closeLbug internally which may affect shared state
|
|
describe('stale data detection via meta.json', () => {
|
|
it('initial query works', async () => {
|
|
const result = await backend.callTool('cypher', {
|
|
query: 'MATCH (n:Function) RETURN n.name AS name ORDER BY n.name',
|
|
});
|
|
expect(result).toHaveProperty('row_count');
|
|
expect(result.row_count).toBeGreaterThanOrEqual(3);
|
|
});
|
|
|
|
it('detects stale index when meta.json indexedAt changes', async () => {
|
|
const metaPath = path.join(storagePath, 'meta.json');
|
|
await fs.writeFile(
|
|
metaPath,
|
|
JSON.stringify({
|
|
indexedAt: new Date(Date.now() + 60000).toISOString(),
|
|
lastCommit: 'new-commit-hash',
|
|
stats: { files: 2, nodes: 3, communities: 1, processes: 1 },
|
|
}),
|
|
);
|
|
|
|
// Next call triggers re-init. May fail but must NOT crash.
|
|
try {
|
|
const result = await backend.callTool('cypher', {
|
|
query: 'MATCH (n:Function) RETURN COUNT(n) AS cnt',
|
|
});
|
|
expect(result).toBeDefined();
|
|
} catch (err: any) {
|
|
expect(err.message).not.toMatch(/SIGSEGV/i);
|
|
}
|
|
});
|
|
|
|
it('throttle: no re-read within 5s window', async () => {
|
|
const metaPath = path.join(storagePath, 'meta.json');
|
|
await fs.writeFile(
|
|
metaPath,
|
|
JSON.stringify({
|
|
indexedAt: new Date(Date.now() + 120000).toISOString(),
|
|
lastCommit: 'another-commit',
|
|
stats: { files: 2, nodes: 3, communities: 1, processes: 1 },
|
|
}),
|
|
);
|
|
|
|
try {
|
|
const result = await backend.callTool('cypher', {
|
|
query: 'MATCH (n:Function) RETURN COUNT(n) AS cnt',
|
|
});
|
|
expect(result).toBeDefined();
|
|
} catch {
|
|
// No crash = success
|
|
}
|
|
});
|
|
});
|
|
},
|
|
{
|
|
seed: LOCAL_BACKEND_SEED_DATA,
|
|
ftsIndexes: LOCAL_BACKEND_FTS_INDEXES,
|
|
poolAdapter: true,
|
|
afterSetup: async (handle) => {
|
|
// Write initial meta.json for staleness tests
|
|
const metaPath = path.join(handle.tmpHandle.dbPath, 'meta.json');
|
|
const initialMeta = {
|
|
indexedAt: new Date().toISOString(),
|
|
lastCommit: 'abc123',
|
|
stats: { files: 2, nodes: 3, communities: 1, processes: 1 },
|
|
};
|
|
await fs.writeFile(metaPath, JSON.stringify(initialMeta));
|
|
|
|
vi.mocked(listRegisteredRepos).mockResolvedValue([
|
|
{
|
|
name: 'test-repo',
|
|
path: '/test/repo',
|
|
storagePath: handle.tmpHandle.dbPath,
|
|
indexedAt: initialMeta.indexedAt,
|
|
lastCommit: 'abc123',
|
|
stats: { files: 2, nodes: 3, communities: 1, processes: 1 },
|
|
},
|
|
]);
|
|
const backend = new LocalBackend();
|
|
await backend.init();
|
|
(handle as any)._backend = backend;
|
|
},
|
|
},
|
|
);
|