quantum-cli-core
Version:
Quantum CLI Core - Multi-LLM Collaboration System
243 lines • 10.7 kB
JavaScript
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { GitService } from './gitService.js';
import * as path from 'path';
import { QUANTUM_CONFIG_DIR } from '../tools/memoryTool.js';
const hoistedMockExec = vi.hoisted(() => vi.fn());
vi.mock('node:child_process', () => ({
exec: hoistedMockExec,
}));
const hoistedMockMkdir = vi.hoisted(() => vi.fn());
const hoistedMockReadFile = vi.hoisted(() => vi.fn());
const hoistedMockWriteFile = vi.hoisted(() => vi.fn());
vi.mock('fs/promises', async (importOriginal) => {
const actual = (await importOriginal());
return {
...actual,
mkdir: hoistedMockMkdir,
readFile: hoistedMockReadFile,
writeFile: hoistedMockWriteFile,
};
});
const hoistedMockEnv = vi.hoisted(() => vi.fn());
const hoistedMockSimpleGit = vi.hoisted(() => vi.fn());
const hoistedMockCheckIsRepo = vi.hoisted(() => vi.fn());
const hoistedMockInit = vi.hoisted(() => vi.fn());
const hoistedMockRaw = vi.hoisted(() => vi.fn());
const hoistedMockAdd = vi.hoisted(() => vi.fn());
const hoistedMockCommit = vi.hoisted(() => vi.fn());
vi.mock('simple-git', () => ({
simpleGit: hoistedMockSimpleGit.mockImplementation(() => ({
checkIsRepo: hoistedMockCheckIsRepo,
init: hoistedMockInit,
raw: hoistedMockRaw,
add: hoistedMockAdd,
commit: hoistedMockCommit,
env: hoistedMockEnv,
})),
CheckRepoActions: { IS_REPO_ROOT: 'is-repo-root' },
}));
const hoistedIsGitRepositoryMock = vi.hoisted(() => vi.fn());
vi.mock('../utils/gitUtils.js', () => ({
isGitRepository: hoistedIsGitRepositoryMock,
}));
const hoistedMockIsNodeError = vi.hoisted(() => vi.fn());
vi.mock('../utils/errors.js', () => ({
isNodeError: hoistedMockIsNodeError,
}));
const hoistedMockHomedir = vi.hoisted(() => vi.fn());
vi.mock('os', () => ({
homedir: hoistedMockHomedir,
}));
const hoistedMockCreateHash = vi.hoisted(() => {
const mockUpdate = vi.fn().mockReturnThis();
const mockDigest = vi.fn();
return {
createHash: vi.fn(() => ({
update: mockUpdate,
digest: mockDigest,
})),
mockUpdate,
mockDigest,
};
});
vi.mock('crypto', () => ({
createHash: hoistedMockCreateHash.createHash,
}));
// Mock the paths module to control QUANTUM_DIR in tests
const hoistedQuantumDir = vi.hoisted(() => '.quantum');
vi.mock('../utils/paths.js', async (importOriginal) => {
const actual = await importOriginal();
return {
...actual,
GEMINI_DIR: hoistedQuantumDir,
};
});
describe('GitService', () => {
const mockProjectRoot = '/test/project';
const mockHomedir = '/mock/home';
const mockHash = 'mock-hash';
beforeEach(() => {
vi.clearAllMocks();
hoistedIsGitRepositoryMock.mockReturnValue(true);
hoistedMockExec.mockImplementation((command, callback) => {
if (command === 'git --version') {
callback(null, 'git version 2.0.0');
}
else {
callback(new Error('Command not mocked'));
}
return {};
});
hoistedMockMkdir.mockResolvedValue(undefined);
hoistedMockReadFile.mockResolvedValue('');
hoistedMockWriteFile.mockResolvedValue(undefined);
hoistedMockIsNodeError.mockImplementation((e) => e instanceof Error);
hoistedMockHomedir.mockReturnValue(mockHomedir);
hoistedMockCreateHash.mockUpdate.mockReturnThis();
hoistedMockCreateHash.mockDigest.mockReturnValue(mockHash);
hoistedMockEnv.mockImplementation(() => ({
checkIsRepo: hoistedMockCheckIsRepo,
init: hoistedMockInit,
raw: hoistedMockRaw,
add: hoistedMockAdd,
commit: hoistedMockCommit,
}));
hoistedMockSimpleGit.mockImplementation(() => ({
checkIsRepo: hoistedMockCheckIsRepo,
init: hoistedMockInit,
raw: hoistedMockRaw,
add: hoistedMockAdd,
commit: hoistedMockCommit,
env: hoistedMockEnv,
}));
hoistedMockCheckIsRepo.mockResolvedValue(false);
hoistedMockInit.mockResolvedValue(undefined);
hoistedMockRaw.mockResolvedValue('');
hoistedMockAdd.mockResolvedValue(undefined);
hoistedMockCommit.mockResolvedValue({
commit: 'initial',
});
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('constructor', () => {
it('should successfully create an instance if projectRoot is a Git repository', () => {
expect(() => new GitService(mockProjectRoot)).not.toThrow();
});
});
describe('verifyGitAvailability', () => {
it('should resolve true if git --version command succeeds', async () => {
const service = new GitService(mockProjectRoot);
await expect(service.verifyGitAvailability()).resolves.toBe(true);
});
it('should resolve false if git --version command fails', async () => {
hoistedMockExec.mockImplementation((command, callback) => {
callback(new Error('git not found'));
return {};
});
const service = new GitService(mockProjectRoot);
await expect(service.verifyGitAvailability()).resolves.toBe(false);
});
});
describe('initialize', () => {
it('should throw an error if projectRoot is not a Git repository', async () => {
hoistedIsGitRepositoryMock.mockReturnValue(false);
const service = new GitService(mockProjectRoot);
await expect(service.initialize()).rejects.toThrow('GitService requires a Git repository');
});
it('should throw an error if Git is not available', async () => {
hoistedMockExec.mockImplementation((command, callback) => {
callback(new Error('git not found'));
return {};
});
const service = new GitService(mockProjectRoot);
await expect(service.initialize()).rejects.toThrow('GitService requires Git to be installed');
});
it('should call setupShadowGitRepository if Git is available', async () => {
const service = new GitService(mockProjectRoot);
const setupSpy = vi
.spyOn(service, 'setupShadowGitRepository')
.mockResolvedValue(undefined);
await service.initialize();
expect(setupSpy).toHaveBeenCalled();
});
});
describe('setupShadowGitRepository', () => {
const repoDir = path.join(mockHomedir, QUANTUM_CONFIG_DIR, 'history', mockHash);
const hiddenGitIgnorePath = path.join(repoDir, '.gitignore');
const visibleGitIgnorePath = path.join(mockProjectRoot, '.gitignore');
const gitConfigPath = path.join(repoDir, '.gitconfig');
it('should create a .gitconfig file with the correct content', async () => {
const service = new GitService(mockProjectRoot);
await service.setupShadowGitRepository();
const expectedConfigContent = '[user]\n name = Gemini CLI\n email = gemini-cli@google.com\n[commit]\n gpgsign = false\n';
expect(hoistedMockWriteFile).toHaveBeenCalledWith(gitConfigPath, expectedConfigContent);
});
it('should create history and repository directories', async () => {
const service = new GitService(mockProjectRoot);
await service.setupShadowGitRepository();
expect(hoistedMockMkdir).toHaveBeenCalledWith(repoDir, {
recursive: true,
});
});
it('should initialize git repo in historyDir if not already initialized', async () => {
hoistedMockCheckIsRepo.mockResolvedValue(false);
const service = new GitService(mockProjectRoot);
await service.setupShadowGitRepository();
expect(hoistedMockSimpleGit).toHaveBeenCalledWith(repoDir);
expect(hoistedMockInit).toHaveBeenCalled();
});
it('should not initialize git repo if already initialized', async () => {
hoistedMockCheckIsRepo.mockResolvedValue(true);
const service = new GitService(mockProjectRoot);
await service.setupShadowGitRepository();
expect(hoistedMockInit).not.toHaveBeenCalled();
});
it('should copy .gitignore from projectRoot if it exists', async () => {
const gitignoreContent = `node_modules/\n.env`;
hoistedMockReadFile.mockImplementation(async (filePath) => {
if (filePath === visibleGitIgnorePath) {
return gitignoreContent;
}
return '';
});
const service = new GitService(mockProjectRoot);
await service.setupShadowGitRepository();
expect(hoistedMockReadFile).toHaveBeenCalledWith(visibleGitIgnorePath, 'utf-8');
expect(hoistedMockWriteFile).toHaveBeenCalledWith(hiddenGitIgnorePath, gitignoreContent);
});
it('should throw an error if reading projectRoot .gitignore fails with other errors', async () => {
const readError = new Error('Read permission denied');
hoistedMockReadFile.mockImplementation(async (filePath) => {
if (filePath === visibleGitIgnorePath) {
throw readError;
}
return '';
});
hoistedMockIsNodeError.mockImplementation((e) => e instanceof Error);
const service = new GitService(mockProjectRoot);
await expect(service.setupShadowGitRepository()).rejects.toThrow('Read permission denied');
});
it('should make an initial commit if no commits exist in history repo', async () => {
hoistedMockCheckIsRepo.mockResolvedValue(false);
const service = new GitService(mockProjectRoot);
await service.setupShadowGitRepository();
expect(hoistedMockCommit).toHaveBeenCalledWith('Initial commit', {
'--allow-empty': null,
});
});
it('should not make an initial commit if commits already exist', async () => {
hoistedMockCheckIsRepo.mockResolvedValue(true);
const service = new GitService(mockProjectRoot);
await service.setupShadowGitRepository();
expect(hoistedMockCommit).not.toHaveBeenCalled();
});
});
});
//# sourceMappingURL=gitService.test.js.map