WIP
This commit is contained in:
parent
ce388e07e4
commit
6568ff640d
@ -28,9 +28,9 @@ export function formatHttpResponse(results: ProcessResult[]): HttpResponse {
|
||||
const projects: ProjectSummary[] = results.map(result => {
|
||||
// Count workitems
|
||||
const workitemsProcessed = result.processedWorkitems.length;
|
||||
const workitemsSkipped = result.processedWorkitems.filter(w => !w.workitem.isActive).length;
|
||||
const workitemsUpdated = result.processedWorkitems.filter(w => w.success).length;
|
||||
const workitemsCreated = result.processedWorkitems.filter(w => w.success && w.status === 'created').length;
|
||||
const workitemsSkipped = result.processedWorkitems.filter(w => w.success && w.status === "skip").length;
|
||||
const workitemsUpdated = result.processedWorkitems.filter(w => w.success && w.status === "update").length;
|
||||
const workitemsCreated = result.processedWorkitems.filter(w => w.success && w.status === 'create').length;
|
||||
const filesWritten = result.processedWorkitems.reduce((sum, w) => sum + (w.filesWritten?.length || 0), 0);
|
||||
|
||||
return {
|
||||
|
@ -21,7 +21,8 @@ describe('formatHttpResponse', () => {
|
||||
description: 'Description 1',
|
||||
isActive: true
|
||||
},
|
||||
success: true
|
||||
success: true,
|
||||
status: 'update'
|
||||
},
|
||||
{
|
||||
workitem: {
|
||||
@ -31,7 +32,8 @@ describe('formatHttpResponse', () => {
|
||||
description: 'Description 2',
|
||||
isActive: false
|
||||
},
|
||||
success: true
|
||||
success: true,
|
||||
status: 'update'
|
||||
}
|
||||
],
|
||||
pullRequestUrl: 'https://github.com/org/project1/pull/123'
|
||||
@ -74,8 +76,9 @@ describe('formatHttpResponse', () => {
|
||||
{
|
||||
name: 'project1',
|
||||
success: true,
|
||||
error: undefined,
|
||||
workitemsProcessed: 2,
|
||||
workitemsSkipped: 1,
|
||||
workitemsSkipped: 0,
|
||||
workitemsUpdated: 2,
|
||||
workitemsCreated: 0,
|
||||
filesWritten: 0,
|
||||
@ -89,7 +92,8 @@ describe('formatHttpResponse', () => {
|
||||
workitemsSkipped: 0,
|
||||
workitemsUpdated: 0,
|
||||
workitemsCreated: 0,
|
||||
filesWritten: 0
|
||||
filesWritten: 0,
|
||||
pullRequestUrl: undefined
|
||||
}
|
||||
]
|
||||
});
|
||||
|
@ -1,269 +1,267 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { ProcessorService } from '../processor-service';
|
||||
import { ProjectService } from '../project-service';
|
||||
import { Project, Workitem, ProcessResult } from '../../types';
|
||||
import {ProcessorService} from '../processor-service';
|
||||
import {ProjectService} from '../project-service';
|
||||
import {ProcessResult, Workitem} from '../../types';
|
||||
import {
|
||||
RepositoryService as SharedRepositoryService,
|
||||
PullRequestService as SharedPullRequestService,
|
||||
GeminiService
|
||||
GeminiService, Project,
|
||||
PullRequestService as SharedPullRequestService,
|
||||
RepositoryService as SharedRepositoryService
|
||||
} from 'shared-functions';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('../project-service');
|
||||
jest.mock('shared-functions');
|
||||
jest.mock('../../config', () => ({
|
||||
validateConfig: jest.fn(),
|
||||
getMainRepoCredentials: jest.fn().mockReturnValue({ type: 'token', token: 'mock-token' }),
|
||||
getGithubCredentials: jest.fn().mockReturnValue({ type: 'token', token: 'mock-token' }),
|
||||
getGiteaCredentials: jest.fn().mockReturnValue({ type: 'token', token: 'mock-token' }),
|
||||
MAIN_REPO_URL: 'https://github.com/org/main-repo.git',
|
||||
GOOGLE_CLOUD_PROJECT_ID: 'mock-project-id',
|
||||
GOOGLE_CLOUD_LOCATION: 'mock-location',
|
||||
GEMINI_MODEL: 'mock-model',
|
||||
USE_LOCAL_REPO: false,
|
||||
DRY_RUN_SKIP_COMMITS: false,
|
||||
DRY_RUN_SKIP_GEMINI: false
|
||||
validateConfig: jest.fn(),
|
||||
getMainRepoCredentials: jest.fn().mockReturnValue({type: 'token', token: 'mock-token'}),
|
||||
getGithubCredentials: jest.fn().mockReturnValue({type: 'token', token: 'mock-token'}),
|
||||
getGiteaCredentials: jest.fn().mockReturnValue({type: 'token', token: 'mock-token'}),
|
||||
MAIN_REPO_URL: 'https://github.com/org/main-repo.git',
|
||||
GOOGLE_CLOUD_PROJECT_ID: 'mock-project-id',
|
||||
GOOGLE_CLOUD_LOCATION: 'mock-location',
|
||||
GEMINI_MODEL: 'mock-model',
|
||||
USE_LOCAL_REPO: false,
|
||||
DRY_RUN_SKIP_COMMITS: false,
|
||||
DRY_RUN_SKIP_GEMINI: false
|
||||
}));
|
||||
|
||||
describe('ProcessorService', () => {
|
||||
let processorService: ProcessorService;
|
||||
let mockProjectService: jest.Mocked<ProjectService>;
|
||||
let mockSharedRepositoryService: jest.Mocked<SharedRepositoryService>;
|
||||
let mockSharedPullRequestService: jest.Mocked<SharedPullRequestService>;
|
||||
let mockGeminiService: jest.Mocked<GeminiService>;
|
||||
let processorService: ProcessorService;
|
||||
let mockProjectService: jest.Mocked<ProjectService>;
|
||||
let mockSharedRepositoryService: jest.Mocked<SharedRepositoryService>;
|
||||
let mockSharedPullRequestService: jest.Mocked<SharedPullRequestService>;
|
||||
let mockGeminiService: jest.Mocked<GeminiService>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
processorService = new ProcessorService();
|
||||
mockProjectService = ProjectService.prototype as jest.Mocked<ProjectService>;
|
||||
mockSharedRepositoryService = SharedRepositoryService.prototype as jest.Mocked<SharedRepositoryService>;
|
||||
mockSharedPullRequestService = SharedPullRequestService.prototype as jest.Mocked<SharedPullRequestService>;
|
||||
mockGeminiService = GeminiService.prototype as jest.Mocked<GeminiService>;
|
||||
});
|
||||
|
||||
describe('updateWorkitemFilesWithPullRequestUrls', () => {
|
||||
it('should update workitem files with pull request URLs and commit changes', async () => {
|
||||
// Create test data
|
||||
const mainRepoPath = '/path/to/main/repo';
|
||||
const project: Project = {
|
||||
name: 'test-project',
|
||||
path: '/path/to/project',
|
||||
repoHost: 'https://github.com',
|
||||
repoUrl: 'https://github.com/org/test-project.git'
|
||||
};
|
||||
|
||||
const workitem1: Workitem = {
|
||||
name: 'workitem1',
|
||||
path: '/path/to/workitem1.md',
|
||||
title: 'Workitem 1',
|
||||
description: 'Description 1',
|
||||
isActive: true
|
||||
};
|
||||
|
||||
const workitem2: Workitem = {
|
||||
name: 'workitem2',
|
||||
path: '/path/to/workitem2.md',
|
||||
title: 'Workitem 2',
|
||||
description: 'Description 2',
|
||||
isActive: true
|
||||
};
|
||||
|
||||
const results: ProcessResult[] = [
|
||||
{
|
||||
project,
|
||||
processedWorkitems: [
|
||||
{ workitem: workitem1, success: true, status: 'updated', filesWritten: [] },
|
||||
{ workitem: workitem2, success: true, status: 'updated', filesWritten: [] }
|
||||
],
|
||||
pullRequestUrl: 'https://github.com/org/test-project/pull/123',
|
||||
gitPatch: 'mock-git-patch'
|
||||
}
|
||||
];
|
||||
|
||||
// Mock the updateWorkitemWithPullRequestUrl method
|
||||
mockProjectService.updateWorkitemWithPullRequestUrl.mockImplementation(
|
||||
async (workitem, pullRequestUrl) => {
|
||||
return { ...workitem, pullRequestUrl };
|
||||
}
|
||||
);
|
||||
|
||||
// Call the method
|
||||
await (processorService as any).updateWorkitemFilesWithPullRequestUrls(results, mainRepoPath);
|
||||
|
||||
// Verify the method calls
|
||||
expect(mockSharedRepositoryService.createBranch).toHaveBeenCalledWith(
|
||||
mainRepoPath,
|
||||
expect.stringMatching(/update-workitem-pr-urls-\d{4}-\d{2}-\d{2}/)
|
||||
);
|
||||
|
||||
expect(mockProjectService.updateWorkitemWithPullRequestUrl).toHaveBeenCalledTimes(2);
|
||||
expect(mockProjectService.updateWorkitemWithPullRequestUrl).toHaveBeenCalledWith(
|
||||
workitem1,
|
||||
'https://github.com/org/test-project/pull/123'
|
||||
);
|
||||
expect(mockProjectService.updateWorkitemWithPullRequestUrl).toHaveBeenCalledWith(
|
||||
workitem2,
|
||||
'https://github.com/org/test-project/pull/123'
|
||||
);
|
||||
|
||||
expect(mockSharedRepositoryService.commitChanges).toHaveBeenCalledWith(
|
||||
mainRepoPath,
|
||||
expect.stringMatching(/Update workitem files with pull request URLs: \d{4}-\d{2}-\d{2}/)
|
||||
);
|
||||
|
||||
expect(mockSharedRepositoryService.pushChanges).toHaveBeenCalledWith(
|
||||
mainRepoPath,
|
||||
expect.stringMatching(/update-workitem-pr-urls-\d{4}-\d{2}-\d{2}/),
|
||||
expect.anything()
|
||||
);
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
processorService = new ProcessorService();
|
||||
mockProjectService = ProjectService.prototype as jest.Mocked<ProjectService>;
|
||||
mockSharedRepositoryService = SharedRepositoryService.prototype as jest.Mocked<SharedRepositoryService>;
|
||||
mockSharedPullRequestService = SharedPullRequestService.prototype as jest.Mocked<SharedPullRequestService>;
|
||||
mockGeminiService = GeminiService.prototype as jest.Mocked<GeminiService>;
|
||||
});
|
||||
|
||||
it('should handle deactivated workitems correctly', async () => {
|
||||
// Create test data
|
||||
const mainRepoPath = '/path/to/main/repo';
|
||||
const project: Project = {
|
||||
name: 'test-project',
|
||||
path: '/path/to/project',
|
||||
repoHost: 'https://github.com',
|
||||
repoUrl: 'https://github.com/org/test-project.git'
|
||||
};
|
||||
describe('updateWorkitemFilesWithPullRequestUrls', () => {
|
||||
it('should update workitem files with pull request URLs and commit changes', async () => {
|
||||
// Create test data
|
||||
const mainRepoPath = '/path/to/main/repo';
|
||||
const project: Project = {
|
||||
name: 'test-project',
|
||||
path: '/path/to/project',
|
||||
repoHost: 'https://github.com',
|
||||
repoUrl: 'https://github.com/org/test-project.git'
|
||||
};
|
||||
|
||||
const activeWorkitem: Workitem = {
|
||||
name: 'active-workitem',
|
||||
path: '/path/to/active-workitem.md',
|
||||
title: 'Active Workitem',
|
||||
description: 'This is an active workitem',
|
||||
isActive: true
|
||||
};
|
||||
const workitem1: Workitem = {
|
||||
name: 'workitem1',
|
||||
path: '/path/to/workitem1.md',
|
||||
title: 'Workitem 1',
|
||||
description: 'Description 1',
|
||||
isActive: true
|
||||
};
|
||||
|
||||
const deactivatedWorkitem: Workitem = {
|
||||
name: 'deactivated-workitem',
|
||||
path: '/path/to/deactivated-workitem.md',
|
||||
title: 'Deactivated Workitem',
|
||||
description: 'This is a deactivated workitem',
|
||||
isActive: false
|
||||
};
|
||||
const workitem2: Workitem = {
|
||||
name: 'workitem2',
|
||||
path: '/path/to/workitem2.md',
|
||||
title: 'Workitem 2',
|
||||
description: 'Description 2',
|
||||
isActive: true
|
||||
};
|
||||
|
||||
const results: ProcessResult[] = [
|
||||
{
|
||||
project,
|
||||
processedWorkitems: [
|
||||
{ workitem: activeWorkitem, success: true, status: 'updated', filesWritten: [] },
|
||||
{ workitem: deactivatedWorkitem, success: true, status: 'skipped', filesWritten: [] }
|
||||
],
|
||||
pullRequestUrl: 'https://github.com/org/test-project/pull/123'
|
||||
}
|
||||
];
|
||||
const results: ProcessResult[] = [
|
||||
{
|
||||
project,
|
||||
processedWorkitems: [
|
||||
{workitem: workitem1, success: true, status: 'update', filesWritten: []},
|
||||
{workitem: workitem2, success: true, status: 'update', filesWritten: []}
|
||||
],
|
||||
pullRequestUrl: 'https://github.com/org/test-project/pull/123',
|
||||
gitPatch: 'mock-git-patch'
|
||||
}
|
||||
];
|
||||
|
||||
// Mock the updateWorkitemWithPullRequestUrl method
|
||||
mockProjectService.updateWorkitemWithPullRequestUrl.mockImplementation(
|
||||
async (workitem, pullRequestUrl) => {
|
||||
return { ...workitem, pullRequestUrl };
|
||||
}
|
||||
);
|
||||
// Mock the updateWorkitemWithPullRequestUrl method
|
||||
mockProjectService.updateWorkitemWithPullRequestUrl.mockImplementation(
|
||||
async (workitem, pullRequestUrl) => {
|
||||
return {...workitem, pullRequestUrl};
|
||||
}
|
||||
);
|
||||
|
||||
// Call the method
|
||||
await (processorService as any).updateWorkitemFilesWithPullRequestUrls(results, mainRepoPath);
|
||||
// Call the method
|
||||
await (processorService as any).updateWorkitemFilesWithPullRequestUrls(results, mainRepoPath);
|
||||
|
||||
// Verify the method calls
|
||||
expect(mockSharedRepositoryService.createBranch).toHaveBeenCalledWith(
|
||||
mainRepoPath,
|
||||
expect.stringMatching(/update-workitem-pr-urls-\d{4}-\d{2}-\d{2}/)
|
||||
);
|
||||
// Verify the method calls
|
||||
expect(mockSharedRepositoryService.createBranch).toHaveBeenCalledWith(
|
||||
mainRepoPath,
|
||||
expect.stringMatching(/update-workitem-pr-urls-\d{4}-\d{2}-\d{2}/)
|
||||
);
|
||||
|
||||
// Should only update the active workitem
|
||||
expect(mockProjectService.updateWorkitemWithPullRequestUrl).toHaveBeenCalledTimes(2);
|
||||
expect(mockProjectService.updateWorkitemWithPullRequestUrl).toHaveBeenCalledWith(
|
||||
activeWorkitem,
|
||||
'https://github.com/org/test-project/pull/123'
|
||||
);
|
||||
expect(mockProjectService.updateWorkitemWithPullRequestUrl).toHaveBeenCalledWith(
|
||||
deactivatedWorkitem,
|
||||
'https://github.com/org/test-project/pull/123'
|
||||
);
|
||||
expect(mockProjectService.updateWorkitemWithPullRequestUrl).toHaveBeenCalledTimes(2);
|
||||
expect(mockProjectService.updateWorkitemWithPullRequestUrl).toHaveBeenCalledWith(
|
||||
workitem1,
|
||||
'https://github.com/org/test-project/pull/123'
|
||||
);
|
||||
expect(mockProjectService.updateWorkitemWithPullRequestUrl).toHaveBeenCalledWith(
|
||||
workitem2,
|
||||
'https://github.com/org/test-project/pull/123'
|
||||
);
|
||||
|
||||
expect(mockSharedRepositoryService.commitChanges).toHaveBeenCalledWith(
|
||||
mainRepoPath,
|
||||
expect.stringMatching(/Update workitem files with pull request URLs: \d{4}-\d{2}-\d{2}/)
|
||||
);
|
||||
expect(mockSharedRepositoryService.commitChanges).toHaveBeenCalledWith(
|
||||
mainRepoPath,
|
||||
expect.stringMatching(/Update workitem files with pull request URLs: \d{4}-\d{2}-\d{2}/)
|
||||
);
|
||||
|
||||
expect(mockSharedRepositoryService.pushChanges).toHaveBeenCalledWith(
|
||||
mainRepoPath,
|
||||
expect.stringMatching(/update-workitem-pr-urls-\d{4}-\d{2}-\d{2}/),
|
||||
expect.anything()
|
||||
);
|
||||
expect(mockSharedRepositoryService.pushChanges).toHaveBeenCalledWith(
|
||||
mainRepoPath,
|
||||
expect.stringMatching(/update-workitem-pr-urls-\d{4}-\d{2}-\d{2}/),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle deactivated workitems correctly', async () => {
|
||||
// Create test data
|
||||
const mainRepoPath = '/path/to/main/repo';
|
||||
const project: Project = {
|
||||
name: 'test-project',
|
||||
path: '/path/to/project',
|
||||
repoHost: 'https://github.com',
|
||||
repoUrl: 'https://github.com/org/test-project.git'
|
||||
};
|
||||
|
||||
const activeWorkitem: Workitem = {
|
||||
name: 'active-workitem',
|
||||
path: '/path/to/active-workitem.md',
|
||||
title: 'Active Workitem',
|
||||
description: 'This is an active workitem',
|
||||
isActive: true
|
||||
};
|
||||
|
||||
const deactivatedWorkitem: Workitem = {
|
||||
name: 'deactivated-workitem',
|
||||
path: '/path/to/deactivated-workitem.md',
|
||||
title: 'Deactivated Workitem',
|
||||
description: 'This is a deactivated workitem',
|
||||
isActive: false
|
||||
};
|
||||
|
||||
const results: ProcessResult[] = [
|
||||
{
|
||||
project,
|
||||
processedWorkitems: [
|
||||
{workitem: activeWorkitem, success: true, status: 'update', filesWritten: []},
|
||||
{workitem: deactivatedWorkitem, success: true, status: 'skip', filesWritten: []}
|
||||
],
|
||||
pullRequestUrl: 'https://github.com/org/test-project/pull/123'
|
||||
}
|
||||
];
|
||||
|
||||
// Mock the updateWorkitemWithPullRequestUrl method
|
||||
mockProjectService.updateWorkitemWithPullRequestUrl.mockImplementation(
|
||||
async (workitem, pullRequestUrl) => {
|
||||
return {...workitem, pullRequestUrl};
|
||||
}
|
||||
);
|
||||
|
||||
// Call the method
|
||||
await (processorService as any).updateWorkitemFilesWithPullRequestUrls(results, mainRepoPath);
|
||||
|
||||
// Verify the method calls
|
||||
expect(mockSharedRepositoryService.createBranch).toHaveBeenCalledWith(
|
||||
mainRepoPath,
|
||||
expect.stringMatching(/update-workitem-pr-urls-\d{4}-\d{2}-\d{2}/)
|
||||
);
|
||||
|
||||
// Should only update the active workitem
|
||||
expect(mockProjectService.updateWorkitemWithPullRequestUrl).toHaveBeenCalledTimes(2);
|
||||
expect(mockProjectService.updateWorkitemWithPullRequestUrl).toHaveBeenCalledWith(
|
||||
activeWorkitem,
|
||||
'https://github.com/org/test-project/pull/123'
|
||||
);
|
||||
expect(mockProjectService.updateWorkitemWithPullRequestUrl).toHaveBeenCalledWith(
|
||||
deactivatedWorkitem,
|
||||
'https://github.com/org/test-project/pull/123'
|
||||
);
|
||||
|
||||
expect(mockSharedRepositoryService.commitChanges).toHaveBeenCalledWith(
|
||||
mainRepoPath,
|
||||
expect.stringMatching(/Update workitem files with pull request URLs: \d{4}-\d{2}-\d{2}/)
|
||||
);
|
||||
|
||||
expect(mockSharedRepositoryService.pushChanges).toHaveBeenCalledWith(
|
||||
mainRepoPath,
|
||||
expect.stringMatching(/update-workitem-pr-urls-\d{4}-\d{2}-\d{2}/),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
|
||||
it('should not commit changes if no workitems were updated', async () => {
|
||||
// Create test data with no pull request URL
|
||||
const mainRepoPath = '/path/to/main/repo';
|
||||
const project: Project = {
|
||||
name: 'test-project',
|
||||
path: '/path/to/project',
|
||||
repoHost: 'https://github.com',
|
||||
repoUrl: 'https://github.com/org/test-project.git'
|
||||
};
|
||||
|
||||
const results: ProcessResult[] = [
|
||||
{
|
||||
project,
|
||||
processedWorkitems: [],
|
||||
// No pull request URL
|
||||
}
|
||||
];
|
||||
|
||||
// Call the method
|
||||
await (processorService as any).updateWorkitemFilesWithPullRequestUrls(results, mainRepoPath);
|
||||
|
||||
// Verify the method calls
|
||||
expect(mockSharedRepositoryService.createBranch).toHaveBeenCalled();
|
||||
expect(mockProjectService.updateWorkitemWithPullRequestUrl).not.toHaveBeenCalled();
|
||||
expect(mockSharedRepositoryService.commitChanges).not.toHaveBeenCalled();
|
||||
expect(mockSharedRepositoryService.pushChanges).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle errors when updating workitem files', async () => {
|
||||
// Create test data
|
||||
const mainRepoPath = '/path/to/main/repo';
|
||||
const project: Project = {
|
||||
name: 'test-project',
|
||||
path: '/path/to/project',
|
||||
repoHost: 'https://github.com',
|
||||
repoUrl: 'https://github.com/org/test-project.git'
|
||||
};
|
||||
|
||||
const workitem: Workitem = {
|
||||
name: 'workitem',
|
||||
path: '/path/to/workitem.md',
|
||||
title: 'Workitem',
|
||||
description: 'Description',
|
||||
isActive: true
|
||||
};
|
||||
|
||||
const results: ProcessResult[] = [
|
||||
{
|
||||
project,
|
||||
processedWorkitems: [
|
||||
{workitem, success: true}
|
||||
],
|
||||
pullRequestUrl: 'https://github.com/org/test-project/pull/123'
|
||||
}
|
||||
];
|
||||
|
||||
// Mock the updateWorkitemWithPullRequestUrl method to throw an error
|
||||
mockProjectService.updateWorkitemWithPullRequestUrl.mockRejectedValueOnce(
|
||||
new Error('Failed to update workitem')
|
||||
);
|
||||
|
||||
// Call the method
|
||||
await (processorService as any).updateWorkitemFilesWithPullRequestUrls(results, mainRepoPath);
|
||||
|
||||
// Verify the method calls
|
||||
expect(mockSharedRepositoryService.createBranch).toHaveBeenCalled();
|
||||
expect(mockProjectService.updateWorkitemWithPullRequestUrl).toHaveBeenCalled();
|
||||
expect(mockSharedRepositoryService.commitChanges).not.toHaveBeenCalled();
|
||||
expect(mockSharedRepositoryService.pushChanges).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not commit changes if no workitems were updated', async () => {
|
||||
// Create test data with no pull request URL
|
||||
const mainRepoPath = '/path/to/main/repo';
|
||||
const project: Project = {
|
||||
name: 'test-project',
|
||||
path: '/path/to/project',
|
||||
repoHost: 'https://github.com',
|
||||
repoUrl: 'https://github.com/org/test-project.git'
|
||||
};
|
||||
|
||||
const results: ProcessResult[] = [
|
||||
{
|
||||
project,
|
||||
processedWorkitems: [],
|
||||
// No pull request URL
|
||||
}
|
||||
];
|
||||
|
||||
// Call the method
|
||||
await (processorService as any).updateWorkitemFilesWithPullRequestUrls(results, mainRepoPath);
|
||||
|
||||
// Verify the method calls
|
||||
expect(mockSharedRepositoryService.createBranch).toHaveBeenCalled();
|
||||
expect(mockProjectService.updateWorkitemWithPullRequestUrl).not.toHaveBeenCalled();
|
||||
expect(mockSharedRepositoryService.commitChanges).not.toHaveBeenCalled();
|
||||
expect(mockSharedRepositoryService.pushChanges).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle errors when updating workitem files', async () => {
|
||||
// Create test data
|
||||
const mainRepoPath = '/path/to/main/repo';
|
||||
const project: Project = {
|
||||
name: 'test-project',
|
||||
path: '/path/to/project',
|
||||
repoHost: 'https://github.com',
|
||||
repoUrl: 'https://github.com/org/test-project.git'
|
||||
};
|
||||
|
||||
const workitem: Workitem = {
|
||||
name: 'workitem',
|
||||
path: '/path/to/workitem.md',
|
||||
title: 'Workitem',
|
||||
description: 'Description',
|
||||
isActive: true
|
||||
};
|
||||
|
||||
const results: ProcessResult[] = [
|
||||
{
|
||||
project,
|
||||
processedWorkitems: [
|
||||
{ workitem, success: true }
|
||||
],
|
||||
pullRequestUrl: 'https://github.com/org/test-project/pull/123'
|
||||
}
|
||||
];
|
||||
|
||||
// Mock the updateWorkitemWithPullRequestUrl method to throw an error
|
||||
mockProjectService.updateWorkitemWithPullRequestUrl.mockRejectedValueOnce(
|
||||
new Error('Failed to update workitem')
|
||||
);
|
||||
|
||||
// Call the method
|
||||
await (processorService as any).updateWorkitemFilesWithPullRequestUrls(results, mainRepoPath);
|
||||
|
||||
// Verify the method calls
|
||||
expect(mockSharedRepositoryService.createBranch).toHaveBeenCalled();
|
||||
expect(mockProjectService.updateWorkitemWithPullRequestUrl).toHaveBeenCalled();
|
||||
expect(mockSharedRepositoryService.commitChanges).not.toHaveBeenCalled();
|
||||
expect(mockSharedRepositoryService.pushChanges).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -3,11 +3,11 @@
|
||||
*/
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import {ProcessResult, Project, RepoCredentials} from '../types';
|
||||
import {ProcessResult, RepoCredentials} from '../types';
|
||||
import {
|
||||
RepositoryService as SharedRepositoryService,
|
||||
PullRequestService as SharedPullRequestService,
|
||||
GeminiService
|
||||
GeminiService, Project
|
||||
} from 'shared-functions';
|
||||
import {ProjectService} from './project-service';
|
||||
import {ProjectWorkitemsService} from './project-workitems-service';
|
||||
@ -284,8 +284,11 @@ export class ProcessorService {
|
||||
await this.sharedRepositoryService.pushChanges(projectRepoPath, branchName, credentials);
|
||||
|
||||
// Generate PR description using Gemini
|
||||
const workItemsSummary = result.processedWorkitems
|
||||
.map(item => `${item.workitem.name}: ${item.status} (${item.filesWritten?.length ?? 0} written, ${item.filesRemoved?.length ?? 0} removed)`)
|
||||
.reduce((acc, item) => `${acc}\n${item}`, '');
|
||||
const description = await this.geminiService.generatePullRequestDescription(
|
||||
result.processedWorkitems,
|
||||
workItemsSummary,
|
||||
result.gitPatch
|
||||
);
|
||||
|
||||
|
@ -3,8 +3,8 @@
|
||||
*/
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import {ProjectService as SharedProjectService, Project, Workitem} from 'shared-functions';
|
||||
import { WorkitemImplementationStatus } from '../types';
|
||||
import {Project, ProjectService as SharedProjectService} from 'shared-functions';
|
||||
import {Workitem, WorkitemImplementationStatus} from '../types';
|
||||
|
||||
export class ProjectService {
|
||||
private sharedProjectService: SharedProjectService;
|
||||
|
@ -3,14 +3,13 @@
|
||||
*/
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import {ProcessResult} from '../types';
|
||||
import {ProcessedWorkItem, ProcessResult, Workitem} from '../types';
|
||||
import {ProjectService} from './project-service';
|
||||
import {DRY_RUN_SKIP_GEMINI} from '../config';
|
||||
import {
|
||||
GeminiFileSystemService,
|
||||
Project,
|
||||
Workitem,
|
||||
RepositoryService as SharedRepositoryService
|
||||
RepositoryService as SharedRepositoryService,
|
||||
} from 'shared-functions';
|
||||
|
||||
export class ProjectWorkitemsService {
|
||||
@ -50,11 +49,10 @@ export class ProjectWorkitemsService {
|
||||
const projectGuidelines = await this.projectService.readProjectGuidelines(project.path);
|
||||
|
||||
// Process each workitem
|
||||
const processedWorkitems = [];
|
||||
const processedWorkitems: ProcessedWorkItem[] = [];
|
||||
for (const workitem of workitems) {
|
||||
console.log(`ProjectWorkitemsService: Processing workitem: ${workitem.name}`);
|
||||
const result = await this.processWorkitem(project, projectRepoPath, workitem, projectGuidelines);
|
||||
processedWorkitems.push({workitem, ...result});
|
||||
const result: ProcessedWorkItem = await this.processWorkitem(project, projectRepoPath, workitem, projectGuidelines);
|
||||
processedWorkitems.push(result);
|
||||
}
|
||||
|
||||
// Generate git patch if any files were written
|
||||
@ -98,13 +96,7 @@ export class ProjectWorkitemsService {
|
||||
projectRepoPath: string,
|
||||
workitem: Workitem,
|
||||
projectGuidelines: string
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
decision?: 'skip' | 'update' | 'create' | 'delete';
|
||||
filesWritten?: string[],
|
||||
filesRemoved?: string[],
|
||||
}> {
|
||||
): Promise<ProcessedWorkItem> {
|
||||
try {
|
||||
// Set the current workitem
|
||||
console.log(`ProjectWorkitemsService: Processing workitem: ${workitem.name} (Active: ${workitem.isActive})`);
|
||||
@ -176,7 +168,8 @@ export class ProjectWorkitemsService {
|
||||
console.log(`ProjectWorkitemsService: Completed processing workitem: ${workitem.name} (Status: ${decision}, Files written: ${result.filesWritten.length})`);
|
||||
return {
|
||||
success: true,
|
||||
decision,
|
||||
status: decision,
|
||||
workitem,
|
||||
filesWritten: result.filesWritten,
|
||||
filesRemoved: result.filesDeleted,
|
||||
};
|
||||
@ -184,6 +177,7 @@ export class ProjectWorkitemsService {
|
||||
console.error(`Error processing workitem ${workitem.name}:`, error);
|
||||
return {
|
||||
success: false,
|
||||
workitem: workitem,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
|
@ -2,78 +2,73 @@
|
||||
* Type definitions for the prompts-to-test-spec function
|
||||
*/
|
||||
|
||||
import {Project} from "shared-functions";
|
||||
|
||||
/**
|
||||
* Status of a workitem implementation
|
||||
*/
|
||||
export type WorkitemImplementationStatus = 'create' | 'update' | 'delete';
|
||||
|
||||
export interface Project {
|
||||
name: string;
|
||||
path: string;
|
||||
repoHost?: string;
|
||||
repoUrl?: string;
|
||||
jiraComponent?: string;
|
||||
targetBranch?: string;
|
||||
aiGuidelines?: string;
|
||||
}
|
||||
|
||||
export interface Workitem {
|
||||
name: string;
|
||||
path: string;
|
||||
title: string;
|
||||
description: string;
|
||||
jiraReference?: string;
|
||||
implementation?: string;
|
||||
pullRequestUrl?: string;
|
||||
isActive: boolean;
|
||||
name: string;
|
||||
path: string;
|
||||
title: string;
|
||||
description: string;
|
||||
jiraReference?: string;
|
||||
implementation?: string;
|
||||
pullRequestUrl?: string;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export interface RepoCredentials {
|
||||
type: 'username-password' | 'token';
|
||||
username?: string;
|
||||
password?: string;
|
||||
token?: string;
|
||||
type: 'username-password' | 'token';
|
||||
username?: string;
|
||||
password?: string;
|
||||
token?: string;
|
||||
}
|
||||
|
||||
export interface ProcessResult {
|
||||
project: Project;
|
||||
processedWorkitems: {
|
||||
export interface ProcessedWorkItem {
|
||||
workitem: Workitem;
|
||||
success: boolean;
|
||||
error?: string;
|
||||
status?: 'skipped' | 'updated' | 'created';
|
||||
status?: 'create' | 'update' | 'delete' | 'skip';
|
||||
filesWritten?: string[];
|
||||
}[];
|
||||
pullRequestUrl?: string;
|
||||
error?: string;
|
||||
gitPatch?: string;
|
||||
filesRemoved?: string[];
|
||||
}
|
||||
|
||||
export interface ProcessResult {
|
||||
project: Project;
|
||||
processedWorkitems: ProcessedWorkItem[];
|
||||
pullRequestUrl?: string;
|
||||
error?: string;
|
||||
gitPatch?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP response format for the API
|
||||
*/
|
||||
export interface HttpResponse {
|
||||
success: boolean;
|
||||
projectsProcessed: number;
|
||||
projectsSucceeded: number;
|
||||
projectsFailed: number;
|
||||
mainPullRequestUrl?: string;
|
||||
projects: ProjectSummary[];
|
||||
error?: string;
|
||||
success: boolean;
|
||||
projectsProcessed: number;
|
||||
projectsSucceeded: number;
|
||||
projectsFailed: number;
|
||||
mainPullRequestUrl?: string;
|
||||
projects: ProjectSummary[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Summary of a project's processing results
|
||||
*/
|
||||
export interface ProjectSummary {
|
||||
name: string;
|
||||
success: boolean;
|
||||
error?: string;
|
||||
workitemsProcessed: number;
|
||||
workitemsSkipped: number;
|
||||
workitemsUpdated: number;
|
||||
workitemsCreated: number;
|
||||
filesWritten: number;
|
||||
pullRequestUrl?: string;
|
||||
gitPatch?: string;
|
||||
name: string;
|
||||
success: boolean;
|
||||
error?: string;
|
||||
workitemsProcessed: number;
|
||||
workitemsSkipped: number;
|
||||
workitemsUpdated: number;
|
||||
workitemsCreated: number;
|
||||
filesWritten: number;
|
||||
pullRequestUrl?: string;
|
||||
gitPatch?: string;
|
||||
}
|
||||
|
@ -1,162 +0,0 @@
|
||||
/**
|
||||
* Tests for the GeminiService
|
||||
*/
|
||||
import { GeminiService } from '../gemini-service';
|
||||
import { Workitem } from '../../types';
|
||||
import { VertexAI } from '@google-cloud/vertexai';
|
||||
|
||||
// Mock VertexAI
|
||||
jest.mock('@google-cloud/vertexai', () => {
|
||||
return {
|
||||
VertexAI: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
getGenerativeModel: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
generateContent: jest.fn().mockResolvedValue({
|
||||
response: {
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
parts: [
|
||||
{
|
||||
text: '# Generated PR Description\n\nThis is a test PR description.'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
};
|
||||
})
|
||||
};
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
describe('GeminiService', () => {
|
||||
let geminiService: GeminiService;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
geminiService = new GeminiService('test-project-id');
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should initialize with default values', () => {
|
||||
const service = new GeminiService('test-project-id');
|
||||
expect(VertexAI).toHaveBeenCalledWith({
|
||||
project: 'test-project-id',
|
||||
location: 'us-central1'
|
||||
});
|
||||
});
|
||||
|
||||
it('should initialize with custom values', () => {
|
||||
const service = new GeminiService('test-project-id', 'europe-west1', 'gemini-1.0-pro');
|
||||
expect(VertexAI).toHaveBeenCalledWith({
|
||||
project: 'test-project-id',
|
||||
location: 'europe-west1'
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if project ID is not provided', () => {
|
||||
expect(() => new GeminiService('')).toThrow('Google Cloud Project ID is required');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generatePullRequestDescription', () => {
|
||||
it('should generate a PR description', async () => {
|
||||
const workitems: { workitem: Workitem; success: boolean; error?: string }[] = [
|
||||
{
|
||||
workitem: {
|
||||
name: 'test-workitem',
|
||||
path: '/path/to/workitem',
|
||||
title: 'Test Workitem',
|
||||
description: 'This is a test workitem',
|
||||
isActive: true
|
||||
},
|
||||
success: true
|
||||
}
|
||||
];
|
||||
|
||||
const result = await geminiService.generatePullRequestDescription(workitems);
|
||||
|
||||
expect(result).toBe('# Generated PR Description\n\nThis is a test PR description.');
|
||||
});
|
||||
|
||||
it('should return a mock PR description in dry run mode', async () => {
|
||||
const dryRunService = new GeminiService('test-project-id', 'us-central1', 'gemini-1.5-pro', true);
|
||||
|
||||
const workitems: { workitem: Workitem; success: boolean; error?: string }[] = [
|
||||
{
|
||||
workitem: {
|
||||
name: 'test-workitem',
|
||||
path: '/path/to/workitem',
|
||||
title: 'Test Workitem',
|
||||
description: 'This is a test workitem',
|
||||
isActive: true
|
||||
},
|
||||
success: true
|
||||
}
|
||||
];
|
||||
|
||||
const result = await dryRunService.generatePullRequestDescription(workitems);
|
||||
|
||||
expect(result).toContain('# Automated PR: Update Workitems (DRY RUN)');
|
||||
expect(result).toContain('All workitems:');
|
||||
expect(result).toContain('- test-workitem');
|
||||
expect(result).toContain('*Note: This is a mock PR description generated during dry run.');
|
||||
});
|
||||
|
||||
it('should include git patch in the prompt if provided', async () => {
|
||||
const workitems: { workitem: Workitem; success: boolean; error?: string }[] = [
|
||||
{
|
||||
workitem: {
|
||||
name: 'test-workitem',
|
||||
path: '/path/to/workitem',
|
||||
title: 'Test Workitem',
|
||||
description: 'This is a test workitem',
|
||||
isActive: true
|
||||
},
|
||||
success: true
|
||||
}
|
||||
];
|
||||
|
||||
const gitPatch = 'diff --git a/file.txt b/file.txt\nindex 1234..5678 100644\n--- a/file.txt\n+++ b/file.txt\n@@ -1,3 +1,3 @@\n-old line\n+new line';
|
||||
|
||||
const mockGenerateContent = jest.fn().mockResolvedValue({
|
||||
response: {
|
||||
candidates: [
|
||||
{
|
||||
content: {
|
||||
parts: [
|
||||
{
|
||||
text: '# Generated PR Description\n\nThis is a test PR description with git patch.'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const mockGetGenerativeModel = jest.fn().mockReturnValue({
|
||||
generateContent: mockGenerateContent
|
||||
});
|
||||
|
||||
(VertexAI as jest.Mock).mockImplementationOnce(() => ({
|
||||
getGenerativeModel: mockGetGenerativeModel
|
||||
}));
|
||||
|
||||
const service = new GeminiService('test-project-id');
|
||||
const result = await service.generatePullRequestDescription(workitems, gitPatch);
|
||||
|
||||
expect(mockGenerateContent).toHaveBeenCalled();
|
||||
const promptArg = mockGenerateContent.mock.calls[0][0];
|
||||
expect(promptArg).toContain('Code Changes');
|
||||
expect(promptArg).toContain('```diff');
|
||||
expect(promptArg).toContain(gitPatch);
|
||||
expect(result).toBe('# Generated PR Description\n\nThis is a test PR description with git patch.');
|
||||
});
|
||||
});
|
||||
});
|
@ -20,7 +20,7 @@ describe('ProjectService', () => {
|
||||
});
|
||||
|
||||
describe('findProjects', () => {
|
||||
it('should find all projects in the function directory', async () => {
|
||||
it('should find all projects in the function directory', () => {
|
||||
// Mock fs.existsSync to return true for prompts directory and function directory
|
||||
(fs.existsSync as jest.Mock).mockImplementation((path: string) => {
|
||||
return path === 'prompts' || path === 'prompts/function1';
|
||||
@ -42,7 +42,7 @@ describe('ProjectService', () => {
|
||||
});
|
||||
|
||||
// Mock readProjectInfo
|
||||
jest.spyOn(projectService, 'readProjectInfo').mockImplementation(async (projectPath, projectName) => {
|
||||
jest.spyOn(projectService, 'readProjectInfo').mockImplementation((projectPath, projectName) => {
|
||||
return {
|
||||
name: projectName,
|
||||
path: projectPath,
|
||||
@ -52,7 +52,7 @@ describe('ProjectService', () => {
|
||||
};
|
||||
});
|
||||
|
||||
const projects = await projectService.findProjects('prompts', 'function1');
|
||||
const projects = projectService.findProjects('prompts', 'function1');
|
||||
|
||||
expect(projects).toHaveLength(2);
|
||||
expect(projects[0].name).toBe('project1');
|
||||
@ -63,24 +63,24 @@ describe('ProjectService', () => {
|
||||
expect(fs.existsSync).toHaveBeenCalledWith('prompts/function1/project2/INFO.md');
|
||||
});
|
||||
|
||||
it('should return empty array if prompts directory does not exist', async () => {
|
||||
it('should return empty array if prompts directory does not exist', () => {
|
||||
// Mock fs.existsSync to return false for prompts directory
|
||||
(fs.existsSync as jest.Mock).mockReturnValueOnce(false);
|
||||
|
||||
const projects = await projectService.findProjects('prompts', 'function1');
|
||||
const projects = projectService.findProjects('prompts', 'function1');
|
||||
|
||||
expect(projects).toHaveLength(0);
|
||||
expect(fs.existsSync).toHaveBeenCalledWith('prompts');
|
||||
expect(fs.readdirSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return empty array if function directory does not exist', async () => {
|
||||
it('should return empty array if function directory does not exist', () => {
|
||||
// Mock fs.existsSync to return true for prompts directory but false for function directory
|
||||
(fs.existsSync as jest.Mock).mockImplementation((path: string) => {
|
||||
return path === 'prompts';
|
||||
});
|
||||
|
||||
const projects = await projectService.findProjects('prompts', 'function1');
|
||||
const projects = projectService.findProjects('prompts', 'function1');
|
||||
|
||||
expect(projects).toHaveLength(0);
|
||||
expect(fs.existsSync).toHaveBeenCalledWith('prompts');
|
||||
@ -90,7 +90,7 @@ describe('ProjectService', () => {
|
||||
});
|
||||
|
||||
describe('readProjectInfo', () => {
|
||||
it('should read project information from INFO.md', async () => {
|
||||
it('should read project information from INFO.md', () => {
|
||||
const infoContent = `# Project Name
|
||||
|
||||
- [x] Repo host: https://github.com
|
||||
@ -100,10 +100,13 @@ describe('ProjectService', () => {
|
||||
- [x] Jira component: project-component
|
||||
`;
|
||||
|
||||
// Mock fs.existsSync to return true for INFO.md
|
||||
(fs.existsSync as jest.Mock).mockReturnValueOnce(true);
|
||||
|
||||
// Mock fs.readFileSync to return INFO.md content
|
||||
(fs.readFileSync as jest.Mock).mockReturnValueOnce(infoContent);
|
||||
|
||||
const project = await projectService.readProjectInfo('path/to/project', 'project');
|
||||
const project = projectService.readProjectInfo('path/to/project', 'project');
|
||||
|
||||
expect(project).toEqual({
|
||||
name: 'project',
|
||||
@ -117,17 +120,20 @@ describe('ProjectService', () => {
|
||||
expect(fs.readFileSync).toHaveBeenCalledWith('path/to/project/INFO.md', 'utf-8');
|
||||
});
|
||||
|
||||
it('should handle project information that does not follow the expected format', async () => {
|
||||
it('should handle project information that does not follow the expected format', () => {
|
||||
const infoContent = `# Project Name
|
||||
|
||||
This is a project description.
|
||||
Some other content that doesn't match the expected format.
|
||||
`;
|
||||
|
||||
// Mock fs.existsSync to return true for INFO.md
|
||||
(fs.existsSync as jest.Mock).mockReturnValueOnce(true);
|
||||
|
||||
// Mock fs.readFileSync to return malformed INFO.md content
|
||||
(fs.readFileSync as jest.Mock).mockReturnValueOnce(infoContent);
|
||||
|
||||
const project = await projectService.readProjectInfo('path/to/project', 'project');
|
||||
const project = projectService.readProjectInfo('path/to/project', 'project');
|
||||
|
||||
expect(project).toEqual({
|
||||
name: 'project',
|
||||
@ -140,10 +146,40 @@ Some other content that doesn't match the expected format.
|
||||
});
|
||||
expect(fs.readFileSync).toHaveBeenCalledWith('path/to/project/INFO.md', 'utf-8');
|
||||
});
|
||||
|
||||
it('should throw an error if INFO.md file does not exist', () => {
|
||||
// Mock fs.existsSync to return false for INFO.md
|
||||
(fs.existsSync as jest.Mock).mockReturnValueOnce(false);
|
||||
|
||||
expect(() => {
|
||||
projectService.readProjectInfo('path/to/project', 'project');
|
||||
}).toThrow('INFO.md file not found for project project at path/to/project/INFO.md');
|
||||
|
||||
expect(fs.existsSync).toHaveBeenCalledWith('path/to/project/INFO.md');
|
||||
expect(fs.readFileSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw an error if INFO.md file cannot be read', () => {
|
||||
// Mock fs.existsSync to return true for INFO.md
|
||||
(fs.existsSync as jest.Mock).mockReturnValueOnce(true);
|
||||
|
||||
// Mock fs.readFileSync to throw an error
|
||||
const error = new Error('File read error');
|
||||
(fs.readFileSync as jest.Mock).mockImplementationOnce(() => {
|
||||
throw error;
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
projectService.readProjectInfo('path/to/project', 'project');
|
||||
}).toThrow(`Failed to read INFO.md for project project: ${error.message}`);
|
||||
|
||||
expect(fs.existsSync).toHaveBeenCalledWith('path/to/project/INFO.md');
|
||||
expect(fs.readFileSync).toHaveBeenCalledWith('path/to/project/INFO.md', 'utf-8');
|
||||
});
|
||||
});
|
||||
|
||||
describe('readProjectGuidelines', () => {
|
||||
it('should read AI guidelines for a project', async () => {
|
||||
it('should read AI guidelines for a project', () => {
|
||||
const guidelinesContent = '## Guidelines\n\nThese are the guidelines.';
|
||||
|
||||
// Mock fs.existsSync to return true for AI.md
|
||||
@ -152,18 +188,18 @@ Some other content that doesn't match the expected format.
|
||||
// Mock fs.readFileSync to return guidelines content
|
||||
(fs.readFileSync as jest.Mock).mockReturnValueOnce(guidelinesContent);
|
||||
|
||||
const guidelines = await projectService.readProjectGuidelines('path/to/project');
|
||||
const guidelines = projectService.readProjectGuidelines('path/to/project');
|
||||
|
||||
expect(guidelines).toBe(guidelinesContent);
|
||||
expect(fs.existsSync).toHaveBeenCalledWith('path/to/project/AI.md');
|
||||
expect(fs.readFileSync).toHaveBeenCalledWith('path/to/project/AI.md', 'utf-8');
|
||||
});
|
||||
|
||||
it('should return empty string if AI.md does not exist', async () => {
|
||||
it('should return empty string if AI.md does not exist', () => {
|
||||
// Mock fs.existsSync to return false for AI.md
|
||||
(fs.existsSync as jest.Mock).mockReturnValueOnce(false);
|
||||
|
||||
const guidelines = await projectService.readProjectGuidelines('path/to/project');
|
||||
const guidelines = projectService.readProjectGuidelines('path/to/project');
|
||||
|
||||
expect(guidelines).toBe('');
|
||||
expect(fs.existsSync).toHaveBeenCalledWith('path/to/project/AI.md');
|
||||
|
@ -263,17 +263,17 @@ export class GeminiFileSystemService {
|
||||
|
||||
/**
|
||||
* Search for a string in files
|
||||
* @param rootPath Root path to search in
|
||||
* @param searchString String to search for
|
||||
* @param filePattern Optional file pattern to limit the search (e.g., "*.ts", "src/*.java")
|
||||
* @returns Array of matches with file paths and line numbers
|
||||
* @throws Error if search string is not provided
|
||||
*/
|
||||
grepFiles(rootPath: string, searchString: string, filePattern?: string): Array<{
|
||||
file: string,
|
||||
line: number,
|
||||
content: string
|
||||
}> {
|
||||
console.log(`Searching for "${searchString}" in files${filePattern ? ` matching ${filePattern}` : ''}`);
|
||||
|
||||
if (!searchString) {
|
||||
throw new Error('Search string is required');
|
||||
}
|
||||
@ -296,7 +296,7 @@ export class GeminiFileSystemService {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error searching in file ${filePath}:`, error);
|
||||
// Silently ignore file read errors
|
||||
}
|
||||
};
|
||||
|
||||
@ -322,14 +322,13 @@ export class GeminiFileSystemService {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error searching in directory ${dirPath}:`, error);
|
||||
// Silently ignore directory read errors
|
||||
}
|
||||
};
|
||||
|
||||
// Start the search from the root path
|
||||
searchInDirectory(rootPath, rootPath);
|
||||
|
||||
console.log(`Found ${results.length} matches for "${searchString}"`);
|
||||
return results;
|
||||
}
|
||||
|
||||
@ -432,13 +431,6 @@ After you have implemented the workitem using function calls, use the makeDecisi
|
||||
|
||||
// Process the streaming response
|
||||
for await (const item of streamingResp.stream) {
|
||||
// Add debug logging for each item in the model stream
|
||||
console.log(`[DEBUG] Processing stream item`);
|
||||
// Avoid stringifying the entire item which can be too complex
|
||||
if (item.candidates && item.candidates.length > 0) {
|
||||
console.log(`[DEBUG] Item has ${item.candidates.length} candidates`);
|
||||
}
|
||||
|
||||
// Check if there's a function call in any part of the response
|
||||
let functionCall = null;
|
||||
let textContent = '';
|
||||
@ -447,16 +439,13 @@ After you have implemented the workitem using function calls, use the makeDecisi
|
||||
for (const part of item.candidates?.[0]?.content?.parts || []) {
|
||||
if (part.functionCall) {
|
||||
functionCall = part.functionCall;
|
||||
console.log(`[DEBUG] Function call detected in stream: ${functionCall.name}`);
|
||||
break;
|
||||
} else if (part.text) {
|
||||
textContent += part.text;
|
||||
console.log(`[DEBUG] Text content detected in stream: ${textContent.substring(0, 100)}${textContent.length > 100 ? '...' : ''}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (functionCall) {
|
||||
console.log(`Function call detected: ${functionCall.name}`);
|
||||
pendingFunctionCalls.push(functionCall);
|
||||
} else if (textContent) {
|
||||
// If there's text, append it to the final response
|
||||
@ -467,8 +456,6 @@ After you have implemented the workitem using function calls, use the makeDecisi
|
||||
|
||||
// Process any function calls that were detected
|
||||
if (pendingFunctionCalls.length > 0) {
|
||||
console.log(`Processing ${pendingFunctionCalls.length} function calls from streaming response`);
|
||||
|
||||
let currentRequest: GenerateContentRequest = request;
|
||||
|
||||
// Process each function call
|
||||
@ -477,8 +464,6 @@ After you have implemented the workitem using function calls, use the makeDecisi
|
||||
const functionArgs = (typeof functionCall.args === 'string' ?
|
||||
JSON.parse(functionCall.args) : functionCall.args) as FunctionArgs;
|
||||
|
||||
console.log(`Executing function: ${functionName} with args:`, functionArgs);
|
||||
|
||||
let functionResponse;
|
||||
try {
|
||||
// Execute the function
|
||||
@ -513,7 +498,6 @@ After you have implemented the workitem using function calls, use the makeDecisi
|
||||
reason: functionArgs.reason!
|
||||
};
|
||||
functionResponse = `Decision recorded: ${functionArgs.decision} - ${functionArgs.reason}`;
|
||||
console.log(`Model decision: ${functionArgs.decision} - ${functionArgs.reason}`);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown function: ${functionName}`);
|
||||
@ -573,9 +557,6 @@ After you have implemented the workitem using function calls, use the makeDecisi
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Model stream processing completed`);
|
||||
console.log(`Files written: ${filesWritten.length}, Files deleted: ${filesDeleted.length}`);
|
||||
|
||||
// If no explicit decision was made using the makeDecision function, try to parse it from the text
|
||||
if (!decision) {
|
||||
try {
|
||||
@ -583,10 +564,9 @@ After you have implemented the workitem using function calls, use the makeDecisi
|
||||
const jsonMatch = finalResponse.match(/\{[\s\S]*"decision"[\s\S]*\}/);
|
||||
if (jsonMatch) {
|
||||
decision = JSON.parse(jsonMatch[0]) as ModelResponse;
|
||||
console.log(`Parsed decision from text: ${decision.decision}, reason: ${decision.reason}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error parsing decision from text: ${error}`);
|
||||
console.error(`Error parsing JSON decision:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
@ -602,7 +582,7 @@ After you have implemented the workitem using function calls, use the makeDecisi
|
||||
/**
|
||||
* Create the next request with function call and response
|
||||
* @param currentRequest Current request
|
||||
* @param functionCall Function call
|
||||
* @param functionCall Function call object
|
||||
* @param functionResponseObj Function response object
|
||||
* @param isError Whether the response is an error
|
||||
* @returns Next request
|
||||
@ -654,21 +634,13 @@ After you have implemented the workitem using function calls, use the makeDecisi
|
||||
let functionCall = null;
|
||||
|
||||
for await (const nextItem of nextStreamingResp.stream) {
|
||||
console.log(`[DEBUG] Processing next stream item${isAfterError ? ' after error' : ''}`);
|
||||
// Avoid stringifying the entire item which can be too complex
|
||||
if (nextItem.candidates && nextItem.candidates.length > 0) {
|
||||
console.log(`[DEBUG] Next item${isAfterError ? ' after error' : ''} has ${nextItem.candidates.length} candidates`);
|
||||
}
|
||||
|
||||
// Iterate over every part in the response
|
||||
for (const part of nextItem.candidates?.[0]?.content?.parts || []) {
|
||||
if (part.functionCall) {
|
||||
functionCall = part.functionCall;
|
||||
console.log(`[DEBUG] Function call detected in next stream${isAfterError ? ' after error' : ''}: ${functionCall.name}`);
|
||||
break;
|
||||
} else if (part.text) {
|
||||
textContent += part.text;
|
||||
console.log(`[DEBUG] Text content detected in next stream${isAfterError ? ' after error' : ''}: ${textContent.substring(0, 100)}${textContent.length > 100 ? '...' : ''}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,7 @@
|
||||
/**
|
||||
* Service for handling Gemini API operations
|
||||
*/
|
||||
import {
|
||||
GenerateContentCandidate,
|
||||
VertexAI
|
||||
} from '@google-cloud/vertexai';
|
||||
import { Workitem } from '../types';
|
||||
import {VertexAI} from '@google-cloud/vertexai';
|
||||
|
||||
export class GeminiService {
|
||||
private vertexAI: VertexAI;
|
||||
@ -58,25 +54,9 @@ export class GeminiService {
|
||||
* const prDescription = await geminiService.generatePullRequestDescription(processedWorkitems, gitPatch);
|
||||
*/
|
||||
async generatePullRequestDescription(
|
||||
processedWorkitems: { workitem: Workitem; success: boolean; error?: string }[],
|
||||
description: string,
|
||||
gitPatch?: string
|
||||
): Promise<string> {
|
||||
// Prepare workitem data for the prompt
|
||||
const all: string[] = [];
|
||||
|
||||
for (const item of processedWorkitems) {
|
||||
const {workitem, success, error} = item;
|
||||
|
||||
// Add all workitems to the all list regardless of status
|
||||
all.push(`- ${workitem.name}`);
|
||||
}
|
||||
|
||||
// Create a structured summary of changes
|
||||
let workitemSummary = '';
|
||||
|
||||
if (all.length > 0) {
|
||||
workitemSummary += 'All workitems:\n' + all.join('\n') + '\n\n';
|
||||
}
|
||||
|
||||
// If dry run is enabled, return a mock PR description
|
||||
if (this.dryRunSkipGemini) {
|
||||
@ -87,7 +67,7 @@ This pull request was automatically generated in dry run mode.
|
||||
|
||||
## Changes Summary
|
||||
|
||||
${workitemSummary}
|
||||
${description}
|
||||
|
||||
*Note: This is a mock PR description generated during dry run. No actual Gemini API call was made.*`;
|
||||
}
|
||||
@ -113,7 +93,7 @@ ${gitPatch}
|
||||
You are tasked with creating a pull request description for changes to test specifications.
|
||||
|
||||
The following is a summary of the changes made:
|
||||
${workitemSummary}
|
||||
${description}
|
||||
${gitPatch && gitPatch !== "No changes detected." ? gitPatchSection : ''}
|
||||
|
||||
Create a clear, professional pull request description that:
|
||||
|
@ -1,5 +1,7 @@
|
||||
/**
|
||||
* Service for handling project operations
|
||||
* This service provides functionality for finding projects, reading project information,
|
||||
* and reading project guidelines.
|
||||
*/
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
@ -12,7 +14,7 @@ export class ProjectService {
|
||||
* @param functionName Name of the function to find projects for
|
||||
* @returns Array of projects
|
||||
*/
|
||||
async findProjects(promptsDir: string, functionName: string): Promise<Project[]> {
|
||||
findProjects(promptsDir: string, functionName: string): Project[] {
|
||||
const projects: Project[] = [];
|
||||
|
||||
// Check if prompts directory exists
|
||||
@ -41,7 +43,7 @@ export class ProjectService {
|
||||
}
|
||||
|
||||
// Read project info
|
||||
const project = await this.readProjectInfo(projectPath, dir.name);
|
||||
const project = this.readProjectInfo(projectPath, dir.name);
|
||||
projects.push(project);
|
||||
}
|
||||
|
||||
@ -53,10 +55,32 @@ export class ProjectService {
|
||||
* @param projectPath Path to the project directory
|
||||
* @param projectName Name of the project
|
||||
* @returns Project information
|
||||
* @throws Error if INFO.md file doesn't exist or can't be read
|
||||
*
|
||||
* The INFO.md file is expected to have the following format:
|
||||
* ```
|
||||
* # Project Name
|
||||
*
|
||||
* - [x] Repo host: https://github.com
|
||||
* - [x] Repo url: https://github.com/org/project.git
|
||||
* - [x] Target branch: main
|
||||
* - [x] AI guidelines: docs/AI_GUIDELINES.md
|
||||
* - [x] Jira component: project-component
|
||||
* ```
|
||||
*/
|
||||
async readProjectInfo(projectPath: string, projectName: string): Promise<Project> {
|
||||
readProjectInfo(projectPath: string, projectName: string): Project {
|
||||
const infoPath = path.join(projectPath, 'INFO.md');
|
||||
const infoContent = fs.readFileSync(infoPath, 'utf-8');
|
||||
|
||||
if (!fs.existsSync(infoPath)) {
|
||||
throw new Error(`INFO.md file not found for project ${projectName} at ${infoPath}`);
|
||||
}
|
||||
|
||||
let infoContent: string;
|
||||
try {
|
||||
infoContent = fs.readFileSync(infoPath, 'utf-8');
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to read INFO.md for project ${projectName}: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
|
||||
// Parse INFO.md content
|
||||
const repoHostMatch = infoContent.match(/- \[[ x]\] Repo host: (.*)/);
|
||||
@ -81,9 +105,9 @@ export class ProjectService {
|
||||
/**
|
||||
* Read AI guidelines for a project
|
||||
* @param projectPath Path to the project directory
|
||||
* @returns AI guidelines content
|
||||
* @returns AI guidelines content or empty string if AI.md doesn't exist
|
||||
*/
|
||||
async readProjectGuidelines(projectPath: string): Promise<string> {
|
||||
readProjectGuidelines(projectPath: string): string {
|
||||
const aiPath = path.join(projectPath, 'AI.md');
|
||||
|
||||
if (!fs.existsSync(aiPath)) {
|
||||
|
@ -72,7 +72,6 @@ export class RepositoryService {
|
||||
|
||||
// Checkout the target branch if specified
|
||||
if (project.targetBranch) {
|
||||
console.log(`Checking out target branch: ${project.targetBranch}`);
|
||||
await this.checkoutBranch(projectRepoDir, project.targetBranch);
|
||||
}
|
||||
|
||||
@ -147,14 +146,13 @@ export class RepositoryService {
|
||||
* Checkout an existing branch in a repository
|
||||
* @param repoDir Path to the repository
|
||||
* @param branchName Name of the branch to checkout
|
||||
* @throws Error if checkout fails
|
||||
*/
|
||||
async checkoutBranch(repoDir: string, branchName: string): Promise<void> {
|
||||
const git = simpleGit(repoDir);
|
||||
try {
|
||||
await git.checkout(branchName);
|
||||
console.log(`Successfully checked out branch: ${branchName}`);
|
||||
} catch (error) {
|
||||
console.error(`Error checking out branch ${branchName}:`, error);
|
||||
throw new Error(`Failed to checkout branch ${branchName}: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
@ -3,29 +3,18 @@
|
||||
*/
|
||||
|
||||
export interface Project {
|
||||
name: string;
|
||||
path: string;
|
||||
repoHost?: string;
|
||||
repoUrl?: string;
|
||||
jiraComponent?: string;
|
||||
targetBranch?: string;
|
||||
aiGuidelines?: string;
|
||||
}
|
||||
|
||||
export interface Workitem {
|
||||
name: string;
|
||||
path: string;
|
||||
title: string;
|
||||
description: string;
|
||||
jiraReference?: string;
|
||||
implementation?: string;
|
||||
pullRequestUrl?: string;
|
||||
isActive: boolean;
|
||||
name: string;
|
||||
path: string;
|
||||
repoHost?: string;
|
||||
repoUrl?: string;
|
||||
jiraComponent?: string;
|
||||
targetBranch?: string;
|
||||
aiGuidelines?: string;
|
||||
}
|
||||
|
||||
export interface RepoCredentials {
|
||||
type: 'username-password' | 'token';
|
||||
username?: string;
|
||||
password?: string;
|
||||
token?: string;
|
||||
type: 'username-password' | 'token';
|
||||
username?: string;
|
||||
password?: string;
|
||||
token?: string;
|
||||
}
|
||||
|
@ -7,13 +7,3 @@ The nitro-back backend should have a /test endpoint implemented returning the js
|
||||
- [ ] Jira: NITRO-0001
|
||||
- [ ] Implementation:
|
||||
- [x] Active
|
||||
|
||||
|
||||
### Log
|
||||
|
||||
2025-06-08T03:25:45.195Z - Workitem has been implemented. Created files:
|
||||
- nitro-it/src/test/resources/workitems/2025-06-08-test.feature
|
||||
|
||||
|
||||
2025-06-08T03:00:46.571Z - Workitem has been implemented. Created files:
|
||||
No files were affected.
|
||||
|
Loading…
x
Reference in New Issue
Block a user