summaryrefslogtreecommitdiff
path: root/packages/core/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/core/src')
-rw-r--r--packages/core/src/services/gitService.test.ts180
-rw-r--r--packages/core/src/tools/shell.test.ts8
2 files changed, 80 insertions, 108 deletions
diff --git a/packages/core/src/services/gitService.test.ts b/packages/core/src/services/gitService.test.ts
index d23b0737..9820ba5f 100644
--- a/packages/core/src/services/gitService.test.ts
+++ b/packages/core/src/services/gitService.test.ts
@@ -7,28 +7,16 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { GitService } from './gitService.js';
import * as path from 'path';
-import type * as FsPromisesModule from 'fs/promises';
+import * as fs from 'fs/promises';
+import * as os from 'os';
import type { ChildProcess } from 'node:child_process';
+import { getProjectHash, GEMINI_DIR } from '../utils/paths.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()) as typeof FsPromisesModule;
- 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());
@@ -53,38 +41,30 @@ 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();
+vi.mock('os', async (importOriginal) => {
+ const actual = await importOriginal<typeof os>();
return {
- createHash: vi.fn(() => ({
- update: mockUpdate,
- digest: mockDigest,
- })),
- mockUpdate,
- mockDigest,
+ ...actual,
+ homedir: hoistedMockHomedir,
};
});
-vi.mock('crypto', () => ({
- createHash: hoistedMockCreateHash.createHash,
-}));
describe('GitService', () => {
- const mockProjectRoot = '/test/project';
- const mockHomedir = '/mock/home';
- const mockHash = 'mock-hash';
+ let testRootDir: string;
+ let projectRoot: string;
+ let homedir: string;
+ let hash: string;
+
+ beforeEach(async () => {
+ testRootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'git-service-test-'));
+ projectRoot = path.join(testRootDir, 'project');
+ homedir = path.join(testRootDir, 'home');
+ await fs.mkdir(projectRoot, { recursive: true });
+ await fs.mkdir(homedir, { recursive: true });
+
+ hash = getProjectHash(projectRoot);
- beforeEach(() => {
vi.clearAllMocks();
hoistedIsGitRepositoryMock.mockReturnValue(true);
hoistedMockExec.mockImplementation((command, callback) => {
@@ -95,13 +75,8 @@ describe('GitService', () => {
}
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);
+
+ hoistedMockHomedir.mockReturnValue(homedir);
hoistedMockEnv.mockImplementation(() => ({
checkIsRepo: hoistedMockCheckIsRepo,
@@ -127,19 +102,20 @@ describe('GitService', () => {
});
});
- afterEach(() => {
+ afterEach(async () => {
vi.restoreAllMocks();
+ await fs.rm(testRootDir, { recursive: true, force: true });
});
describe('constructor', () => {
- it('should successfully create an instance if projectRoot is a Git repository', () => {
- expect(() => new GitService(mockProjectRoot)).not.toThrow();
+ it('should successfully create an instance', () => {
+ expect(() => new GitService(projectRoot)).not.toThrow();
});
});
describe('verifyGitAvailability', () => {
it('should resolve true if git --version command succeeds', async () => {
- const service = new GitService(mockProjectRoot);
+ const service = new GitService(projectRoot);
await expect(service.verifyGitAvailability()).resolves.toBe(true);
});
@@ -148,7 +124,7 @@ describe('GitService', () => {
callback(new Error('git not found'));
return {} as ChildProcess;
});
- const service = new GitService(mockProjectRoot);
+ const service = new GitService(projectRoot);
await expect(service.verifyGitAvailability()).resolves.toBe(false);
});
});
@@ -159,14 +135,14 @@ describe('GitService', () => {
callback(new Error('git not found'));
return {} as ChildProcess;
});
- const service = new GitService(mockProjectRoot);
+ const service = new GitService(projectRoot);
await expect(service.initialize()).rejects.toThrow(
'Checkpointing is enabled, but Git is not installed. Please install Git or disable checkpointing to continue.',
);
});
it('should call setupShadowGitRepository if Git is available', async () => {
- const service = new GitService(mockProjectRoot);
+ const service = new GitService(projectRoot);
const setupSpy = vi
.spyOn(service, 'setupShadowGitRepository')
.mockResolvedValue(undefined);
@@ -177,33 +153,34 @@ describe('GitService', () => {
});
describe('setupShadowGitRepository', () => {
- const repoDir = path.join(mockHomedir, '.gemini', 'history', mockHash);
- const hiddenGitIgnorePath = path.join(repoDir, '.gitignore');
- const visibleGitIgnorePath = path.join(mockProjectRoot, '.gitignore');
- const gitConfigPath = path.join(repoDir, '.gitconfig');
+ let repoDir: string;
+ let gitConfigPath: string;
- 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 = [email protected]\n[commit]\n gpgsign = false\n';
- expect(hoistedMockWriteFile).toHaveBeenCalledWith(
- gitConfigPath,
- expectedConfigContent,
- );
+ beforeEach(() => {
+ repoDir = path.join(homedir, GEMINI_DIR, 'history', hash);
+ gitConfigPath = path.join(repoDir, '.gitconfig');
});
it('should create history and repository directories', async () => {
- const service = new GitService(mockProjectRoot);
+ const service = new GitService(projectRoot);
await service.setupShadowGitRepository();
- expect(hoistedMockMkdir).toHaveBeenCalledWith(repoDir, {
- recursive: true,
- });
+ const stats = await fs.stat(repoDir);
+ expect(stats.isDirectory()).toBe(true);
+ });
+
+ it('should create a .gitconfig file with the correct content', async () => {
+ const service = new GitService(projectRoot);
+ await service.setupShadowGitRepository();
+
+ const expectedConfigContent =
+ '[user]\n name = Gemini CLI\n email = [email protected]\n[commit]\n gpgsign = false\n';
+ const actualConfigContent = await fs.readFile(gitConfigPath, 'utf-8');
+ expect(actualConfigContent).toBe(expectedConfigContent);
});
it('should initialize git repo in historyDir if not already initialized', async () => {
hoistedMockCheckIsRepo.mockResolvedValue(false);
- const service = new GitService(mockProjectRoot);
+ const service = new GitService(projectRoot);
await service.setupShadowGitRepository();
expect(hoistedMockSimpleGit).toHaveBeenCalledWith(repoDir);
expect(hoistedMockInit).toHaveBeenCalled();
@@ -211,52 +188,49 @@ describe('GitService', () => {
it('should not initialize git repo if already initialized', async () => {
hoistedMockCheckIsRepo.mockResolvedValue(true);
- const service = new GitService(mockProjectRoot);
+ const service = new GitService(projectRoot);
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);
+ const gitignoreContent = 'node_modules/\n.env';
+ const visibleGitIgnorePath = path.join(projectRoot, '.gitignore');
+ await fs.writeFile(visibleGitIgnorePath, gitignoreContent);
+
+ const service = new GitService(projectRoot);
await service.setupShadowGitRepository();
- expect(hoistedMockReadFile).toHaveBeenCalledWith(
- visibleGitIgnorePath,
- 'utf-8',
- );
- expect(hoistedMockWriteFile).toHaveBeenCalledWith(
- hiddenGitIgnorePath,
- gitignoreContent,
- );
+
+ const hiddenGitIgnorePath = path.join(repoDir, '.gitignore');
+ const copiedContent = await fs.readFile(hiddenGitIgnorePath, 'utf-8');
+ expect(copiedContent).toBe(gitignoreContent);
+ });
+
+ it('should not create a .gitignore in shadow repo if project .gitignore does not exist', async () => {
+ const service = new GitService(projectRoot);
+ await service.setupShadowGitRepository();
+
+ const hiddenGitIgnorePath = path.join(repoDir, '.gitignore');
+ // An empty string is written if the file doesn't exist.
+ const content = await fs.readFile(hiddenGitIgnorePath, 'utf-8');
+ expect(content).toBe('');
});
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: unknown): e is NodeJS.ErrnoException => e instanceof Error,
- );
+ const visibleGitIgnorePath = path.join(projectRoot, '.gitignore');
+ // Create a directory instead of a file to cause a read error
+ await fs.mkdir(visibleGitIgnorePath);
- const service = new GitService(mockProjectRoot);
+ const service = new GitService(projectRoot);
+ // EISDIR is the expected error code on Unix-like systems
await expect(service.setupShadowGitRepository()).rejects.toThrow(
- 'Read permission denied',
+ /EISDIR: illegal operation on a directory, read|EBUSY: resource busy or locked, read/,
);
});
it('should make an initial commit if no commits exist in history repo', async () => {
hoistedMockCheckIsRepo.mockResolvedValue(false);
- const service = new GitService(mockProjectRoot);
+ const service = new GitService(projectRoot);
await service.setupShadowGitRepository();
expect(hoistedMockCommit).toHaveBeenCalledWith('Initial commit', {
'--allow-empty': null,
@@ -265,7 +239,7 @@ describe('GitService', () => {
it('should not make an initial commit if commits already exist', async () => {
hoistedMockCheckIsRepo.mockResolvedValue(true);
- const service = new GitService(mockProjectRoot);
+ const service = new GitService(projectRoot);
await service.setupShadowGitRepository();
expect(hoistedMockCommit).not.toHaveBeenCalled();
});
diff --git a/packages/core/src/tools/shell.test.ts b/packages/core/src/tools/shell.test.ts
index b0c7d317..460c871a 100644
--- a/packages/core/src/tools/shell.test.ts
+++ b/packages/core/src/tools/shell.test.ts
@@ -142,11 +142,9 @@ describe('ShellTool Bug Reproduction', () => {
shellTool = new ShellTool(config);
const abortSignal = new AbortController().signal;
- const result = await shellTool.execute(
- { command: 'echo "$GEMINI_CLI"' },
- abortSignal,
- () => {},
- );
+ const command =
+ os.platform() === 'win32' ? 'echo %GEMINI_CLI%' : 'echo "$GEMINI_CLI"';
+ const result = await shellTool.execute({ command }, abortSignal, () => {});
expect(result.returnDisplay).toBe('1' + os.EOL);
});