GitNexus/gitnexus/test/integration/staleness-and-stability.test.ts
Copilot 962f22482b
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
feat(cli): Fingerprint indexed repos by remote URL to detect sibling-clone graph drift (#982)
* 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>
2026-04-21 21:58:54 +01:00

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;
},
},
);