mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
* perf(mcp): parallelize staleness checks in list_repos (#1363) Replace sequential synchronous git spawns with parallel async execFile calls so 200-repo registries resolve in under a second instead of ~50 s. * fix(test): address @claude review findings for parallel staleness PR - Add missing checkStalenessAsync mock to calltool-dispatch.test.ts (BLOCKER: caused 5 CI failures on every list_repos test path) - Add async invalid-commit-hash test for symmetry with sync suite - Document why promisified execFile omits stdio option
146 lines
4.6 KiB
TypeScript
146 lines
4.6 KiB
TypeScript
/**
|
|
* P2 Unit Tests: Staleness Check
|
|
*
|
|
* Tests: checkStaleness from staleness.ts
|
|
* - HEAD matches → not stale
|
|
* - HEAD differs → stale with commit count
|
|
* - Git failure → fail open (not stale)
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import { execFileSync } from 'child_process';
|
|
import { checkStaleness, checkStalenessAsync } from '../../src/core/git-staleness.js';
|
|
|
|
// We test checkStaleness with a real git repo (the project itself)
|
|
// since mocking execFileSync across ESM modules is complex.
|
|
|
|
describe('checkStaleness', () => {
|
|
it('returns not stale when HEAD matches lastCommit', () => {
|
|
// Get the actual HEAD commit of this repo
|
|
let headCommit: string;
|
|
try {
|
|
headCommit = execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
encoding: 'utf-8',
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
}).trim();
|
|
} catch {
|
|
// If we can't get HEAD (e.g., not in a git repo), skip
|
|
return;
|
|
}
|
|
|
|
const result = checkStaleness(process.cwd(), headCommit);
|
|
expect(result.isStale).toBe(false);
|
|
expect(result.commitsBehind).toBe(0);
|
|
expect(result.hint).toBeUndefined();
|
|
});
|
|
|
|
it('returns stale when lastCommit is behind HEAD', () => {
|
|
// Use HEAD~1 — works in shallow clones (GitHub Actions) unlike rev-list --max-parents=0
|
|
let previousCommit: string;
|
|
try {
|
|
previousCommit = execFileSync('git', ['rev-parse', 'HEAD~1'], {
|
|
encoding: 'utf-8',
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
}).trim();
|
|
} catch {
|
|
return; // Not in a git repo or only 1 commit
|
|
}
|
|
|
|
if (!previousCommit) return;
|
|
|
|
const result = checkStaleness(process.cwd(), previousCommit);
|
|
expect(result.isStale).toBe(true);
|
|
expect(result.commitsBehind).toBeGreaterThan(0);
|
|
expect(result.hint).toContain('behind HEAD');
|
|
});
|
|
|
|
it('fails open when git command fails (e.g., invalid path)', () => {
|
|
const result = checkStaleness('/nonexistent/path', 'abc123');
|
|
expect(result.isStale).toBe(false);
|
|
expect(result.commitsBehind).toBe(0);
|
|
});
|
|
|
|
it('fails open with invalid commit hash', () => {
|
|
const result = checkStaleness(process.cwd(), 'not-a-real-commit-hash');
|
|
expect(result.isStale).toBe(false);
|
|
expect(result.commitsBehind).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('checkStalenessAsync', () => {
|
|
it('returns not stale when HEAD matches lastCommit', async () => {
|
|
let headCommit: string;
|
|
try {
|
|
headCommit = execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
encoding: 'utf-8',
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
}).trim();
|
|
} catch {
|
|
return;
|
|
}
|
|
|
|
const result = await checkStalenessAsync(process.cwd(), headCommit);
|
|
expect(result.isStale).toBe(false);
|
|
expect(result.commitsBehind).toBe(0);
|
|
expect(result.hint).toBeUndefined();
|
|
});
|
|
|
|
it('returns stale when lastCommit is behind HEAD', async () => {
|
|
let previousCommit: string;
|
|
try {
|
|
previousCommit = execFileSync('git', ['rev-parse', 'HEAD~1'], {
|
|
encoding: 'utf-8',
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
}).trim();
|
|
} catch {
|
|
return;
|
|
}
|
|
|
|
if (!previousCommit) return;
|
|
|
|
const result = await checkStalenessAsync(process.cwd(), previousCommit);
|
|
expect(result.isStale).toBe(true);
|
|
expect(result.commitsBehind).toBeGreaterThan(0);
|
|
expect(result.hint).toContain('behind HEAD');
|
|
});
|
|
|
|
it('fails open when git command fails (e.g., invalid path)', async () => {
|
|
const result = await checkStalenessAsync('/nonexistent/path', 'abc123');
|
|
expect(result.isStale).toBe(false);
|
|
expect(result.commitsBehind).toBe(0);
|
|
});
|
|
|
|
it('fails open with invalid commit hash', async () => {
|
|
const result = await checkStalenessAsync(process.cwd(), 'not-a-real-commit-hash');
|
|
expect(result.isStale).toBe(false);
|
|
expect(result.commitsBehind).toBe(0);
|
|
});
|
|
|
|
it('parallel calls complete faster than sequential', async () => {
|
|
let headCommit: string;
|
|
try {
|
|
headCommit = execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
encoding: 'utf-8',
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
}).trim();
|
|
} catch {
|
|
return;
|
|
}
|
|
|
|
const cwd = process.cwd();
|
|
const N = 10;
|
|
|
|
// Parallel
|
|
const t0 = performance.now();
|
|
await Promise.all(Array.from({ length: N }, () => checkStalenessAsync(cwd, headCommit)));
|
|
const parallelMs = performance.now() - t0;
|
|
|
|
// Sequential sync
|
|
const t1 = performance.now();
|
|
for (let i = 0; i < N; i++) checkStaleness(cwd, headCommit);
|
|
const sequentialMs = performance.now() - t1;
|
|
|
|
// Parallel should be meaningfully faster than sequential.
|
|
// Use a generous ratio to avoid flakiness on slow CI machines.
|
|
expect(parallelMs).toBeLessThan(sequentialMs * 1.5);
|
|
});
|
|
});
|