From 6568ff640d23101ae553176780b0c6643072b141 Mon Sep 17 00:00:00 2001 From: cghislai Date: Sun, 8 Jun 2025 08:17:52 +0200 Subject: [PATCH] WIP --- .../prompts-to-test-spec/src/index.ts | 6 +- .../src/services/__tests__/index.test.ts | 12 +- .../__tests__/processor-service.test.ts | 488 +++++++++--------- .../src/services/processor-service.ts | 9 +- .../src/services/project-service.ts | 4 +- .../src/services/project-workitems-service.ts | 24 +- .../prompts-to-test-spec/src/types.ts | 89 ++-- .../services/__tests__/gemini-service.test.ts | 162 ------ .../__tests__/project-service.test.ts | 66 ++- .../services/gemini-file-system-service.ts | 40 +- .../shared/src/services/gemini-service.ts | 28 +- .../shared/src/services/project-service.ts | 36 +- .../shared/src/services/repository-service.ts | 4 +- src/functions/shared/src/types.ts | 33 +- .../nitro-back/workitems/2025-06-08-test.md | 10 - 15 files changed, 416 insertions(+), 595 deletions(-) delete mode 100644 src/functions/shared/src/services/__tests__/gemini-service.test.ts diff --git a/src/functions/prompts-to-test-spec/src/index.ts b/src/functions/prompts-to-test-spec/src/index.ts index 6cd73cc..47c2f12 100644 --- a/src/functions/prompts-to-test-spec/src/index.ts +++ b/src/functions/prompts-to-test-spec/src/index.ts @@ -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 { diff --git a/src/functions/prompts-to-test-spec/src/services/__tests__/index.test.ts b/src/functions/prompts-to-test-spec/src/services/__tests__/index.test.ts index 23a7863..dc0a0b9 100644 --- a/src/functions/prompts-to-test-spec/src/services/__tests__/index.test.ts +++ b/src/functions/prompts-to-test-spec/src/services/__tests__/index.test.ts @@ -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 } ] }); diff --git a/src/functions/prompts-to-test-spec/src/services/__tests__/processor-service.test.ts b/src/functions/prompts-to-test-spec/src/services/__tests__/processor-service.test.ts index 370865b..0fe346a 100644 --- a/src/functions/prompts-to-test-spec/src/services/__tests__/processor-service.test.ts +++ b/src/functions/prompts-to-test-spec/src/services/__tests__/processor-service.test.ts @@ -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; - let mockSharedRepositoryService: jest.Mocked; - let mockSharedPullRequestService: jest.Mocked; - let mockGeminiService: jest.Mocked; + let processorService: ProcessorService; + let mockProjectService: jest.Mocked; + let mockSharedRepositoryService: jest.Mocked; + let mockSharedPullRequestService: jest.Mocked; + let mockGeminiService: jest.Mocked; - beforeEach(() => { - jest.clearAllMocks(); - processorService = new ProcessorService(); - mockProjectService = ProjectService.prototype as jest.Mocked; - mockSharedRepositoryService = SharedRepositoryService.prototype as jest.Mocked; - mockSharedPullRequestService = SharedPullRequestService.prototype as jest.Mocked; - mockGeminiService = GeminiService.prototype as jest.Mocked; - }); - - 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; + mockSharedRepositoryService = SharedRepositoryService.prototype as jest.Mocked; + mockSharedPullRequestService = SharedPullRequestService.prototype as jest.Mocked; + mockGeminiService = GeminiService.prototype as jest.Mocked; }); - 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(); - }); - }); }); diff --git a/src/functions/prompts-to-test-spec/src/services/processor-service.ts b/src/functions/prompts-to-test-spec/src/services/processor-service.ts index 6eba094..3902e6b 100644 --- a/src/functions/prompts-to-test-spec/src/services/processor-service.ts +++ b/src/functions/prompts-to-test-spec/src/services/processor-service.ts @@ -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 ); diff --git a/src/functions/prompts-to-test-spec/src/services/project-service.ts b/src/functions/prompts-to-test-spec/src/services/project-service.ts index e1126b7..937e4d7 100644 --- a/src/functions/prompts-to-test-spec/src/services/project-service.ts +++ b/src/functions/prompts-to-test-spec/src/services/project-service.ts @@ -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; diff --git a/src/functions/prompts-to-test-spec/src/services/project-workitems-service.ts b/src/functions/prompts-to-test-spec/src/services/project-workitems-service.ts index ff34b98..cdc5e38 100644 --- a/src/functions/prompts-to-test-spec/src/services/project-workitems-service.ts +++ b/src/functions/prompts-to-test-spec/src/services/project-workitems-service.ts @@ -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 { 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), }; } diff --git a/src/functions/prompts-to-test-spec/src/types.ts b/src/functions/prompts-to-test-spec/src/types.ts index 13958a9..552b8de 100644 --- a/src/functions/prompts-to-test-spec/src/types.ts +++ b/src/functions/prompts-to-test-spec/src/types.ts @@ -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; } diff --git a/src/functions/shared/src/services/__tests__/gemini-service.test.ts b/src/functions/shared/src/services/__tests__/gemini-service.test.ts deleted file mode 100644 index aa8620b..0000000 --- a/src/functions/shared/src/services/__tests__/gemini-service.test.ts +++ /dev/null @@ -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.'); - }); - }); -}); diff --git a/src/functions/shared/src/services/__tests__/project-service.test.ts b/src/functions/shared/src/services/__tests__/project-service.test.ts index d68d9ac..8c5a7dc 100644 --- a/src/functions/shared/src/services/__tests__/project-service.test.ts +++ b/src/functions/shared/src/services/__tests__/project-service.test.ts @@ -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'); diff --git a/src/functions/shared/src/services/gemini-file-system-service.ts b/src/functions/shared/src/services/gemini-file-system-service.ts index 779a173..053c331 100644 --- a/src/functions/shared/src/services/gemini-file-system-service.ts +++ b/src/functions/shared/src/services/gemini-file-system-service.ts @@ -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 ? '...' : ''}`); } } } diff --git a/src/functions/shared/src/services/gemini-service.ts b/src/functions/shared/src/services/gemini-service.ts index 15a5f79..1fba4a3 100644 --- a/src/functions/shared/src/services/gemini-service.ts +++ b/src/functions/shared/src/services/gemini-service.ts @@ -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 { - // 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: diff --git a/src/functions/shared/src/services/project-service.ts b/src/functions/shared/src/services/project-service.ts index 3877b9b..1f4920d 100644 --- a/src/functions/shared/src/services/project-service.ts +++ b/src/functions/shared/src/services/project-service.ts @@ -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 { + 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 { + 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 { + readProjectGuidelines(projectPath: string): string { const aiPath = path.join(projectPath, 'AI.md'); if (!fs.existsSync(aiPath)) { diff --git a/src/functions/shared/src/services/repository-service.ts b/src/functions/shared/src/services/repository-service.ts index 27d173f..7b58cce 100644 --- a/src/functions/shared/src/services/repository-service.ts +++ b/src/functions/shared/src/services/repository-service.ts @@ -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 { 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)}`); } } diff --git a/src/functions/shared/src/types.ts b/src/functions/shared/src/types.ts index 35e0e74..8ff250d 100644 --- a/src/functions/shared/src/types.ts +++ b/src/functions/shared/src/types.ts @@ -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; } diff --git a/src/prompts/prompts-to-test-spec/nitro-back/workitems/2025-06-08-test.md b/src/prompts/prompts-to-test-spec/nitro-back/workitems/2025-06-08-test.md index 083a812..fcfb1f3 100644 --- a/src/prompts/prompts-to-test-spec/nitro-back/workitems/2025-06-08-test.md +++ b/src/prompts/prompts-to-test-spec/nitro-back/workitems/2025-06-08-test.md @@ -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.