kimi-code/packages/agent-core-v2/test/git/gitService.test.ts
haozhe.yang c2e88c1846 refactor(agent-core-v2): implement git domain on os process service
- spawn git/gh via IHostProcessService instead of node:child_process
- probe path existence via IHostFileSystem instead of node:fs/promises
- normalize git porcelain paths without node:path
- resolve IGitService through the container in gitService.test.ts
2026-07-03 15:43:17 +08:00

138 lines
4.6 KiB
TypeScript

import { execFileSync } from 'node:child_process';
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { DisposableStore } from '#/_base/di/lifecycle';
import { createServices, type TestInstantiationService } from '#/_base/di/test';
import { IGitService } from '#/app/git/git';
import { GitService } from '#/app/git/gitService';
import { ErrorCodes } from '#/errors';
import { HostFileSystem } from '#/os/backends/node-local/hostFsService';
import { HostProcessService } from '#/os/backends/node-local/hostProcessService';
import { IHostFileSystem } from '#/os/interface/hostFileSystem';
import { IHostProcessService } from '#/os/interface/hostProcess';
function git(cwd: string, ...args: string[]): string {
return execFileSync('git', args, {
cwd,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
})
.toString()
.trim();
}
describe('GitService', () => {
let repo: string;
let disposables: DisposableStore;
let ix: TestInstantiationService;
let service: IGitService;
beforeEach(() => {
repo = mkdtempSync(join(tmpdir(), 'git-service-'));
git(repo, 'init');
git(repo, 'config', 'user.email', 'test@example.com');
git(repo, 'config', 'user.name', 'Test');
disposables = new DisposableStore();
ix = createServices(disposables, {
additionalServices: (reg) => {
reg.define(IHostProcessService, HostProcessService);
reg.define(IHostFileSystem, HostFileSystem);
reg.define(IGitService, GitService);
},
});
service = ix.get(IGitService);
});
afterEach(() => {
disposables.dispose();
rmSync(repo, { recursive: true, force: true });
});
function commitAll(message: string): void {
git(repo, 'add', '-A');
git(repo, 'commit', '-m', message);
}
describe('status', () => {
it('reports a clean tree', async () => {
writeFileSync(join(repo, 'a.txt'), 'hello\n');
commitAll('init');
const result = await service.status(repo);
expect(typeof result.branch).toBe('string');
expect(result.entries).toEqual({});
expect(result.additions).toBe(0);
expect(result.deletions).toBe(0);
expect(result.pullRequest).toBeNull();
});
it('reports a modified file with numstat', async () => {
writeFileSync(join(repo, 'a.txt'), 'line1\n');
commitAll('init');
writeFileSync(join(repo, 'a.txt'), 'line1\nline2\nline3\n');
const result = await service.status(repo);
expect(result.entries).toEqual({ 'a.txt': 'modified' });
expect(result.additions).toBe(2);
expect(result.deletions).toBe(0);
});
it('restricts entries to the path filter', async () => {
writeFileSync(join(repo, 'a.txt'), 'a\n');
writeFileSync(join(repo, 'b.txt'), 'b\n');
commitAll('init');
writeFileSync(join(repo, 'a.txt'), 'a2\n');
writeFileSync(join(repo, 'b.txt'), 'b2\n');
const result = await service.status(repo, new Set(['a.txt']));
expect(result.entries).toEqual({ 'a.txt': 'modified' });
});
it('throws FS_GIT_UNAVAILABLE when not a repo', async () => {
const notRepo = mkdtempSync(join(tmpdir(), 'not-repo-'));
try {
await expect(service.status(notRepo)).rejects.toMatchObject({
code: ErrorCodes.FS_GIT_UNAVAILABLE,
});
} finally {
rmSync(notRepo, { recursive: true, force: true });
}
});
});
describe('diff', () => {
it('returns the unified diff for a tracked modified file', async () => {
writeFileSync(join(repo, 'a.txt'), 'old\n');
commitAll('init');
writeFileSync(join(repo, 'a.txt'), 'new\n');
const result = await service.diff(repo, 'a.txt', join(repo, 'a.txt'));
expect(result.path).toBe('a.txt');
expect(result.diff).toContain('+new');
expect(result.diff).toContain('-old');
expect(result.truncated).toBe(false);
});
it('returns an all-added diff for an untracked file', async () => {
writeFileSync(join(repo, 'a.txt'), 'hello\n');
commitAll('init');
writeFileSync(join(repo, 'b.txt'), 'brand new\n');
const result = await service.diff(repo, 'b.txt', join(repo, 'b.txt'));
expect(result.diff).toContain('+brand new');
});
it('throws FS_PATH_NOT_FOUND for a missing path', async () => {
writeFileSync(join(repo, 'a.txt'), 'hello\n');
commitAll('init');
await expect(
service.diff(repo, 'missing.txt', join(repo, 'missing.txt')),
).rejects.toMatchObject({ code: ErrorCodes.FS_PATH_NOT_FOUND });
});
});
});