import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { mkdtempSync, rmSync, writeFileSync, closeSync, openSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { execFile } from 'node:child_process'; import { promisify } from 'node:util'; import { GitClient } from '@main/services/GitClient.js'; const execFileAsync = promisify(execFile); // Empty file used as GIT_CONFIG_GLOBAL/SYSTEM to isolate from the user's git config. let emptyConfigPath: string; let emptyConfigDir: string; function gitEnv(): NodeJS.ProcessEnv { return { ...process.env, GIT_CONFIG_GLOBAL: emptyConfigPath, GIT_CONFIG_SYSTEM: emptyConfigPath, GIT_AUTHOR_NAME: 'Test', GIT_AUTHOR_EMAIL: 'test@inkling.local', GIT_COMMITTER_NAME: 'Test', GIT_COMMITTER_EMAIL: 'test@inkling.local' }; } async function initRepo(dir: string) { const env = gitEnv(); await execFileAsync('git', ['init', '-b', 'main', dir], { env }); await execFileAsync('git', ['-C', dir, 'config', 'user.email', 'test@inkling.local'], { env }); await execFileAsync('git', ['-C', dir, 'config', 'user.name', 'Test'], { env }); // Initial commit so HEAD exists. writeFileSync(join(dir, 'init.txt'), 'init'); await execFileAsync('git', ['-C', dir, 'add', '-A'], { env }); await execFileAsync('git', ['-C', dir, 'commit', '-m', 'initial'], { env }); } describe('GitClient', () => { let dir: string; let prevEnv: NodeJS.ProcessEnv; beforeEach(() => { // Create an empty file for GIT_CONFIG_GLOBAL/SYSTEM to neutralize the user's global git config. emptyConfigDir = mkdtempSync(join(tmpdir(), 'gc-cfg-')); emptyConfigPath = join(emptyConfigDir, 'empty.gitconfig'); closeSync(openSync(emptyConfigPath, 'w')); // touch // Stash and replace process.env so child gits inherit isolated config (GitClient uses default env). prevEnv = { ...process.env }; Object.assign(process.env, { GIT_CONFIG_GLOBAL: emptyConfigPath, GIT_CONFIG_SYSTEM: emptyConfigPath, GIT_AUTHOR_NAME: 'Test', GIT_AUTHOR_EMAIL: 'test@inkling.local', GIT_COMMITTER_NAME: 'Test', GIT_COMMITTER_EMAIL: 'test@inkling.local' }); dir = mkdtempSync(join(tmpdir(), 'gc-')); }); afterEach(() => { rmSync(dir, { recursive: true, force: true }); rmSync(emptyConfigDir, { recursive: true, force: true }); // Restore env exactly. for (const k of Object.keys(process.env)) delete process.env[k]; Object.assign(process.env, prevEnv); }); it('isRepo returns false when dir is not a repo', async () => { const g = new GitClient(dir); expect(await g.isRepo()).toBe(false); }); it('isRepo returns true after git init', async () => { await initRepo(dir); const g = new GitClient(dir); expect(await g.isRepo()).toBe(true); }); it('hasRemote returns false when no remote', async () => { await initRepo(dir); const g = new GitClient(dir); expect(await g.hasRemote()).toBe(false); }); it('hasRemote returns true after remote add', async () => { await initRepo(dir); const remoteDir = mkdtempSync(join(tmpdir(), 'gc-remote-')); try { await execFileAsync('git', ['init', '--bare', remoteDir], { env: gitEnv() }); await execFileAsync('git', ['-C', dir, 'remote', 'add', 'origin', remoteDir], { env: gitEnv() }); const g = new GitClient(dir); expect(await g.hasRemote()).toBe(true); } finally { rmSync(remoteDir, { recursive: true, force: true }); } }); it('commit returns changed=false when nothing to commit', async () => { await initRepo(dir); const g = new GitClient(dir); const r = await g.commit('test'); expect(r.changed).toBe(false); expect(r.sha).toBeNull(); }); it('commit returns changed=true with sha when something to commit', async () => { await initRepo(dir); writeFileSync(join(dir, 'new.txt'), 'hi'); const g = new GitClient(dir); await g.addAll(); const r = await g.commit('test commit'); expect(r.changed).toBe(true); expect(r.sha).toMatch(/^[0-9a-f]{40}$/); }); it('currentBranch returns the branch name', async () => { await initRepo(dir); const g = new GitClient(dir); const branch = await g.currentBranch(); expect(branch).toBe('main'); }); it('run returns non-zero exit code without throwing for invalid args', async () => { await initRepo(dir); const g = new GitClient(dir); const r = await g.run(['this-is-not-a-real-git-subcommand']); expect(r.exitCode).not.toBe(0); }); });