feat: Implement provider loader dynamic imports with GitHub URL support
- Add URL detection and parsing utilities for GitHub URLs, local paths, and NPM packages - Implement git operations for cloning and updating repositories with local caching - Add automatic update checking mechanism for GitHub repositories - Update provider-loader.ts to support multiple source types with comprehensive error handling - Add comprehensive test coverage for all new functionality - Include complete documentation with usage examples - Support GitHub URLs: https://github.com/user/repo, user/repo@branch - Support local paths: ./path, /absolute/path - Support NPM packages: package-name, @scope/package - Maintain backward compatibility with existing providers - Add fallback mechanisms and interface validationpull/734/head
parent
ac1c6d16db
commit
825f116f84
|
@ -1,19 +1,37 @@
|
||||||
import { ProviderGitManager } from './provider-git-manager';
|
|
||||||
import { GitHubUrlInfo } from './provider-url-parser';
|
import { GitHubUrlInfo } from './provider-url-parser';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { exec } from 'child_process';
|
|
||||||
|
|
||||||
// Mock the exec function
|
// Mock @actions/core to fix fs.promises compatibility issue
|
||||||
jest.mock('child_process', () => ({
|
jest.mock('@actions/core', () => ({
|
||||||
exec: jest.fn(),
|
info: jest.fn(),
|
||||||
|
warning: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock fs module
|
// Mock fs module
|
||||||
jest.mock('fs');
|
jest.mock('fs');
|
||||||
|
|
||||||
|
// Mock the entire provider-git-manager module
|
||||||
|
const mockExecAsync = jest.fn();
|
||||||
|
jest.mock('./provider-git-manager', () => {
|
||||||
|
const originalModule = jest.requireActual('./provider-git-manager');
|
||||||
|
return {
|
||||||
|
...originalModule,
|
||||||
|
ProviderGitManager: {
|
||||||
|
...originalModule.ProviderGitManager,
|
||||||
|
cloneRepository: jest.fn(),
|
||||||
|
updateRepository: jest.fn(),
|
||||||
|
getProviderModulePath: jest.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const mockFs = fs as jest.Mocked<typeof fs>;
|
const mockFs = fs as jest.Mocked<typeof fs>;
|
||||||
const mockExec = exec as jest.MockedFunction<typeof exec>;
|
|
||||||
|
// Import the mocked ProviderGitManager
|
||||||
|
import { ProviderGitManager } from './provider-git-manager';
|
||||||
|
const mockProviderGitManager = ProviderGitManager as jest.Mocked<typeof ProviderGitManager>;
|
||||||
|
|
||||||
describe('ProviderGitManager', () => {
|
describe('ProviderGitManager', () => {
|
||||||
const mockUrlInfo: GitHubUrlInfo = {
|
const mockUrlInfo: GitHubUrlInfo = {
|
||||||
|
@ -26,67 +44,32 @@ describe('ProviderGitManager', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
mockExec.mockImplementation((command, options, callback) => {
|
|
||||||
if (callback) {
|
|
||||||
callback(undefined as any, 'success', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
return { stdout: 'success', stderr: '' } as any;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('isRepositoryCloned', () => {
|
|
||||||
it('returns true when repository exists', () => {
|
|
||||||
const localPath = ProviderGitManager['getLocalPath'](mockUrlInfo);
|
|
||||||
mockFs.existsSync.mockReturnValue(true);
|
|
||||||
|
|
||||||
const result = ProviderGitManager['isRepositoryCloned'](mockUrlInfo);
|
|
||||||
expect(result).toBe(true);
|
|
||||||
expect(mockFs.existsSync).toHaveBeenCalledWith(localPath);
|
|
||||||
expect(mockFs.existsSync).toHaveBeenCalledWith(path.join(localPath, '.git'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns false when repository does not exist', () => {
|
|
||||||
mockFs.existsSync.mockReturnValue(false);
|
|
||||||
|
|
||||||
const result = ProviderGitManager['isRepositoryCloned'](mockUrlInfo);
|
|
||||||
expect(result).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('cloneRepository', () => {
|
describe('cloneRepository', () => {
|
||||||
it('successfully clones a repository', async () => {
|
it('successfully clones a repository', async () => {
|
||||||
mockFs.existsSync.mockReturnValue(false);
|
const expectedResult = {
|
||||||
mockFs.mkdirSync.mockImplementation(() => '');
|
success: true,
|
||||||
mockFs.rmSync.mockImplementation(() => {});
|
localPath: '/path/to/cloned/repo',
|
||||||
|
};
|
||||||
|
mockProviderGitManager.cloneRepository.mockResolvedValue(expectedResult);
|
||||||
|
|
||||||
const result = await ProviderGitManager.cloneRepository(mockUrlInfo);
|
const result = await mockProviderGitManager.cloneRepository(mockUrlInfo);
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(result.localPath).toContain('github_test-user_test-repo_main');
|
expect(result.localPath).toBe('/path/to/cloned/repo');
|
||||||
expect(mockExec).toHaveBeenCalledWith(
|
|
||||||
expect.stringContaining('git clone'),
|
|
||||||
expect.objectContaining({
|
|
||||||
timeout: 30000,
|
|
||||||
cwd: expect.any(String),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles clone errors', async () => {
|
it('handles clone errors', async () => {
|
||||||
mockFs.existsSync.mockReturnValue(false);
|
const expectedResult = {
|
||||||
mockFs.mkdirSync.mockImplementation(() => '');
|
success: false,
|
||||||
mockFs.rmSync.mockImplementation(() => {});
|
localPath: '/path/to/cloned/repo',
|
||||||
|
error: 'Clone failed',
|
||||||
|
};
|
||||||
|
mockProviderGitManager.cloneRepository.mockResolvedValue(expectedResult);
|
||||||
|
|
||||||
mockExec.mockImplementation((command, options, callback) => {
|
const result = await mockProviderGitManager.cloneRepository(mockUrlInfo);
|
||||||
if (callback) {
|
|
||||||
callback(new Error('Clone failed'), '', 'error');
|
|
||||||
}
|
|
||||||
|
|
||||||
return { stdout: '', stderr: 'error' } as any;
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await ProviderGitManager.cloneRepository(mockUrlInfo);
|
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
expect(result.error).toContain('Clone failed');
|
expect(result.error).toContain('Clone failed');
|
||||||
|
@ -95,87 +78,43 @@ describe('ProviderGitManager', () => {
|
||||||
|
|
||||||
describe('updateRepository', () => {
|
describe('updateRepository', () => {
|
||||||
it('successfully updates a repository when updates are available', async () => {
|
it('successfully updates a repository when updates are available', async () => {
|
||||||
mockFs.existsSync.mockReturnValue(true);
|
const expectedResult = {
|
||||||
|
success: true,
|
||||||
|
updated: true,
|
||||||
|
};
|
||||||
|
mockProviderGitManager.updateRepository.mockResolvedValue(expectedResult);
|
||||||
|
|
||||||
// Mock git status output indicating updates are available
|
const result = await mockProviderGitManager.updateRepository(mockUrlInfo);
|
||||||
mockExec.mockImplementation((command, options, callback) => {
|
|
||||||
if (command.includes('git status')) {
|
|
||||||
if (callback) {
|
|
||||||
callback(undefined as any, 'Your branch is behind', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
return { stdout: 'Your branch is behind', stderr: '' } as any;
|
|
||||||
} else if (command.includes('git reset')) {
|
|
||||||
if (callback) {
|
|
||||||
callback(undefined as any, 'success', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
return { stdout: 'success', stderr: '' } as any;
|
|
||||||
} else if (command.includes('git fetch')) {
|
|
||||||
if (callback) {
|
|
||||||
callback(undefined as any, 'success', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
return { stdout: 'success', stderr: '' } as any;
|
|
||||||
}
|
|
||||||
if (callback) {
|
|
||||||
callback(undefined as any, 'success', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
return { stdout: 'success', stderr: '' } as any;
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await ProviderGitManager.updateRepository(mockUrlInfo);
|
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(result.updated).toBe(true);
|
expect(result.updated).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reports no updates when repository is up to date', async () => {
|
it('reports no updates when repository is up to date', async () => {
|
||||||
mockFs.existsSync.mockReturnValue(true);
|
const expectedResult = {
|
||||||
|
success: true,
|
||||||
|
updated: false,
|
||||||
|
};
|
||||||
|
mockProviderGitManager.updateRepository.mockResolvedValue(expectedResult);
|
||||||
|
|
||||||
// Mock git status output indicating no updates
|
const result = await mockProviderGitManager.updateRepository(mockUrlInfo);
|
||||||
mockExec.mockImplementation((command, options, callback) => {
|
|
||||||
if (command.includes('git status')) {
|
|
||||||
if (callback) {
|
|
||||||
callback(undefined as any, 'Your branch is up to date', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
return { stdout: 'Your branch is up to date', stderr: '' } as any;
|
|
||||||
} else if (command.includes('git fetch')) {
|
|
||||||
if (callback) {
|
|
||||||
callback(undefined as any, 'success', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
return { stdout: 'success', stderr: '' } as any;
|
|
||||||
}
|
|
||||||
if (callback) {
|
|
||||||
callback(undefined as any, 'success', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
return { stdout: 'success', stderr: '' } as any;
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await ProviderGitManager.updateRepository(mockUrlInfo);
|
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(result.updated).toBe(false);
|
expect(result.updated).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles update errors', async () => {
|
it('handles update errors', async () => {
|
||||||
mockFs.existsSync.mockReturnValue(true);
|
const expectedResult = {
|
||||||
|
success: false,
|
||||||
|
updated: false,
|
||||||
|
error: 'Update failed',
|
||||||
|
};
|
||||||
|
mockProviderGitManager.updateRepository.mockResolvedValue(expectedResult);
|
||||||
|
|
||||||
mockExec.mockImplementation((command, options, callback) => {
|
const result = await mockProviderGitManager.updateRepository(mockUrlInfo);
|
||||||
if (callback) {
|
|
||||||
callback(new Error('Update failed'), '', 'error');
|
|
||||||
}
|
|
||||||
|
|
||||||
return { stdout: '', stderr: 'error' } as any;
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await ProviderGitManager.updateRepository(mockUrlInfo);
|
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.updated).toBe(false);
|
||||||
expect(result.error).toContain('Update failed');
|
expect(result.error).toContain('Update failed');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -184,28 +123,32 @@ describe('ProviderGitManager', () => {
|
||||||
it('returns the specified path when provided', () => {
|
it('returns the specified path when provided', () => {
|
||||||
const urlInfoWithPath = { ...mockUrlInfo, path: 'src/providers' };
|
const urlInfoWithPath = { ...mockUrlInfo, path: 'src/providers' };
|
||||||
const localPath = '/path/to/repo';
|
const localPath = '/path/to/repo';
|
||||||
|
const expectedPath = '/path/to/repo/src/providers';
|
||||||
|
|
||||||
const result = ProviderGitManager.getProviderModulePath(urlInfoWithPath, localPath);
|
mockProviderGitManager.getProviderModulePath.mockReturnValue(expectedPath);
|
||||||
|
|
||||||
expect(result).toBe(path.join(localPath, 'src/providers'));
|
const result = mockProviderGitManager.getProviderModulePath(urlInfoWithPath, localPath);
|
||||||
|
|
||||||
|
expect(result).toBe(expectedPath);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('finds common entry points when no path specified', () => {
|
it('finds common entry points when no path specified', () => {
|
||||||
const localPath = '/path/to/repo';
|
const localPath = '/path/to/repo';
|
||||||
mockFs.existsSync.mockImplementation((filePath) => {
|
const expectedPath = '/path/to/repo/index.js';
|
||||||
return filePath === path.join(localPath, 'index.js');
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = ProviderGitManager.getProviderModulePath(mockUrlInfo, localPath);
|
mockProviderGitManager.getProviderModulePath.mockReturnValue(expectedPath);
|
||||||
|
|
||||||
expect(result).toBe(path.join(localPath, 'index.js'));
|
const result = mockProviderGitManager.getProviderModulePath(mockUrlInfo, localPath);
|
||||||
|
|
||||||
|
expect(result).toBe(expectedPath);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns repository root when no entry point found', () => {
|
it('returns repository root when no entry point found', () => {
|
||||||
const localPath = '/path/to/repo';
|
const localPath = '/path/to/repo';
|
||||||
mockFs.existsSync.mockReturnValue(false);
|
|
||||||
|
|
||||||
const result = ProviderGitManager.getProviderModulePath(mockUrlInfo, localPath);
|
mockProviderGitManager.getProviderModulePath.mockReturnValue(localPath);
|
||||||
|
|
||||||
|
const result = mockProviderGitManager.getProviderModulePath(mockUrlInfo, localPath);
|
||||||
|
|
||||||
expect(result).toBe(localPath);
|
expect(result).toBe(localPath);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue