GitNexus/gitnexus/test/unit/git.test.ts
Gergő Magyar bf09eab95b
feat: configure prettier with pre-commit hook (#563)
* 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.
2026-03-28 14:58:04 +00:00

95 lines
3 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { execSync } from 'child_process';
import { isGitRepo, getCurrentCommit, getGitRoot } from '../../src/storage/git.js';
// Mock child_process.execSync
vi.mock('child_process', () => ({
execSync: vi.fn(),
}));
const mockExecSync = vi.mocked(execSync);
describe('git utilities', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('isGitRepo', () => {
it('returns true when inside a git work tree', () => {
mockExecSync.mockReturnValueOnce(Buffer.from(''));
expect(isGitRepo('/project')).toBe(true);
expect(mockExecSync).toHaveBeenCalledWith('git rev-parse --is-inside-work-tree', {
cwd: '/project',
stdio: 'ignore',
});
});
it('returns false when not a git repo', () => {
mockExecSync.mockImplementationOnce(() => {
throw new Error('not a git repo');
});
expect(isGitRepo('/not-a-repo')).toBe(false);
});
it('passes the correct cwd', () => {
mockExecSync.mockReturnValueOnce(Buffer.from(''));
isGitRepo('/some/path');
expect(mockExecSync).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({ cwd: '/some/path' }),
);
});
});
describe('getCurrentCommit', () => {
it('returns trimmed commit hash', () => {
mockExecSync.mockReturnValueOnce(Buffer.from('abc123def\n'));
expect(getCurrentCommit('/project')).toBe('abc123def');
});
it('returns empty string on error', () => {
mockExecSync.mockImplementationOnce(() => {
throw new Error('not a git repo');
});
expect(getCurrentCommit('/not-a-repo')).toBe('');
});
it('trims whitespace from output', () => {
mockExecSync.mockReturnValueOnce(Buffer.from(' sha256hash \n'));
expect(getCurrentCommit('/project')).toBe('sha256hash');
});
});
describe('getGitRoot', () => {
it('returns resolved path on success', () => {
mockExecSync.mockReturnValueOnce(Buffer.from('/d/Projects/MyRepo\n'));
const result = getGitRoot('/d/Projects/MyRepo/src');
expect(result).toBeTruthy();
// path.resolve normalizes the git output
expect(typeof result).toBe('string');
});
it('returns null when not in a git repo', () => {
mockExecSync.mockImplementationOnce(() => {
throw new Error('not a git repo');
});
expect(getGitRoot('/not-a-repo')).toBeNull();
});
it('calls git rev-parse --show-toplevel', () => {
mockExecSync.mockReturnValueOnce(Buffer.from('/repo\n'));
getGitRoot('/repo/src');
expect(mockExecSync).toHaveBeenCalledWith(
'git rev-parse --show-toplevel',
expect.objectContaining({ cwd: '/repo/src' }),
);
});
it('trims output before resolving path', () => {
mockExecSync.mockReturnValueOnce(Buffer.from(' /repo \n'));
const result = getGitRoot('/repo/src');
expect(result).not.toBeNull();
expect(result!.trim()).toBe(result);
});
});
});