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 => {
|
const projects: ProjectSummary[] = results.map(result => {
|
||||||
// Count workitems
|
// Count workitems
|
||||||
const workitemsProcessed = result.processedWorkitems.length;
|
const workitemsProcessed = result.processedWorkitems.length;
|
||||||
const workitemsSkipped = result.processedWorkitems.filter(w => !w.workitem.isActive).length;
|
const workitemsSkipped = result.processedWorkitems.filter(w => w.success && w.status === "skip").length;
|
||||||
const workitemsUpdated = result.processedWorkitems.filter(w => w.success).length;
|
const workitemsUpdated = result.processedWorkitems.filter(w => w.success && w.status === "update").length;
|
||||||
const workitemsCreated = result.processedWorkitems.filter(w => w.success && w.status === 'created').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);
|
const filesWritten = result.processedWorkitems.reduce((sum, w) => sum + (w.filesWritten?.length || 0), 0);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -21,7 +21,8 @@ describe('formatHttpResponse', () => {
|
|||||||
description: 'Description 1',
|
description: 'Description 1',
|
||||||
isActive: true
|
isActive: true
|
||||||
},
|
},
|
||||||
success: true
|
success: true,
|
||||||
|
status: 'update'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
workitem: {
|
workitem: {
|
||||||
@ -31,7 +32,8 @@ describe('formatHttpResponse', () => {
|
|||||||
description: 'Description 2',
|
description: 'Description 2',
|
||||||
isActive: false
|
isActive: false
|
||||||
},
|
},
|
||||||
success: true
|
success: true,
|
||||||
|
status: 'update'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
pullRequestUrl: 'https://github.com/org/project1/pull/123'
|
pullRequestUrl: 'https://github.com/org/project1/pull/123'
|
||||||
@ -74,8 +76,9 @@ describe('formatHttpResponse', () => {
|
|||||||
{
|
{
|
||||||
name: 'project1',
|
name: 'project1',
|
||||||
success: true,
|
success: true,
|
||||||
|
error: undefined,
|
||||||
workitemsProcessed: 2,
|
workitemsProcessed: 2,
|
||||||
workitemsSkipped: 1,
|
workitemsSkipped: 0,
|
||||||
workitemsUpdated: 2,
|
workitemsUpdated: 2,
|
||||||
workitemsCreated: 0,
|
workitemsCreated: 0,
|
||||||
filesWritten: 0,
|
filesWritten: 0,
|
||||||
@ -89,7 +92,8 @@ describe('formatHttpResponse', () => {
|
|||||||
workitemsSkipped: 0,
|
workitemsSkipped: 0,
|
||||||
workitemsUpdated: 0,
|
workitemsUpdated: 0,
|
||||||
workitemsCreated: 0,
|
workitemsCreated: 0,
|
||||||
filesWritten: 0
|
filesWritten: 0,
|
||||||
|
pullRequestUrl: undefined
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
@ -1,269 +1,267 @@
|
|||||||
import * as fs from 'fs';
|
import {ProcessorService} from '../processor-service';
|
||||||
import * as path from 'path';
|
import {ProjectService} from '../project-service';
|
||||||
import { ProcessorService } from '../processor-service';
|
import {ProcessResult, Workitem} from '../../types';
|
||||||
import { ProjectService } from '../project-service';
|
|
||||||
import { Project, Workitem, ProcessResult } from '../../types';
|
|
||||||
import {
|
import {
|
||||||
RepositoryService as SharedRepositoryService,
|
GeminiService, Project,
|
||||||
PullRequestService as SharedPullRequestService,
|
PullRequestService as SharedPullRequestService,
|
||||||
GeminiService
|
RepositoryService as SharedRepositoryService
|
||||||
} from 'shared-functions';
|
} from 'shared-functions';
|
||||||
|
|
||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
jest.mock('../project-service');
|
jest.mock('../project-service');
|
||||||
jest.mock('shared-functions');
|
jest.mock('shared-functions');
|
||||||
jest.mock('../../config', () => ({
|
jest.mock('../../config', () => ({
|
||||||
validateConfig: jest.fn(),
|
validateConfig: jest.fn(),
|
||||||
getMainRepoCredentials: jest.fn().mockReturnValue({ type: 'token', token: 'mock-token' }),
|
getMainRepoCredentials: jest.fn().mockReturnValue({type: 'token', token: 'mock-token'}),
|
||||||
getGithubCredentials: 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' }),
|
getGiteaCredentials: jest.fn().mockReturnValue({type: 'token', token: 'mock-token'}),
|
||||||
MAIN_REPO_URL: 'https://github.com/org/main-repo.git',
|
MAIN_REPO_URL: 'https://github.com/org/main-repo.git',
|
||||||
GOOGLE_CLOUD_PROJECT_ID: 'mock-project-id',
|
GOOGLE_CLOUD_PROJECT_ID: 'mock-project-id',
|
||||||
GOOGLE_CLOUD_LOCATION: 'mock-location',
|
GOOGLE_CLOUD_LOCATION: 'mock-location',
|
||||||
GEMINI_MODEL: 'mock-model',
|
GEMINI_MODEL: 'mock-model',
|
||||||
USE_LOCAL_REPO: false,
|
USE_LOCAL_REPO: false,
|
||||||
DRY_RUN_SKIP_COMMITS: false,
|
DRY_RUN_SKIP_COMMITS: false,
|
||||||
DRY_RUN_SKIP_GEMINI: false
|
DRY_RUN_SKIP_GEMINI: false
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('ProcessorService', () => {
|
describe('ProcessorService', () => {
|
||||||
let processorService: ProcessorService;
|
let processorService: ProcessorService;
|
||||||
let mockProjectService: jest.Mocked<ProjectService>;
|
let mockProjectService: jest.Mocked<ProjectService>;
|
||||||
let mockSharedRepositoryService: jest.Mocked<SharedRepositoryService>;
|
let mockSharedRepositoryService: jest.Mocked<SharedRepositoryService>;
|
||||||
let mockSharedPullRequestService: jest.Mocked<SharedPullRequestService>;
|
let mockSharedPullRequestService: jest.Mocked<SharedPullRequestService>;
|
||||||
let mockGeminiService: jest.Mocked<GeminiService>;
|
let mockGeminiService: jest.Mocked<GeminiService>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
processorService = new ProcessorService();
|
processorService = new ProcessorService();
|
||||||
mockProjectService = ProjectService.prototype as jest.Mocked<ProjectService>;
|
mockProjectService = ProjectService.prototype as jest.Mocked<ProjectService>;
|
||||||
mockSharedRepositoryService = SharedRepositoryService.prototype as jest.Mocked<SharedRepositoryService>;
|
mockSharedRepositoryService = SharedRepositoryService.prototype as jest.Mocked<SharedRepositoryService>;
|
||||||
mockSharedPullRequestService = SharedPullRequestService.prototype as jest.Mocked<SharedPullRequestService>;
|
mockSharedPullRequestService = SharedPullRequestService.prototype as jest.Mocked<SharedPullRequestService>;
|
||||||
mockGeminiService = GeminiService.prototype as jest.Mocked<GeminiService>;
|
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()
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle deactivated workitems correctly', async () => {
|
describe('updateWorkitemFilesWithPullRequestUrls', () => {
|
||||||
// Create test data
|
it('should update workitem files with pull request URLs and commit changes', async () => {
|
||||||
const mainRepoPath = '/path/to/main/repo';
|
// Create test data
|
||||||
const project: Project = {
|
const mainRepoPath = '/path/to/main/repo';
|
||||||
name: 'test-project',
|
const project: Project = {
|
||||||
path: '/path/to/project',
|
name: 'test-project',
|
||||||
repoHost: 'https://github.com',
|
path: '/path/to/project',
|
||||||
repoUrl: 'https://github.com/org/test-project.git'
|
repoHost: 'https://github.com',
|
||||||
};
|
repoUrl: 'https://github.com/org/test-project.git'
|
||||||
|
};
|
||||||
|
|
||||||
const activeWorkitem: Workitem = {
|
const workitem1: Workitem = {
|
||||||
name: 'active-workitem',
|
name: 'workitem1',
|
||||||
path: '/path/to/active-workitem.md',
|
path: '/path/to/workitem1.md',
|
||||||
title: 'Active Workitem',
|
title: 'Workitem 1',
|
||||||
description: 'This is an active workitem',
|
description: 'Description 1',
|
||||||
isActive: true
|
isActive: true
|
||||||
};
|
};
|
||||||
|
|
||||||
const deactivatedWorkitem: Workitem = {
|
const workitem2: Workitem = {
|
||||||
name: 'deactivated-workitem',
|
name: 'workitem2',
|
||||||
path: '/path/to/deactivated-workitem.md',
|
path: '/path/to/workitem2.md',
|
||||||
title: 'Deactivated Workitem',
|
title: 'Workitem 2',
|
||||||
description: 'This is a deactivated workitem',
|
description: 'Description 2',
|
||||||
isActive: false
|
isActive: true
|
||||||
};
|
};
|
||||||
|
|
||||||
const results: ProcessResult[] = [
|
const results: ProcessResult[] = [
|
||||||
{
|
{
|
||||||
project,
|
project,
|
||||||
processedWorkitems: [
|
processedWorkitems: [
|
||||||
{ workitem: activeWorkitem, success: true, status: 'updated', filesWritten: [] },
|
{workitem: workitem1, success: true, status: 'update', filesWritten: []},
|
||||||
{ workitem: deactivatedWorkitem, success: true, status: 'skipped', filesWritten: [] }
|
{workitem: workitem2, success: true, status: 'update', filesWritten: []}
|
||||||
],
|
],
|
||||||
pullRequestUrl: 'https://github.com/org/test-project/pull/123'
|
pullRequestUrl: 'https://github.com/org/test-project/pull/123',
|
||||||
}
|
gitPatch: 'mock-git-patch'
|
||||||
];
|
}
|
||||||
|
];
|
||||||
|
|
||||||
// Mock the updateWorkitemWithPullRequestUrl method
|
// Mock the updateWorkitemWithPullRequestUrl method
|
||||||
mockProjectService.updateWorkitemWithPullRequestUrl.mockImplementation(
|
mockProjectService.updateWorkitemWithPullRequestUrl.mockImplementation(
|
||||||
async (workitem, pullRequestUrl) => {
|
async (workitem, pullRequestUrl) => {
|
||||||
return { ...workitem, pullRequestUrl };
|
return {...workitem, pullRequestUrl};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Call the method
|
// Call the method
|
||||||
await (processorService as any).updateWorkitemFilesWithPullRequestUrls(results, mainRepoPath);
|
await (processorService as any).updateWorkitemFilesWithPullRequestUrls(results, mainRepoPath);
|
||||||
|
|
||||||
// Verify the method calls
|
// Verify the method calls
|
||||||
expect(mockSharedRepositoryService.createBranch).toHaveBeenCalledWith(
|
expect(mockSharedRepositoryService.createBranch).toHaveBeenCalledWith(
|
||||||
mainRepoPath,
|
mainRepoPath,
|
||||||
expect.stringMatching(/update-workitem-pr-urls-\d{4}-\d{2}-\d{2}/)
|
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).toHaveBeenCalledTimes(2);
|
expect(mockProjectService.updateWorkitemWithPullRequestUrl).toHaveBeenCalledWith(
|
||||||
expect(mockProjectService.updateWorkitemWithPullRequestUrl).toHaveBeenCalledWith(
|
workitem1,
|
||||||
activeWorkitem,
|
'https://github.com/org/test-project/pull/123'
|
||||||
'https://github.com/org/test-project/pull/123'
|
);
|
||||||
);
|
expect(mockProjectService.updateWorkitemWithPullRequestUrl).toHaveBeenCalledWith(
|
||||||
expect(mockProjectService.updateWorkitemWithPullRequestUrl).toHaveBeenCalledWith(
|
workitem2,
|
||||||
deactivatedWorkitem,
|
'https://github.com/org/test-project/pull/123'
|
||||||
'https://github.com/org/test-project/pull/123'
|
);
|
||||||
);
|
|
||||||
|
|
||||||
expect(mockSharedRepositoryService.commitChanges).toHaveBeenCalledWith(
|
expect(mockSharedRepositoryService.commitChanges).toHaveBeenCalledWith(
|
||||||
mainRepoPath,
|
mainRepoPath,
|
||||||
expect.stringMatching(/Update workitem files with pull request URLs: \d{4}-\d{2}-\d{2}/)
|
expect.stringMatching(/Update workitem files with pull request URLs: \d{4}-\d{2}-\d{2}/)
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mockSharedRepositoryService.pushChanges).toHaveBeenCalledWith(
|
expect(mockSharedRepositoryService.pushChanges).toHaveBeenCalledWith(
|
||||||
mainRepoPath,
|
mainRepoPath,
|
||||||
expect.stringMatching(/update-workitem-pr-urls-\d{4}-\d{2}-\d{2}/),
|
expect.stringMatching(/update-workitem-pr-urls-\d{4}-\d{2}-\d{2}/),
|
||||||
expect.anything()
|
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 path from 'path';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import {ProcessResult, Project, RepoCredentials} from '../types';
|
import {ProcessResult, RepoCredentials} from '../types';
|
||||||
import {
|
import {
|
||||||
RepositoryService as SharedRepositoryService,
|
RepositoryService as SharedRepositoryService,
|
||||||
PullRequestService as SharedPullRequestService,
|
PullRequestService as SharedPullRequestService,
|
||||||
GeminiService
|
GeminiService, Project
|
||||||
} from 'shared-functions';
|
} from 'shared-functions';
|
||||||
import {ProjectService} from './project-service';
|
import {ProjectService} from './project-service';
|
||||||
import {ProjectWorkitemsService} from './project-workitems-service';
|
import {ProjectWorkitemsService} from './project-workitems-service';
|
||||||
@ -284,8 +284,11 @@ export class ProcessorService {
|
|||||||
await this.sharedRepositoryService.pushChanges(projectRepoPath, branchName, credentials);
|
await this.sharedRepositoryService.pushChanges(projectRepoPath, branchName, credentials);
|
||||||
|
|
||||||
// Generate PR description using Gemini
|
// 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(
|
const description = await this.geminiService.generatePullRequestDescription(
|
||||||
result.processedWorkitems,
|
workItemsSummary,
|
||||||
result.gitPatch
|
result.gitPatch
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
*/
|
*/
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import {ProjectService as SharedProjectService, Project, Workitem} from 'shared-functions';
|
import {Project, ProjectService as SharedProjectService} from 'shared-functions';
|
||||||
import { WorkitemImplementationStatus } from '../types';
|
import {Workitem, WorkitemImplementationStatus} from '../types';
|
||||||
|
|
||||||
export class ProjectService {
|
export class ProjectService {
|
||||||
private sharedProjectService: SharedProjectService;
|
private sharedProjectService: SharedProjectService;
|
||||||
|
@ -3,14 +3,13 @@
|
|||||||
*/
|
*/
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import {ProcessResult} from '../types';
|
import {ProcessedWorkItem, ProcessResult, Workitem} from '../types';
|
||||||
import {ProjectService} from './project-service';
|
import {ProjectService} from './project-service';
|
||||||
import {DRY_RUN_SKIP_GEMINI} from '../config';
|
import {DRY_RUN_SKIP_GEMINI} from '../config';
|
||||||
import {
|
import {
|
||||||
GeminiFileSystemService,
|
GeminiFileSystemService,
|
||||||
Project,
|
Project,
|
||||||
Workitem,
|
RepositoryService as SharedRepositoryService,
|
||||||
RepositoryService as SharedRepositoryService
|
|
||||||
} from 'shared-functions';
|
} from 'shared-functions';
|
||||||
|
|
||||||
export class ProjectWorkitemsService {
|
export class ProjectWorkitemsService {
|
||||||
@ -50,11 +49,10 @@ export class ProjectWorkitemsService {
|
|||||||
const projectGuidelines = await this.projectService.readProjectGuidelines(project.path);
|
const projectGuidelines = await this.projectService.readProjectGuidelines(project.path);
|
||||||
|
|
||||||
// Process each workitem
|
// Process each workitem
|
||||||
const processedWorkitems = [];
|
const processedWorkitems: ProcessedWorkItem[] = [];
|
||||||
for (const workitem of workitems) {
|
for (const workitem of workitems) {
|
||||||
console.log(`ProjectWorkitemsService: Processing workitem: ${workitem.name}`);
|
const result: ProcessedWorkItem = await this.processWorkitem(project, projectRepoPath, workitem, projectGuidelines);
|
||||||
const result = await this.processWorkitem(project, projectRepoPath, workitem, projectGuidelines);
|
processedWorkitems.push(result);
|
||||||
processedWorkitems.push({workitem, ...result});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate git patch if any files were written
|
// Generate git patch if any files were written
|
||||||
@ -98,13 +96,7 @@ export class ProjectWorkitemsService {
|
|||||||
projectRepoPath: string,
|
projectRepoPath: string,
|
||||||
workitem: Workitem,
|
workitem: Workitem,
|
||||||
projectGuidelines: string
|
projectGuidelines: string
|
||||||
): Promise<{
|
): Promise<ProcessedWorkItem> {
|
||||||
success: boolean;
|
|
||||||
error?: string;
|
|
||||||
decision?: 'skip' | 'update' | 'create' | 'delete';
|
|
||||||
filesWritten?: string[],
|
|
||||||
filesRemoved?: string[],
|
|
||||||
}> {
|
|
||||||
try {
|
try {
|
||||||
// Set the current workitem
|
// Set the current workitem
|
||||||
console.log(`ProjectWorkitemsService: Processing workitem: ${workitem.name} (Active: ${workitem.isActive})`);
|
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})`);
|
console.log(`ProjectWorkitemsService: Completed processing workitem: ${workitem.name} (Status: ${decision}, Files written: ${result.filesWritten.length})`);
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
decision,
|
status: decision,
|
||||||
|
workitem,
|
||||||
filesWritten: result.filesWritten,
|
filesWritten: result.filesWritten,
|
||||||
filesRemoved: result.filesDeleted,
|
filesRemoved: result.filesDeleted,
|
||||||
};
|
};
|
||||||
@ -184,6 +177,7 @@ export class ProjectWorkitemsService {
|
|||||||
console.error(`Error processing workitem ${workitem.name}:`, error);
|
console.error(`Error processing workitem ${workitem.name}:`, error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
|
workitem: workitem,
|
||||||
error: error instanceof Error ? error.message : String(error),
|
error: error instanceof Error ? error.message : String(error),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -2,78 +2,73 @@
|
|||||||
* Type definitions for the prompts-to-test-spec function
|
* Type definitions for the prompts-to-test-spec function
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {Project} from "shared-functions";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Status of a workitem implementation
|
* Status of a workitem implementation
|
||||||
*/
|
*/
|
||||||
export type WorkitemImplementationStatus = 'create' | 'update' | 'delete';
|
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 {
|
export interface Workitem {
|
||||||
name: string;
|
name: string;
|
||||||
path: string;
|
path: string;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
jiraReference?: string;
|
jiraReference?: string;
|
||||||
implementation?: string;
|
implementation?: string;
|
||||||
pullRequestUrl?: string;
|
pullRequestUrl?: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RepoCredentials {
|
export interface RepoCredentials {
|
||||||
type: 'username-password' | 'token';
|
type: 'username-password' | 'token';
|
||||||
username?: string;
|
username?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
token?: string;
|
token?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProcessResult {
|
export interface ProcessedWorkItem {
|
||||||
project: Project;
|
|
||||||
processedWorkitems: {
|
|
||||||
workitem: Workitem;
|
workitem: Workitem;
|
||||||
success: boolean;
|
success: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
status?: 'skipped' | 'updated' | 'created';
|
status?: 'create' | 'update' | 'delete' | 'skip';
|
||||||
filesWritten?: string[];
|
filesWritten?: string[];
|
||||||
}[];
|
filesRemoved?: string[];
|
||||||
pullRequestUrl?: string;
|
}
|
||||||
error?: string;
|
|
||||||
gitPatch?: string;
|
export interface ProcessResult {
|
||||||
|
project: Project;
|
||||||
|
processedWorkitems: ProcessedWorkItem[];
|
||||||
|
pullRequestUrl?: string;
|
||||||
|
error?: string;
|
||||||
|
gitPatch?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP response format for the API
|
* HTTP response format for the API
|
||||||
*/
|
*/
|
||||||
export interface HttpResponse {
|
export interface HttpResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
projectsProcessed: number;
|
projectsProcessed: number;
|
||||||
projectsSucceeded: number;
|
projectsSucceeded: number;
|
||||||
projectsFailed: number;
|
projectsFailed: number;
|
||||||
mainPullRequestUrl?: string;
|
mainPullRequestUrl?: string;
|
||||||
projects: ProjectSummary[];
|
projects: ProjectSummary[];
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Summary of a project's processing results
|
* Summary of a project's processing results
|
||||||
*/
|
*/
|
||||||
export interface ProjectSummary {
|
export interface ProjectSummary {
|
||||||
name: string;
|
name: string;
|
||||||
success: boolean;
|
success: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
workitemsProcessed: number;
|
workitemsProcessed: number;
|
||||||
workitemsSkipped: number;
|
workitemsSkipped: number;
|
||||||
workitemsUpdated: number;
|
workitemsUpdated: number;
|
||||||
workitemsCreated: number;
|
workitemsCreated: number;
|
||||||
filesWritten: number;
|
filesWritten: number;
|
||||||
pullRequestUrl?: string;
|
pullRequestUrl?: string;
|
||||||
gitPatch?: 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', () => {
|
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
|
// Mock fs.existsSync to return true for prompts directory and function directory
|
||||||
(fs.existsSync as jest.Mock).mockImplementation((path: string) => {
|
(fs.existsSync as jest.Mock).mockImplementation((path: string) => {
|
||||||
return path === 'prompts' || path === 'prompts/function1';
|
return path === 'prompts' || path === 'prompts/function1';
|
||||||
@ -42,7 +42,7 @@ describe('ProjectService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Mock readProjectInfo
|
// Mock readProjectInfo
|
||||||
jest.spyOn(projectService, 'readProjectInfo').mockImplementation(async (projectPath, projectName) => {
|
jest.spyOn(projectService, 'readProjectInfo').mockImplementation((projectPath, projectName) => {
|
||||||
return {
|
return {
|
||||||
name: projectName,
|
name: projectName,
|
||||||
path: projectPath,
|
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).toHaveLength(2);
|
||||||
expect(projects[0].name).toBe('project1');
|
expect(projects[0].name).toBe('project1');
|
||||||
@ -63,24 +63,24 @@ describe('ProjectService', () => {
|
|||||||
expect(fs.existsSync).toHaveBeenCalledWith('prompts/function1/project2/INFO.md');
|
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
|
// Mock fs.existsSync to return false for prompts directory
|
||||||
(fs.existsSync as jest.Mock).mockReturnValueOnce(false);
|
(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(projects).toHaveLength(0);
|
||||||
expect(fs.existsSync).toHaveBeenCalledWith('prompts');
|
expect(fs.existsSync).toHaveBeenCalledWith('prompts');
|
||||||
expect(fs.readdirSync).not.toHaveBeenCalled();
|
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
|
// Mock fs.existsSync to return true for prompts directory but false for function directory
|
||||||
(fs.existsSync as jest.Mock).mockImplementation((path: string) => {
|
(fs.existsSync as jest.Mock).mockImplementation((path: string) => {
|
||||||
return path === 'prompts';
|
return path === 'prompts';
|
||||||
});
|
});
|
||||||
|
|
||||||
const projects = await projectService.findProjects('prompts', 'function1');
|
const projects = projectService.findProjects('prompts', 'function1');
|
||||||
|
|
||||||
expect(projects).toHaveLength(0);
|
expect(projects).toHaveLength(0);
|
||||||
expect(fs.existsSync).toHaveBeenCalledWith('prompts');
|
expect(fs.existsSync).toHaveBeenCalledWith('prompts');
|
||||||
@ -90,7 +90,7 @@ describe('ProjectService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('readProjectInfo', () => {
|
describe('readProjectInfo', () => {
|
||||||
it('should read project information from INFO.md', async () => {
|
it('should read project information from INFO.md', () => {
|
||||||
const infoContent = `# Project Name
|
const infoContent = `# Project Name
|
||||||
|
|
||||||
- [x] Repo host: https://github.com
|
- [x] Repo host: https://github.com
|
||||||
@ -100,10 +100,13 @@ describe('ProjectService', () => {
|
|||||||
- [x] Jira component: project-component
|
- [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
|
// Mock fs.readFileSync to return INFO.md content
|
||||||
(fs.readFileSync as jest.Mock).mockReturnValueOnce(infoContent);
|
(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({
|
expect(project).toEqual({
|
||||||
name: 'project',
|
name: 'project',
|
||||||
@ -117,17 +120,20 @@ describe('ProjectService', () => {
|
|||||||
expect(fs.readFileSync).toHaveBeenCalledWith('path/to/project/INFO.md', 'utf-8');
|
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
|
const infoContent = `# Project Name
|
||||||
|
|
||||||
This is a project description.
|
This is a project description.
|
||||||
Some other content that doesn't match the expected format.
|
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
|
// Mock fs.readFileSync to return malformed INFO.md content
|
||||||
(fs.readFileSync as jest.Mock).mockReturnValueOnce(infoContent);
|
(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({
|
expect(project).toEqual({
|
||||||
name: 'project',
|
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');
|
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', () => {
|
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.';
|
const guidelinesContent = '## Guidelines\n\nThese are the guidelines.';
|
||||||
|
|
||||||
// Mock fs.existsSync to return true for AI.md
|
// 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
|
// Mock fs.readFileSync to return guidelines content
|
||||||
(fs.readFileSync as jest.Mock).mockReturnValueOnce(guidelinesContent);
|
(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(guidelines).toBe(guidelinesContent);
|
||||||
expect(fs.existsSync).toHaveBeenCalledWith('path/to/project/AI.md');
|
expect(fs.existsSync).toHaveBeenCalledWith('path/to/project/AI.md');
|
||||||
expect(fs.readFileSync).toHaveBeenCalledWith('path/to/project/AI.md', 'utf-8');
|
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
|
// Mock fs.existsSync to return false for AI.md
|
||||||
(fs.existsSync as jest.Mock).mockReturnValueOnce(false);
|
(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(guidelines).toBe('');
|
||||||
expect(fs.existsSync).toHaveBeenCalledWith('path/to/project/AI.md');
|
expect(fs.existsSync).toHaveBeenCalledWith('path/to/project/AI.md');
|
||||||
|
@ -263,17 +263,17 @@ export class GeminiFileSystemService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Search for a string in files
|
* Search for a string in files
|
||||||
|
* @param rootPath Root path to search in
|
||||||
* @param searchString String to search for
|
* @param searchString String to search for
|
||||||
* @param filePattern Optional file pattern to limit the search (e.g., "*.ts", "src/*.java")
|
* @param filePattern Optional file pattern to limit the search (e.g., "*.ts", "src/*.java")
|
||||||
* @returns Array of matches with file paths and line numbers
|
* @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<{
|
grepFiles(rootPath: string, searchString: string, filePattern?: string): Array<{
|
||||||
file: string,
|
file: string,
|
||||||
line: number,
|
line: number,
|
||||||
content: string
|
content: string
|
||||||
}> {
|
}> {
|
||||||
console.log(`Searching for "${searchString}" in files${filePattern ? ` matching ${filePattern}` : ''}`);
|
|
||||||
|
|
||||||
if (!searchString) {
|
if (!searchString) {
|
||||||
throw new Error('Search string is required');
|
throw new Error('Search string is required');
|
||||||
}
|
}
|
||||||
@ -296,7 +296,7 @@ export class GeminiFileSystemService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error searching in file ${filePath}:`, error);
|
// Silently ignore file read errors
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -322,14 +322,13 @@ export class GeminiFileSystemService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error searching in directory ${dirPath}:`, error);
|
// Silently ignore directory read errors
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Start the search from the root path
|
// Start the search from the root path
|
||||||
searchInDirectory(rootPath, rootPath);
|
searchInDirectory(rootPath, rootPath);
|
||||||
|
|
||||||
console.log(`Found ${results.length} matches for "${searchString}"`);
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -432,13 +431,6 @@ After you have implemented the workitem using function calls, use the makeDecisi
|
|||||||
|
|
||||||
// Process the streaming response
|
// Process the streaming response
|
||||||
for await (const item of streamingResp.stream) {
|
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
|
// Check if there's a function call in any part of the response
|
||||||
let functionCall = null;
|
let functionCall = null;
|
||||||
let textContent = '';
|
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 || []) {
|
for (const part of item.candidates?.[0]?.content?.parts || []) {
|
||||||
if (part.functionCall) {
|
if (part.functionCall) {
|
||||||
functionCall = part.functionCall;
|
functionCall = part.functionCall;
|
||||||
console.log(`[DEBUG] Function call detected in stream: ${functionCall.name}`);
|
|
||||||
break;
|
break;
|
||||||
} else if (part.text) {
|
} else if (part.text) {
|
||||||
textContent += part.text;
|
textContent += part.text;
|
||||||
console.log(`[DEBUG] Text content detected in stream: ${textContent.substring(0, 100)}${textContent.length > 100 ? '...' : ''}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (functionCall) {
|
if (functionCall) {
|
||||||
console.log(`Function call detected: ${functionCall.name}`);
|
|
||||||
pendingFunctionCalls.push(functionCall);
|
pendingFunctionCalls.push(functionCall);
|
||||||
} else if (textContent) {
|
} else if (textContent) {
|
||||||
// If there's text, append it to the final response
|
// 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
|
// Process any function calls that were detected
|
||||||
if (pendingFunctionCalls.length > 0) {
|
if (pendingFunctionCalls.length > 0) {
|
||||||
console.log(`Processing ${pendingFunctionCalls.length} function calls from streaming response`);
|
|
||||||
|
|
||||||
let currentRequest: GenerateContentRequest = request;
|
let currentRequest: GenerateContentRequest = request;
|
||||||
|
|
||||||
// Process each function call
|
// 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' ?
|
const functionArgs = (typeof functionCall.args === 'string' ?
|
||||||
JSON.parse(functionCall.args) : functionCall.args) as FunctionArgs;
|
JSON.parse(functionCall.args) : functionCall.args) as FunctionArgs;
|
||||||
|
|
||||||
console.log(`Executing function: ${functionName} with args:`, functionArgs);
|
|
||||||
|
|
||||||
let functionResponse;
|
let functionResponse;
|
||||||
try {
|
try {
|
||||||
// Execute the function
|
// Execute the function
|
||||||
@ -513,7 +498,6 @@ After you have implemented the workitem using function calls, use the makeDecisi
|
|||||||
reason: functionArgs.reason!
|
reason: functionArgs.reason!
|
||||||
};
|
};
|
||||||
functionResponse = `Decision recorded: ${functionArgs.decision} - ${functionArgs.reason}`;
|
functionResponse = `Decision recorded: ${functionArgs.decision} - ${functionArgs.reason}`;
|
||||||
console.log(`Model decision: ${functionArgs.decision} - ${functionArgs.reason}`);
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown function: ${functionName}`);
|
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 no explicit decision was made using the makeDecision function, try to parse it from the text
|
||||||
if (!decision) {
|
if (!decision) {
|
||||||
try {
|
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]*\}/);
|
const jsonMatch = finalResponse.match(/\{[\s\S]*"decision"[\s\S]*\}/);
|
||||||
if (jsonMatch) {
|
if (jsonMatch) {
|
||||||
decision = JSON.parse(jsonMatch[0]) as ModelResponse;
|
decision = JSON.parse(jsonMatch[0]) as ModelResponse;
|
||||||
console.log(`Parsed decision from text: ${decision.decision}, reason: ${decision.reason}`);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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
|
* Create the next request with function call and response
|
||||||
* @param currentRequest Current request
|
* @param currentRequest Current request
|
||||||
* @param functionCall Function call
|
* @param functionCall Function call object
|
||||||
* @param functionResponseObj Function response object
|
* @param functionResponseObj Function response object
|
||||||
* @param isError Whether the response is an error
|
* @param isError Whether the response is an error
|
||||||
* @returns Next request
|
* @returns Next request
|
||||||
@ -654,21 +634,13 @@ After you have implemented the workitem using function calls, use the makeDecisi
|
|||||||
let functionCall = null;
|
let functionCall = null;
|
||||||
|
|
||||||
for await (const nextItem of nextStreamingResp.stream) {
|
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
|
// Iterate over every part in the response
|
||||||
for (const part of nextItem.candidates?.[0]?.content?.parts || []) {
|
for (const part of nextItem.candidates?.[0]?.content?.parts || []) {
|
||||||
if (part.functionCall) {
|
if (part.functionCall) {
|
||||||
functionCall = part.functionCall;
|
functionCall = part.functionCall;
|
||||||
console.log(`[DEBUG] Function call detected in next stream${isAfterError ? ' after error' : ''}: ${functionCall.name}`);
|
|
||||||
break;
|
break;
|
||||||
} else if (part.text) {
|
} else if (part.text) {
|
||||||
textContent += 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
|
* Service for handling Gemini API operations
|
||||||
*/
|
*/
|
||||||
import {
|
import {VertexAI} from '@google-cloud/vertexai';
|
||||||
GenerateContentCandidate,
|
|
||||||
VertexAI
|
|
||||||
} from '@google-cloud/vertexai';
|
|
||||||
import { Workitem } from '../types';
|
|
||||||
|
|
||||||
export class GeminiService {
|
export class GeminiService {
|
||||||
private vertexAI: VertexAI;
|
private vertexAI: VertexAI;
|
||||||
@ -58,25 +54,9 @@ export class GeminiService {
|
|||||||
* const prDescription = await geminiService.generatePullRequestDescription(processedWorkitems, gitPatch);
|
* const prDescription = await geminiService.generatePullRequestDescription(processedWorkitems, gitPatch);
|
||||||
*/
|
*/
|
||||||
async generatePullRequestDescription(
|
async generatePullRequestDescription(
|
||||||
processedWorkitems: { workitem: Workitem; success: boolean; error?: string }[],
|
description: string,
|
||||||
gitPatch?: string
|
gitPatch?: string
|
||||||
): Promise<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 dry run is enabled, return a mock PR description
|
||||||
if (this.dryRunSkipGemini) {
|
if (this.dryRunSkipGemini) {
|
||||||
@ -87,7 +67,7 @@ This pull request was automatically generated in dry run mode.
|
|||||||
|
|
||||||
## Changes Summary
|
## Changes Summary
|
||||||
|
|
||||||
${workitemSummary}
|
${description}
|
||||||
|
|
||||||
*Note: This is a mock PR description generated during dry run. No actual Gemini API call was made.*`;
|
*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.
|
You are tasked with creating a pull request description for changes to test specifications.
|
||||||
|
|
||||||
The following is a summary of the changes made:
|
The following is a summary of the changes made:
|
||||||
${workitemSummary}
|
${description}
|
||||||
${gitPatch && gitPatch !== "No changes detected." ? gitPatchSection : ''}
|
${gitPatch && gitPatch !== "No changes detected." ? gitPatchSection : ''}
|
||||||
|
|
||||||
Create a clear, professional pull request description that:
|
Create a clear, professional pull request description that:
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Service for handling project operations
|
* 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 fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
@ -12,7 +14,7 @@ export class ProjectService {
|
|||||||
* @param functionName Name of the function to find projects for
|
* @param functionName Name of the function to find projects for
|
||||||
* @returns Array of projects
|
* @returns Array of projects
|
||||||
*/
|
*/
|
||||||
async findProjects(promptsDir: string, functionName: string): Promise<Project[]> {
|
findProjects(promptsDir: string, functionName: string): Project[] {
|
||||||
const projects: Project[] = [];
|
const projects: Project[] = [];
|
||||||
|
|
||||||
// Check if prompts directory exists
|
// Check if prompts directory exists
|
||||||
@ -41,7 +43,7 @@ export class ProjectService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read project info
|
// Read project info
|
||||||
const project = await this.readProjectInfo(projectPath, dir.name);
|
const project = this.readProjectInfo(projectPath, dir.name);
|
||||||
projects.push(project);
|
projects.push(project);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,10 +55,32 @@ export class ProjectService {
|
|||||||
* @param projectPath Path to the project directory
|
* @param projectPath Path to the project directory
|
||||||
* @param projectName Name of the project
|
* @param projectName Name of the project
|
||||||
* @returns Project information
|
* @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 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
|
// Parse INFO.md content
|
||||||
const repoHostMatch = infoContent.match(/- \[[ x]\] Repo host: (.*)/);
|
const repoHostMatch = infoContent.match(/- \[[ x]\] Repo host: (.*)/);
|
||||||
@ -81,9 +105,9 @@ export class ProjectService {
|
|||||||
/**
|
/**
|
||||||
* Read AI guidelines for a project
|
* Read AI guidelines for a project
|
||||||
* @param projectPath Path to the project directory
|
* @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');
|
const aiPath = path.join(projectPath, 'AI.md');
|
||||||
|
|
||||||
if (!fs.existsSync(aiPath)) {
|
if (!fs.existsSync(aiPath)) {
|
||||||
|
@ -72,7 +72,6 @@ export class RepositoryService {
|
|||||||
|
|
||||||
// Checkout the target branch if specified
|
// Checkout the target branch if specified
|
||||||
if (project.targetBranch) {
|
if (project.targetBranch) {
|
||||||
console.log(`Checking out target branch: ${project.targetBranch}`);
|
|
||||||
await this.checkoutBranch(projectRepoDir, project.targetBranch);
|
await this.checkoutBranch(projectRepoDir, project.targetBranch);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,14 +146,13 @@ export class RepositoryService {
|
|||||||
* Checkout an existing branch in a repository
|
* Checkout an existing branch in a repository
|
||||||
* @param repoDir Path to the repository
|
* @param repoDir Path to the repository
|
||||||
* @param branchName Name of the branch to checkout
|
* @param branchName Name of the branch to checkout
|
||||||
|
* @throws Error if checkout fails
|
||||||
*/
|
*/
|
||||||
async checkoutBranch(repoDir: string, branchName: string): Promise<void> {
|
async checkoutBranch(repoDir: string, branchName: string): Promise<void> {
|
||||||
const git = simpleGit(repoDir);
|
const git = simpleGit(repoDir);
|
||||||
try {
|
try {
|
||||||
await git.checkout(branchName);
|
await git.checkout(branchName);
|
||||||
console.log(`Successfully checked out branch: ${branchName}`);
|
|
||||||
} catch (error) {
|
} 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)}`);
|
throw new Error(`Failed to checkout branch ${branchName}: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,29 +3,18 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export interface Project {
|
export interface Project {
|
||||||
name: string;
|
name: string;
|
||||||
path: string;
|
path: string;
|
||||||
repoHost?: string;
|
repoHost?: string;
|
||||||
repoUrl?: string;
|
repoUrl?: string;
|
||||||
jiraComponent?: string;
|
jiraComponent?: string;
|
||||||
targetBranch?: string;
|
targetBranch?: string;
|
||||||
aiGuidelines?: string;
|
aiGuidelines?: string;
|
||||||
}
|
|
||||||
|
|
||||||
export interface Workitem {
|
|
||||||
name: string;
|
|
||||||
path: string;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
jiraReference?: string;
|
|
||||||
implementation?: string;
|
|
||||||
pullRequestUrl?: string;
|
|
||||||
isActive: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RepoCredentials {
|
export interface RepoCredentials {
|
||||||
type: 'username-password' | 'token';
|
type: 'username-password' | 'token';
|
||||||
username?: string;
|
username?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
token?: string;
|
token?: string;
|
||||||
}
|
}
|
||||||
|
@ -7,13 +7,3 @@ The nitro-back backend should have a /test endpoint implemented returning the js
|
|||||||
- [ ] Jira: NITRO-0001
|
- [ ] Jira: NITRO-0001
|
||||||
- [ ] Implementation:
|
- [ ] Implementation:
|
||||||
- [x] Active
|
- [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