mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-07 08:26:11 +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.
119 lines
4 KiB
TypeScript
119 lines
4 KiB
TypeScript
import { describe, it, expect, afterAll } from 'vitest';
|
|
import { JobManager } from '../../src/server/analyze-job.js';
|
|
import { extractRepoName, getCloneDir } from '../../src/server/git-clone.js';
|
|
|
|
describe('server-side analyze integration', () => {
|
|
const manager = new JobManager();
|
|
|
|
afterAll(() => manager.dispose());
|
|
|
|
it('full job lifecycle: create -> clone -> analyze -> complete', () => {
|
|
const job = manager.createJob({ repoUrl: 'https://github.com/user/test-repo' });
|
|
expect(job.status).toBe('queued');
|
|
|
|
// Simulate clone phase
|
|
manager.updateJob(job.id, {
|
|
status: 'cloning',
|
|
progress: { phase: 'cloning', percent: 5, message: 'Cloning...' },
|
|
});
|
|
expect(manager.getJob(job.id)!.status).toBe('cloning');
|
|
|
|
// Simulate analyze phase
|
|
manager.updateJob(job.id, {
|
|
status: 'analyzing',
|
|
repoPath: '/tmp/test-repo',
|
|
progress: { phase: 'parsing', percent: 30, message: 'Parsing code' },
|
|
});
|
|
expect(manager.getJob(job.id)!.status).toBe('analyzing');
|
|
|
|
// Simulate completion
|
|
manager.updateJob(job.id, {
|
|
status: 'complete',
|
|
repoName: 'test-repo',
|
|
});
|
|
|
|
const final = manager.getJob(job.id)!;
|
|
expect(final.status).toBe('complete');
|
|
expect(final.repoName).toBe('test-repo');
|
|
expect(final.completedAt).toBeDefined();
|
|
});
|
|
|
|
it('SSE progress listener receives all events in order', () => {
|
|
const job = manager.createJob({ repoUrl: 'https://github.com/user/sse-test' });
|
|
const events: any[] = [];
|
|
|
|
const unsubscribe = manager.onProgress(job.id, (progress) => {
|
|
events.push(progress);
|
|
});
|
|
|
|
manager.updateJob(job.id, {
|
|
status: 'analyzing',
|
|
progress: { phase: 'parsing', percent: 30, message: 'Parsing' },
|
|
});
|
|
manager.updateJob(job.id, {
|
|
progress: { phase: 'calls', percent: 50, message: 'Tracing calls' },
|
|
});
|
|
manager.updateJob(job.id, { status: 'complete', repoName: 'sse-test' });
|
|
|
|
unsubscribe();
|
|
|
|
expect(events.length).toBe(3);
|
|
expect(events[0].phase).toBe('parsing');
|
|
expect(events[1].phase).toBe('calls');
|
|
expect(events[2].phase).toBe('complete');
|
|
});
|
|
|
|
it('failed job emits failure event', () => {
|
|
const job = manager.createJob({ repoUrl: 'https://github.com/user/fail-test' });
|
|
const events: any[] = [];
|
|
|
|
const unsubscribe = manager.onProgress(job.id, (progress) => {
|
|
events.push(progress);
|
|
});
|
|
|
|
manager.updateJob(job.id, {
|
|
status: 'failed',
|
|
error: 'Clone failed: repository not found',
|
|
});
|
|
|
|
unsubscribe();
|
|
|
|
expect(events.length).toBe(1);
|
|
expect(events[0].phase).toBe('failed');
|
|
expect(events[0].message).toBe('Clone failed: repository not found');
|
|
});
|
|
|
|
it('git URL name extraction for clone paths', () => {
|
|
expect(extractRepoName('https://github.com/facebook/react.git')).toBe('react');
|
|
expect(extractRepoName('git@github.com:microsoft/vscode.git')).toBe('vscode');
|
|
const dir = getCloneDir('react');
|
|
expect(dir).toMatch(/\.gitnexus/);
|
|
expect(dir).toMatch(/repos/);
|
|
expect(dir).toContain('react');
|
|
});
|
|
|
|
it('concurrency: blocks second job while first is active', () => {
|
|
// First job completes from previous test, start fresh
|
|
const freshManager = new JobManager();
|
|
|
|
const job1 = freshManager.createJob({ repoUrl: 'https://github.com/user/repo-a' });
|
|
freshManager.updateJob(job1.id, { status: 'analyzing' });
|
|
|
|
// Second different repo should be rejected
|
|
expect(() => freshManager.createJob({ repoUrl: 'https://github.com/user/repo-b' })).toThrow(
|
|
/already in progress/,
|
|
);
|
|
|
|
// Same repo should return existing job
|
|
const job1again = freshManager.createJob({ repoUrl: 'https://github.com/user/repo-a' });
|
|
expect(job1again.id).toBe(job1.id);
|
|
|
|
// After completion, new job allowed
|
|
freshManager.updateJob(job1.id, { status: 'complete' });
|
|
const job2 = freshManager.createJob({ repoUrl: 'https://github.com/user/repo-b' });
|
|
expect(job2.status).toBe('queued');
|
|
expect(job2.id).not.toBe(job1.id);
|
|
|
|
freshManager.dispose();
|
|
});
|
|
});
|