GitNexus/gitnexus/test/unit/git-utils.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

182 lines
7.1 KiB
TypeScript

/**
* Unit Tests: git utility helpers (storage/git.ts)
*
* Tests isGitRepo, getCurrentCommit, getGitRoot, and the newly added
* hasGitDir helper introduced for issue #384 (indexing non-git folders).
*/
import { describe, it, expect } from 'vitest';
import path from 'path';
import os from 'os';
import fs from 'fs';
import { execSync } from 'child_process';
// ─── hasGitDir ────────────────────────────────────────────────────────────
//
// hasGitDir is a synchronous fs.statSync check — we test it by actually
// creating temporary directories rather than mocking the fs module,
// because the implementation is a simple one-liner and real disk I/O is
// fast and deterministic for this purpose.
describe('hasGitDir', () => {
// Import after test setup to ensure module resolution is correct
const getHasGitDir = async () => {
const mod = await import('../../src/storage/git.js');
return mod.hasGitDir;
};
it('returns true when .git directory exists', async () => {
const hasGitDir = await getHasGitDir();
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-test-'));
try {
fs.mkdirSync(path.join(tmpDir, '.git'));
expect(hasGitDir(tmpDir)).toBe(true);
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
it('returns true when .git is a file (git worktree)', async () => {
const hasGitDir = await getHasGitDir();
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-test-'));
try {
fs.writeFileSync(path.join(tmpDir, '.git'), 'gitdir: /some/other/.git\n');
expect(hasGitDir(tmpDir)).toBe(true);
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
it('returns false when .git entry is absent', async () => {
const hasGitDir = await getHasGitDir();
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-test-'));
try {
// No .git here — plain directory
expect(hasGitDir(tmpDir)).toBe(false);
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
it('returns false for a non-existent path', async () => {
const hasGitDir = await getHasGitDir();
expect(hasGitDir('/tmp/__gitnexus_nonexistent_path__')).toBe(false);
});
});
// ─── isGitRepo ────────────────────────────────────────────────────────────
//
// isGitRepo shells out to `git rev-parse` — we verify it returns false
// for a plain temp directory without running git init.
describe('isGitRepo', () => {
it('returns false for a plain (non-git) directory', async () => {
const { isGitRepo } = await import('../../src/storage/git.js');
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-test-'));
try {
expect(isGitRepo(tmpDir)).toBe(false);
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
it('returns false for a non-existent path', async () => {
const { isGitRepo } = await import('../../src/storage/git.js');
expect(isGitRepo('/tmp/__gitnexus_nonexistent__')).toBe(false);
});
});
// ─── getCurrentCommit ─────────────────────────────────────────────────────
describe('getCurrentCommit', () => {
it('returns empty string for a non-git directory', async () => {
const { getCurrentCommit } = await import('../../src/storage/git.js');
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-test-'));
try {
expect(getCurrentCommit(tmpDir)).toBe('');
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
});
// ─── getGitRoot ───────────────────────────────────────────────────────────
describe('getGitRoot', () => {
it('returns null for a plain temp directory', async () => {
const { getGitRoot } = await import('../../src/storage/git.js');
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-test-'));
try {
expect(getGitRoot(tmpDir)).toBeNull();
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
});
// ─── getRemoteUrl ─────────────────────────────────────────────────────────
describe('getRemoteUrl', () => {
const setupRepoWithRemote = (remoteUrl: string): string => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-remote-'));
// Use real fs paths and shellouts — the helper itself shells out to
// `git config`, so we need a real git repo for the assertion to be
// meaningful.
execSync('git init -q', { cwd: tmpDir });
execSync(`git remote add origin ${remoteUrl}`, { cwd: tmpDir });
return tmpDir;
};
it('returns undefined for a non-git directory', async () => {
const { getRemoteUrl } = await import('../../src/storage/git.js');
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-test-'));
try {
expect(getRemoteUrl(tmpDir)).toBeUndefined();
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
it('returns undefined for a git repo with no origin remote', async () => {
const { getRemoteUrl } = await import('../../src/storage/git.js');
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-test-'));
try {
execSync('git init -q', { cwd: tmpDir });
expect(getRemoteUrl(tmpDir)).toBeUndefined();
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
it('strips trailing .git and lowercases host for HTTPS remotes', async () => {
const { getRemoteUrl } = await import('../../src/storage/git.js');
const tmpDir = setupRepoWithRemote('https://GitHub.COM/Foo/Bar.git');
try {
expect(getRemoteUrl(tmpDir)).toBe('https://github.com/Foo/Bar');
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
it('lowercases host for SCP-style SSH remotes and strips .git', async () => {
const { getRemoteUrl } = await import('../../src/storage/git.js');
const tmpDir = setupRepoWithRemote('git@GitHub.com:Foo/Bar.git');
try {
expect(getRemoteUrl(tmpDir)).toBe('git@github.com:Foo/Bar');
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
it('returns the same fingerprint for two clones of the same repo', async () => {
const { getRemoteUrl } = await import('../../src/storage/git.js');
const a = setupRepoWithRemote('https://example.com/foo/bar.git');
const b = setupRepoWithRemote('https://example.com/foo/bar');
try {
expect(getRemoteUrl(a)).toBe(getRemoteUrl(b));
expect(getRemoteUrl(a)).toBeTruthy();
} finally {
fs.rmSync(a, { recursive: true, force: true });
fs.rmSync(b, { recursive: true, force: true });
}
});
});