mirror of
https://github.com/game-ci/unity-builder.git
synced 2026-06-08 15:03:58 -07:00
test(orchestrator): expand unit tests for enterprise services
Add comprehensive tests for CLI provider (cleanupWorkflow, garbageCollect, listWorkflow, watchWorkflow, stderr forwarding, timeout handling), local cache service (saveLfsCache full path and error handling), git hooks service (husky install, failure logging, edge cases), and LFS agent service (empty storagePaths, validate logging). 73 tests across 4 test files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -217,6 +217,208 @@ describe('CliProvider', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('cleanupWorkflow', () => {
|
||||
it('sends cleanup-workflow command and returns result', async () => {
|
||||
const child = createMockChildProcess();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
|
||||
const provider = new CliProvider('/path/to/exe', {} as any);
|
||||
const promise = provider.cleanupWorkflow({ editorVersion: '2022.3' } as any, 'main', []);
|
||||
|
||||
child.stdout.emit('data', Buffer.from(JSON.stringify({ success: true, result: 'cleaned' }) + '\n'));
|
||||
child.emit('close', 0);
|
||||
|
||||
const result = await promise;
|
||||
expect(result).toBe('cleaned');
|
||||
|
||||
const parsed = JSON.parse(child.stdin.write.mock.calls[0][0]);
|
||||
expect(parsed.command).toBe('cleanup-workflow');
|
||||
expect(parsed.params.branchName).toBe('main');
|
||||
});
|
||||
});
|
||||
|
||||
describe('garbageCollect', () => {
|
||||
it('sends garbage-collect command with correct params', async () => {
|
||||
const child = createMockChildProcess();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
|
||||
const provider = new CliProvider('/path/to/exe', {} as any);
|
||||
const promise = provider.garbageCollect('filter*', true, 30, false, true);
|
||||
|
||||
child.stdout.emit('data', Buffer.from(JSON.stringify({ success: true, output: '3 items removed' }) + '\n'));
|
||||
child.emit('close', 0);
|
||||
|
||||
const result = await promise;
|
||||
expect(result).toBe('3 items removed');
|
||||
|
||||
const parsed = JSON.parse(child.stdin.write.mock.calls[0][0]);
|
||||
expect(parsed.command).toBe('garbage-collect');
|
||||
expect(parsed.params.filter).toBe('filter*');
|
||||
expect(parsed.params.previewOnly).toBe(true);
|
||||
expect(parsed.params.olderThan).toBe(30);
|
||||
expect(parsed.params.baseDependencies).toBe(true);
|
||||
});
|
||||
|
||||
it('returns empty string when response has no output', async () => {
|
||||
const child = createMockChildProcess();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
|
||||
const provider = new CliProvider('/path/to/exe', {} as any);
|
||||
const promise = provider.garbageCollect('', false, 7, false, false);
|
||||
|
||||
child.stdout.emit('data', Buffer.from(JSON.stringify({ success: true }) + '\n'));
|
||||
child.emit('close', 0);
|
||||
|
||||
const result = await promise;
|
||||
expect(result).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('listWorkflow', () => {
|
||||
it('returns workflow list from response', async () => {
|
||||
const child = createMockChildProcess();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
|
||||
const provider = new CliProvider('/path/to/exe', {} as any);
|
||||
const promise = provider.listWorkflow();
|
||||
|
||||
const workflows = [{ Name: 'wf-1' }];
|
||||
child.stdout.emit('data', Buffer.from(JSON.stringify({ success: true, result: workflows }) + '\n'));
|
||||
child.emit('close', 0);
|
||||
|
||||
const result = await promise;
|
||||
expect(result).toEqual(workflows);
|
||||
});
|
||||
|
||||
it('returns empty array when result is missing', async () => {
|
||||
const child = createMockChildProcess();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
|
||||
const provider = new CliProvider('/path/to/exe', {} as any);
|
||||
const promise = provider.listWorkflow();
|
||||
|
||||
child.stdout.emit('data', Buffer.from(JSON.stringify({ success: true }) + '\n'));
|
||||
child.emit('close', 0);
|
||||
|
||||
const result = await promise;
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('watchWorkflow', () => {
|
||||
it('forwards streaming output and resolves on success', async () => {
|
||||
const child = createMockChildProcess();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
|
||||
const provider = new CliProvider('/path/to/exe', {} as any);
|
||||
const promise = provider.watchWorkflow();
|
||||
|
||||
child.stdout.emit('data', Buffer.from('watching...\nstatus: running\n'));
|
||||
child.stdout.emit('data', Buffer.from(JSON.stringify({ success: true, output: 'completed' }) + '\n'));
|
||||
child.emit('close', 0);
|
||||
|
||||
const result = await promise;
|
||||
expect(result).toBe('completed');
|
||||
|
||||
const parsed = JSON.parse(child.stdin.write.mock.calls[0][0]);
|
||||
expect(parsed.command).toBe('watch-workflow');
|
||||
});
|
||||
|
||||
it('rejects on watch-workflow failure', async () => {
|
||||
const child = createMockChildProcess();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
|
||||
const provider = new CliProvider('/path/to/exe', {} as any);
|
||||
const promise = provider.watchWorkflow();
|
||||
|
||||
child.stdout.emit('data', Buffer.from(JSON.stringify({ success: false, error: 'lost connection' }) + '\n'));
|
||||
child.emit('close', 1);
|
||||
|
||||
await expect(promise).rejects.toThrow('lost connection');
|
||||
});
|
||||
|
||||
it('rejects on spawn error', async () => {
|
||||
const child = createMockChildProcess();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
|
||||
const provider = new CliProvider('/path/to/exe', {} as any);
|
||||
const promise = provider.watchWorkflow();
|
||||
|
||||
child.emit('error', new Error('ENOENT'));
|
||||
|
||||
await expect(promise).rejects.toThrow('failed to spawn executable');
|
||||
});
|
||||
|
||||
it('resolves with collected output when exit code 0 and no JSON', async () => {
|
||||
const child = createMockChildProcess();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
|
||||
const provider = new CliProvider('/path/to/exe', {} as any);
|
||||
const promise = provider.watchWorkflow();
|
||||
|
||||
child.stdout.emit('data', Buffer.from('line A\nline B\n'));
|
||||
child.emit('close', 0);
|
||||
|
||||
const result = await promise;
|
||||
expect(result).toBe('line A\nline B');
|
||||
});
|
||||
});
|
||||
|
||||
describe('stderr forwarding', () => {
|
||||
it('forwards stderr lines to logger during execute', async () => {
|
||||
const child = createMockChildProcess();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
|
||||
const provider = new CliProvider('/path/to/exe', {} as any);
|
||||
const promise = provider.listResources();
|
||||
|
||||
child.stderr.emit('data', Buffer.from('warning: something\n'));
|
||||
child.stdout.emit('data', Buffer.from(JSON.stringify({ success: true, result: [] }) + '\n'));
|
||||
child.emit('close', 0);
|
||||
|
||||
await promise;
|
||||
// stderr content included in error message if process fails
|
||||
// Here it succeeds, so we just verify no rejection
|
||||
});
|
||||
|
||||
it('includes stderr in error message when process fails without JSON', async () => {
|
||||
const child = createMockChildProcess();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
|
||||
const provider = new CliProvider('/path/to/exe', {} as any);
|
||||
const promise = provider.listResources();
|
||||
|
||||
child.stderr.emit('data', Buffer.from('fatal: segfault\n'));
|
||||
child.emit('close', 1);
|
||||
|
||||
await expect(promise).rejects.toThrow('fatal: segfault');
|
||||
});
|
||||
});
|
||||
|
||||
describe('timeout handling', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('rejects and kills process when command times out', async () => {
|
||||
const child = createMockChildProcess();
|
||||
mockSpawn.mockReturnValue(child);
|
||||
|
||||
const provider = new CliProvider('/path/to/exe', {} as any);
|
||||
const promise = provider.listResources();
|
||||
|
||||
// Advance past the 300s default timeout
|
||||
jest.advanceTimersByTime(301_000);
|
||||
|
||||
await expect(promise).rejects.toThrow('timed out');
|
||||
expect(child.kill).toHaveBeenCalledWith('SIGTERM');
|
||||
});
|
||||
});
|
||||
|
||||
describe('available providers list', () => {
|
||||
it('includes cli in the available providers', () => {
|
||||
const providers = ProviderLoader.getAvailableProviders();
|
||||
|
||||
@@ -207,6 +207,38 @@ describe('LocalCacheService', () => {
|
||||
await LocalCacheService.saveLfsCache('/repo', '/cache', 'key1');
|
||||
expect(mockFs.mkdirSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create cache directory and save tar when lfs has content', async () => {
|
||||
(mockFs.existsSync as jest.Mock).mockReturnValue(true);
|
||||
(mockFs.readdirSync as jest.Mock).mockImplementation((dirPath: string) => {
|
||||
if (String(dirPath).includes('lfs') && !String(dirPath).includes('cache')) {
|
||||
return ['objects', 'tmp'];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
(mockFs.statSync as jest.Mock).mockReturnValue({ mtimeMs: Date.now() });
|
||||
(mockFs.mkdirSync as jest.Mock).mockReturnValue(undefined);
|
||||
|
||||
const { OrchestratorSystem } = require('../core/orchestrator-system');
|
||||
OrchestratorSystem.Run.mockResolvedValue('');
|
||||
|
||||
await LocalCacheService.saveLfsCache('/repo', '/cache', 'key1');
|
||||
expect(mockFs.mkdirSync).toHaveBeenCalledWith(path.join('/cache', 'key1', 'lfs'), { recursive: true });
|
||||
expect(OrchestratorSystem.Run).toHaveBeenCalledWith(
|
||||
expect.stringContaining('tar -cf'),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle save errors gracefully', async () => {
|
||||
(mockFs.existsSync as jest.Mock).mockReturnValue(true);
|
||||
(mockFs.readdirSync as jest.Mock).mockImplementation(() => {
|
||||
throw new Error('Disk full');
|
||||
});
|
||||
|
||||
// Should not throw
|
||||
await LocalCacheService.saveLfsCache('/repo', '/cache', 'key1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('garbageCollect', () => {
|
||||
|
||||
@@ -106,6 +106,18 @@ describe('GitHooksService', () => {
|
||||
expect(OrchestratorSystem.Run).toHaveBeenCalledWith(`cd "/repo" && npx lefthook install`, true);
|
||||
});
|
||||
|
||||
it('should run npx husky install when husky is detected', async () => {
|
||||
(mockFs.existsSync as jest.Mock).mockImplementation((filePath: string) => {
|
||||
return String(filePath).endsWith('.husky');
|
||||
});
|
||||
|
||||
const { OrchestratorSystem } = require('../core/orchestrator-system');
|
||||
|
||||
await GitHooksService.installHooks('/repo');
|
||||
|
||||
expect(OrchestratorSystem.Run).toHaveBeenCalledWith(`cd "/repo" && npx husky install`, true);
|
||||
});
|
||||
|
||||
it('should log and return when no framework is detected', async () => {
|
||||
(mockFs.existsSync as jest.Mock).mockReturnValue(false);
|
||||
|
||||
@@ -117,5 +129,45 @@ describe('GitHooksService', () => {
|
||||
expect(OrchestratorSystem.Run).not.toHaveBeenCalled();
|
||||
expect(OrchestratorLogger.log).toHaveBeenCalledWith(expect.stringContaining('No hook framework detected'));
|
||||
});
|
||||
|
||||
it('should log warning on installation failure', async () => {
|
||||
(mockFs.existsSync as jest.Mock).mockImplementation((filePath: string) => {
|
||||
return String(filePath).includes('lefthook.yml') && !String(filePath).startsWith('.');
|
||||
});
|
||||
|
||||
const { OrchestratorSystem } = require('../core/orchestrator-system');
|
||||
const OrchestratorLogger = require('../core/orchestrator-logger').default;
|
||||
OrchestratorSystem.Run.mockRejectedValue(new Error('npx not found'));
|
||||
|
||||
await GitHooksService.installHooks('/repo');
|
||||
|
||||
expect(OrchestratorLogger.logWarning).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Hook installation failed'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('disableHooks', () => {
|
||||
it('should log warning on failure to disable hooks', async () => {
|
||||
(mockFs.mkdirSync as jest.Mock).mockReturnValue(undefined);
|
||||
|
||||
const { OrchestratorSystem } = require('../core/orchestrator-system');
|
||||
const OrchestratorLogger = require('../core/orchestrator-logger').default;
|
||||
OrchestratorSystem.Run.mockRejectedValue(new Error('git config failed'));
|
||||
|
||||
await GitHooksService.disableHooks('/repo');
|
||||
|
||||
expect(OrchestratorLogger.logWarning).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Failed to disable hooks'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('configureSkipList edge cases', () => {
|
||||
it('should handle single hook in skip list', () => {
|
||||
const result = GitHooksService.configureSkipList(['commit-msg']);
|
||||
expect(result.LEFTHOOK_EXCLUDE).toBe('commit-msg');
|
||||
expect(result.HUSKY).toBe('0');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -80,6 +80,23 @@ describe('LfsAgentService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('configure with empty storagePaths', () => {
|
||||
it('should not set LFS_STORAGE_PATHS when storagePaths is empty', async () => {
|
||||
(mockFs.existsSync as jest.Mock).mockReturnValue(true);
|
||||
|
||||
const originalValue = process.env.LFS_STORAGE_PATHS;
|
||||
delete process.env.LFS_STORAGE_PATHS;
|
||||
|
||||
await LfsAgentService.configure('/usr/local/bin/agent', '', [], '/repo');
|
||||
|
||||
expect(process.env.LFS_STORAGE_PATHS).toBeUndefined();
|
||||
|
||||
if (originalValue !== undefined) {
|
||||
process.env.LFS_STORAGE_PATHS = originalValue;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('validate', () => {
|
||||
it('should return true when agent executable exists', async () => {
|
||||
(mockFs.existsSync as jest.Mock).mockReturnValue(true);
|
||||
@@ -92,5 +109,16 @@ describe('LfsAgentService', () => {
|
||||
const result = await LfsAgentService.validate('/nonexistent/agent');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should log warning when agent does not exist', async () => {
|
||||
(mockFs.existsSync as jest.Mock).mockReturnValue(false);
|
||||
const OrchestratorLogger = require('../core/orchestrator-logger').default;
|
||||
|
||||
await LfsAgentService.validate('/nonexistent/agent');
|
||||
|
||||
expect(OrchestratorLogger.logWarning).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Agent executable not found'),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user