From d6ddd8aa451ee29f2cdfc85a9796f71469608731 Mon Sep 17 00:00:00 2001 From: cghislai Date: Sun, 8 Jun 2025 01:20:00 +0200 Subject: [PATCH] WIP --- src/functions/prompts-to-test-spec/.gitignore | 1 + .../prompts-to-test-spec/src/config.ts | 36 ++++--- .../prompts-to-test-spec/src/index.ts | 2 +- .../__tests__/project-service.test.ts | 100 +++++++++++------- .../src/services/gemini-service.ts | 90 ++++++++++++++-- .../src/services/processor-service.ts | 48 +++++++-- .../src/services/project-service.ts | 64 +++++++---- .../src/services/pull-request-service.ts | 65 +++--------- .../prompts-to-test-spec/src/types.ts | 1 + 9 files changed, 268 insertions(+), 139 deletions(-) diff --git a/src/functions/prompts-to-test-spec/.gitignore b/src/functions/prompts-to-test-spec/.gitignore index b947077..deed335 100644 --- a/src/functions/prompts-to-test-spec/.gitignore +++ b/src/functions/prompts-to-test-spec/.gitignore @@ -1,2 +1,3 @@ node_modules/ dist/ +.env diff --git a/src/functions/prompts-to-test-spec/src/config.ts b/src/functions/prompts-to-test-spec/src/config.ts index 53b5eb8..f5eaa66 100644 --- a/src/functions/prompts-to-test-spec/src/config.ts +++ b/src/functions/prompts-to-test-spec/src/config.ts @@ -29,23 +29,27 @@ export const GEMINI_MODEL = process.env.GEMINI_MODEL || 'gemini-1.5-pro'; // Function configuration export const DEBUG = process.env.DEBUG === 'true'; +export const USE_LOCAL_REPO = process.env.USE_LOCAL_REPO === 'true'; // Validate required configuration export function validateConfig(): void { const missingVars: string[] = []; - - if (!MAIN_REPO_URL) { - missingVars.push('MAIN_REPO_URL'); + + // Only check for main repo URL and credentials if not using local repo + if (!USE_LOCAL_REPO) { + if (!MAIN_REPO_URL) { + missingVars.push('MAIN_REPO_URL'); + } + + if (!MAIN_REPO_TOKEN && (!MAIN_REPO_USERNAME || !MAIN_REPO_PASSWORD)) { + missingVars.push('MAIN_REPO_TOKEN or MAIN_REPO_USERNAME/MAIN_REPO_PASSWORD'); + } } - - if (!MAIN_REPO_TOKEN && (!MAIN_REPO_USERNAME || !MAIN_REPO_PASSWORD)) { - missingVars.push('MAIN_REPO_TOKEN or MAIN_REPO_USERNAME/MAIN_REPO_PASSWORD'); - } - + if (!GOOGLE_CLOUD_PROJECT_ID) { missingVars.push('GOOGLE_CLOUD_PROJECT_ID'); } - + if (missingVars.length > 0) { throw new Error(`Missing required environment variables: ${missingVars.join(', ')}`); } @@ -53,6 +57,14 @@ export function validateConfig(): void { // Get repository credentials for the main repository export function getMainRepoCredentials(): { type: 'username-password' | 'token'; username?: string; password?: string; token?: string } { + if (USE_LOCAL_REPO) { + // Return dummy credentials when using local repo + return { + type: 'token', + token: 'dummy-token-for-local-repo' + }; + } + if (MAIN_REPO_TOKEN) { return { type: 'token', @@ -65,7 +77,7 @@ export function getMainRepoCredentials(): { type: 'username-password' | 'token'; password: MAIN_REPO_PASSWORD }; } - + throw new Error('No credentials available for the main repository'); } @@ -83,7 +95,7 @@ export function getGithubCredentials(): { type: 'username-password' | 'token'; u password: GITHUB_PASSWORD }; } - + return undefined; } @@ -96,6 +108,6 @@ export function getGiteaCredentials(): { type: 'username-password'; username: st password: GITEA_PASSWORD }; } - + return undefined; } diff --git a/src/functions/prompts-to-test-spec/src/index.ts b/src/functions/prompts-to-test-spec/src/index.ts index d87ed34..d355105 100644 --- a/src/functions/prompts-to-test-spec/src/index.ts +++ b/src/functions/prompts-to-test-spec/src/index.ts @@ -6,7 +6,7 @@ import { validateConfig } from './config'; try { validateConfig(); } catch (error) { - console.error('Configuration error:', error.message); + console.error('Configuration error:', error instanceof Error ? error.message : String(error)); // Don't throw here to allow the function to start, but it will fail when executed } diff --git a/src/functions/prompts-to-test-spec/src/services/__tests__/project-service.test.ts b/src/functions/prompts-to-test-spec/src/services/__tests__/project-service.test.ts index a74950f..bc9ac07 100644 --- a/src/functions/prompts-to-test-spec/src/services/__tests__/project-service.test.ts +++ b/src/functions/prompts-to-test-spec/src/services/__tests__/project-service.test.ts @@ -8,17 +8,17 @@ jest.mock('path'); describe('ProjectService', () => { let projectService: ProjectService; - + beforeEach(() => { projectService = new ProjectService(); - + // Reset all mocks jest.resetAllMocks(); - + // Mock path.join to return predictable paths (path.join as jest.Mock).mockImplementation((...args) => args.join('/')); }); - + describe('findProjects', () => { it('should find all projects in the prompts directory', async () => { // Mock fs.readdirSync to return project directories @@ -28,12 +28,12 @@ describe('ProjectService', () => { { name: 'not-a-project', isDirectory: () => true }, { name: 'README.md', isDirectory: () => false } ]); - + // Mock fs.existsSync to return true for INFO.md files (fs.existsSync as jest.Mock).mockImplementation((path: string) => { return path.endsWith('project1/INFO.md') || path.endsWith('project2/INFO.md'); }); - + // Mock readProjectInfo jest.spyOn(projectService, 'readProjectInfo').mockImplementation(async (projectPath, projectName) => { return { @@ -44,9 +44,9 @@ describe('ProjectService', () => { jiraComponent: projectName }; }); - + const projects = await projectService.findProjects('prompts'); - + expect(projects).toHaveLength(2); expect(projects[0].name).toBe('project1'); expect(projects[1].name).toBe('project2'); @@ -56,21 +56,21 @@ describe('ProjectService', () => { expect(fs.existsSync).toHaveBeenCalledWith('prompts/not-a-project/INFO.md'); }); }); - + describe('readProjectInfo', () => { it('should read project information from INFO.md', async () => { const infoContent = `# Project Name - + - [x] Repo host: https://github.com - [x] Repo url: https://github.com/org/project.git - [x] Jira component: project-component `; - + // Mock fs.readFileSync to return INFO.md content (fs.readFileSync as jest.Mock).mockReturnValueOnce(infoContent); - + const project = await projectService.readProjectInfo('path/to/project', 'project'); - + expect(project).toEqual({ name: 'project', path: 'path/to/project', @@ -80,20 +80,42 @@ 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 () => { + const infoContent = `# Project Name + +This is a project description. +Some other content that doesn't match the expected format. +`; + + // 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'); + + expect(project).toEqual({ + name: 'project', + path: 'path/to/project', + repoHost: undefined, + repoUrl: undefined, + jiraComponent: undefined + }); + expect(fs.readFileSync).toHaveBeenCalledWith('path/to/project/INFO.md', 'utf-8'); + }); }); - + describe('findWorkitems', () => { it('should find all workitems in a project', async () => { // Mock fs.existsSync to return true for workitems directory (fs.existsSync as jest.Mock).mockReturnValueOnce(true); - + // Mock fs.readdirSync to return workitem files (fs.readdirSync as jest.Mock).mockReturnValueOnce([ 'workitem1.md', 'workitem2.md', 'not-a-workitem.txt' ]); - + // Mock readWorkitemInfo jest.spyOn(projectService, 'readWorkitemInfo').mockImplementation(async (workitemPath, fileName) => { return { @@ -106,32 +128,32 @@ describe('ProjectService', () => { isActive: true }; }); - + const workitems = await projectService.findWorkitems('path/to/project'); - + expect(workitems).toHaveLength(2); expect(workitems[0].name).toBe('workitem1'); expect(workitems[1].name).toBe('workitem2'); expect(fs.existsSync).toHaveBeenCalledWith('path/to/project/workitems'); expect(fs.readdirSync).toHaveBeenCalledWith('path/to/project/workitems'); }); - + it('should return empty array if workitems directory does not exist', async () => { // Mock fs.existsSync to return false for workitems directory (fs.existsSync as jest.Mock).mockReturnValueOnce(false); - + const workitems = await projectService.findWorkitems('path/to/project'); - + expect(workitems).toHaveLength(0); expect(fs.existsSync).toHaveBeenCalledWith('path/to/project/workitems'); expect(fs.readdirSync).not.toHaveBeenCalled(); }); }); - + describe('readWorkitemInfo', () => { it('should read workitem information from markdown file', async () => { const workitemContent = `## Workitem Title - + This is a description of the workitem. It has multiple lines. @@ -139,12 +161,12 @@ It has multiple lines. - [ ] Implementation: - [x] Active `; - + // Mock fs.readFileSync to return workitem content (fs.readFileSync as jest.Mock).mockReturnValueOnce(workitemContent); - + const workitem = await projectService.readWorkitemInfo('path/to/workitem.md', 'workitem.md'); - + expect(workitem).toEqual({ name: 'workitem', path: 'path/to/workitem.md', @@ -156,48 +178,48 @@ It has multiple lines. }); expect(fs.readFileSync).toHaveBeenCalledWith('path/to/workitem.md', 'utf-8'); }); - + it('should handle workitem without Active checkbox', async () => { const workitemContent = `## Workitem Title - + This is a description of the workitem. - [ ] Jira: JIRA-123 - [ ] Implementation: `; - + // Mock fs.readFileSync to return workitem content (fs.readFileSync as jest.Mock).mockReturnValueOnce(workitemContent); - + const workitem = await projectService.readWorkitemInfo('path/to/workitem.md', 'workitem.md'); - + expect(workitem.isActive).toBe(true); }); }); - + describe('readProjectGuidelines', () => { it('should read AI guidelines for a project', async () => { const guidelinesContent = '## Guidelines\n\nThese are the guidelines.'; - + // Mock fs.existsSync to return true for AI.md (fs.existsSync as jest.Mock).mockReturnValueOnce(true); - + // Mock fs.readFileSync to return guidelines content (fs.readFileSync as jest.Mock).mockReturnValueOnce(guidelinesContent); - + const guidelines = await 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 () => { // Mock fs.existsSync to return false for AI.md (fs.existsSync as jest.Mock).mockReturnValueOnce(false); - + const guidelines = await projectService.readProjectGuidelines('path/to/project'); - + expect(guidelines).toBe(''); expect(fs.existsSync).toHaveBeenCalledWith('path/to/project/AI.md'); expect(fs.readFileSync).not.toHaveBeenCalled(); diff --git a/src/functions/prompts-to-test-spec/src/services/gemini-service.ts b/src/functions/prompts-to-test-spec/src/services/gemini-service.ts index f30539b..e2cd609 100644 --- a/src/functions/prompts-to-test-spec/src/services/gemini-service.ts +++ b/src/functions/prompts-to-test-spec/src/services/gemini-service.ts @@ -126,21 +126,16 @@ export class GeminiService { const currentDate = new Date().toISOString(); + // Send the AI.md file directly to Gemini without hardcoded instructions const prompt = ` -You are tasked with creating a Cucumber feature file based on a workitem description. - -Project Guidelines: ${guidelines} Workitem: ${workitemContent} -Create a Cucumber feature file that implements this workitem according to the guidelines. -Include the following comment at the top of the file: +Include the following comment at the top of the generated file: # Generated by prompts-to-test-spec on ${currentDate} # Source: ${workitemName} - -The feature file should be complete and ready to use in a Cucumber test suite. `; const result = await generativeModel.generateContent({ @@ -148,7 +143,86 @@ The feature file should be complete and ready to use in a Cucumber test suite. }); const response = await result.response; - const generatedText = response.candidates[0].content.parts[0].text; + const generatedText = response.candidates[0]?.content?.parts[0]?.text || ''; + + return generatedText; + } + + /** + * Generate a pull request description using Gemini API + * @param processedWorkitems List of processed workitems + * @returns Generated pull request description + */ + async generatePullRequestDescription( + processedWorkitems: { workitem: Workitem; success: boolean; error?: string }[] + ): Promise { + const generativeModel = this.vertexAI.getGenerativeModel({ + model: this.model, + }); + + // Prepare workitem data for the prompt + const added: string[] = []; + const updated: string[] = []; + const deleted: string[] = []; + const failed: string[] = []; + + for (const item of processedWorkitems) { + const { workitem, success, error } = item; + + if (!success) { + failed.push(`- ${workitem.name}: ${error}`); + continue; + } + + if (!workitem.isActive) { + deleted.push(`- ${workitem.name}`); + } else if (workitem.implementation) { + updated.push(`- ${workitem.name}`); + } else { + added.push(`- ${workitem.name}`); + } + } + + // Create a structured summary of changes for Gemini + let workitemSummary = ''; + + if (added.length > 0) { + workitemSummary += 'Added workitems:\n' + added.join('\n') + '\n\n'; + } + + if (updated.length > 0) { + workitemSummary += 'Updated workitems:\n' + updated.join('\n') + '\n\n'; + } + + if (deleted.length > 0) { + workitemSummary += 'Deleted workitems:\n' + deleted.join('\n') + '\n\n'; + } + + if (failed.length > 0) { + workitemSummary += 'Failed workitems:\n' + failed.join('\n') + '\n\n'; + } + + const prompt = ` +You are tasked with creating a pull request description for changes to test specifications. + +The following is a summary of the changes made: +${workitemSummary} + +Create a clear, professional pull request description that: +1. Explains that this PR was automatically generated by the prompts-to-test-spec function +2. Summarizes the changes (added, updated, deleted, and failed workitems) +3. Uses markdown formatting for better readability +4. Keeps the description concise but informative + +The pull request description should be ready to use without further editing. +`; + + const result = await generativeModel.generateContent({ + contents: [{ role: 'user', parts: [{ text: prompt }] }], + }); + + const response = await result.response; + const generatedText = response.candidates[0]?.content?.parts[0]?.text || ''; return generatedText; } 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 8497ae9..9e46a0e 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 @@ -15,7 +15,8 @@ import { getGiteaCredentials, GOOGLE_CLOUD_PROJECT_ID, GOOGLE_CLOUD_LOCATION, - GEMINI_MODEL + GEMINI_MODEL, + USE_LOCAL_REPO } from '../config'; export class ProcessorService { @@ -42,9 +43,15 @@ export class ProcessorService { ); this.pullRequestService = new PullRequestService(); - // Get main repository URL and credentials - this.mainRepoUrl = MAIN_REPO_URL; - this.mainRepoCredentials = getMainRepoCredentials(); + // Get main repository URL and credentials only if not using local repo + if (!USE_LOCAL_REPO) { + this.mainRepoUrl = MAIN_REPO_URL; + this.mainRepoCredentials = getMainRepoCredentials(); + } else { + // Set dummy values when using local repo + this.mainRepoUrl = ''; + this.mainRepoCredentials = getMainRepoCredentials(); + } // Initialize other credentials this.githubCredentials = getGithubCredentials(); @@ -84,12 +91,19 @@ export class ProcessorService { const results: ProcessResult[] = []; try { - // Clone the main repository - console.log(`Cloning main repository: ${this.mainRepoUrl}`); - const mainRepoPath = await this.repositoryService.cloneMainRepository( - this.mainRepoUrl, - this.mainRepoCredentials - ); + // Use local repository or clone the main repository + let mainRepoPath: string; + if (USE_LOCAL_REPO) { + console.log('Using local repository path'); + mainRepoPath = path.resolve(__dirname, '../../..'); + console.log(`Resolved local repository path: ${mainRepoPath}`); + } else { + console.log(`Cloning main repository: ${this.mainRepoUrl}`); + mainRepoPath = await this.repositoryService.cloneMainRepository( + this.mainRepoUrl, + this.mainRepoCredentials + ); + } // Find all projects in the prompts directory const promptsDir = path.join(mainRepoPath, 'src', 'prompts'); @@ -98,10 +112,23 @@ export class ProcessorService { console.log(`Found ${projects.length} projects`); + // Log details of each project + if (projects.length > 0) { + console.log('Projects found:'); + projects.forEach((project, index) => { + console.log(` ${index + 1}. ${project.name} (${project.path})`); + }); + } else { + console.log('No projects found. Check if the prompts directory exists and contains project folders.'); + } + // Process each project + console.log('Starting to process projects...'); for (const project of projects) { try { + console.log(`Starting processing of project: ${project.name}`); const result = await this.processProject(project); + console.log(`Finished processing project: ${project.name}`); results.push(result); } catch (error) { console.error(`Error processing project ${project.name}:`, error); @@ -112,6 +139,7 @@ export class ProcessorService { }); } } + console.log(`Finished processing all ${projects.length} projects`); return results; } catch (error) { 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 e952061..188a83e 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 @@ -13,25 +13,41 @@ export class ProjectService { */ async findProjects(promptsDir: string): Promise { const projects: Project[] = []; - + + console.log(`ProjectService: Searching for projects in ${promptsDir}`); + + // Check if prompts directory exists + if (!fs.existsSync(promptsDir)) { + console.log(`ProjectService: Directory does not exist: ${promptsDir}`); + return projects; + } + // Get all directories in the prompts directory const entries = fs.readdirSync(promptsDir, { withFileTypes: true }); const projectDirs = entries.filter(entry => entry.isDirectory()); - + + console.log(`ProjectService: Found ${projectDirs.length} potential project directories`); + for (const dir of projectDirs) { const projectPath = path.join(promptsDir, dir.name); const infoPath = path.join(projectPath, 'INFO.md'); - + + console.log(`ProjectService: Checking directory: ${dir.name}`); + // Skip directories without INFO.md if (!fs.existsSync(infoPath)) { + console.log(`ProjectService: Skipping ${dir.name} - no INFO.md file found`); continue; } - + + console.log(`ProjectService: Found INFO.md in ${dir.name}, reading project info`); + // Read project info const project = await this.readProjectInfo(projectPath, dir.name); projects.push(project); + console.log(`ProjectService: Added project: ${project.name}`); } - + return projects; } @@ -43,20 +59,28 @@ export class ProjectService { */ async readProjectInfo(projectPath: string, projectName: string): Promise { const infoPath = path.join(projectPath, 'INFO.md'); + console.log(`ProjectService: Reading project info from ${infoPath}`); const infoContent = fs.readFileSync(infoPath, 'utf-8'); - + // Parse INFO.md content const repoHostMatch = infoContent.match(/- \[[ x]\] Repo host: (.*)/); const repoUrlMatch = infoContent.match(/- \[[ x]\] Repo url: (.*)/); const jiraComponentMatch = infoContent.match(/- \[[ x]\] Jira component: (.*)/); - - return { + + const project = { name: projectName, path: projectPath, repoHost: repoHostMatch ? repoHostMatch[1].trim() : undefined, repoUrl: repoUrlMatch ? repoUrlMatch[1].trim() : undefined, jiraComponent: jiraComponentMatch ? jiraComponentMatch[1].trim() : undefined }; + + console.log(`ProjectService: Project info for ${projectName}:`); + console.log(` - Repository host: ${project.repoHost || 'Not found'}`); + console.log(` - Repository URL: ${project.repoUrl || 'Not found'}`); + console.log(` - Jira component: ${project.jiraComponent || 'Not found'}`); + + return project; } /** @@ -67,22 +91,22 @@ export class ProjectService { async findWorkitems(projectPath: string): Promise { const workitems: Workitem[] = []; const workitemsDir = path.join(projectPath, 'workitems'); - + // Skip if workitems directory doesn't exist if (!fs.existsSync(workitemsDir)) { return workitems; } - + // Get all markdown files in the workitems directory const files = fs.readdirSync(workitemsDir) .filter(file => file.endsWith('.md')); - + for (const file of files) { const workitemPath = path.join(workitemsDir, file); const workitem = await this.readWorkitemInfo(workitemPath, file); workitems.push(workitem); } - + return workitems; } @@ -94,19 +118,19 @@ export class ProjectService { */ async readWorkitemInfo(workitemPath: string, fileName: string): Promise { const content = fs.readFileSync(workitemPath, 'utf-8'); - + // Parse workitem content const titleMatch = content.match(/## (.*)/); const jiraMatch = content.match(/- \[[ x]\] Jira: (.*)/); const implementationMatch = content.match(/- \[[ x]\] Implementation: (.*)/); const activeMatch = content.match(/- \[([x ])\] Active/); - + // Extract description (everything between title and first metadata line) let description = ''; const lines = content.split('\n'); let titleIndex = -1; let metadataIndex = -1; - + for (let i = 0; i < lines.length; i++) { if (titleIndex === -1 && lines[i].startsWith('## ')) { titleIndex = i; @@ -114,15 +138,15 @@ export class ProjectService { metadataIndex = i; } } - + if (titleIndex !== -1 && metadataIndex !== -1) { description = lines.slice(titleIndex + 1, metadataIndex).join('\n').trim(); } - + // Determine if workitem is active // If the Active checkbox is missing, assume it's active const isActive = activeMatch ? activeMatch[1] === 'x' : true; - + return { name: fileName.replace('.md', ''), path: workitemPath, @@ -141,11 +165,11 @@ export class ProjectService { */ async readProjectGuidelines(projectPath: string): Promise { const aiPath = path.join(projectPath, 'AI.md'); - + if (!fs.existsSync(aiPath)) { return ''; } - + return fs.readFileSync(aiPath, 'utf-8'); } } diff --git a/src/functions/prompts-to-test-spec/src/services/pull-request-service.ts b/src/functions/prompts-to-test-spec/src/services/pull-request-service.ts index d88eeaa..20b762d 100644 --- a/src/functions/prompts-to-test-spec/src/services/pull-request-service.ts +++ b/src/functions/prompts-to-test-spec/src/services/pull-request-service.ts @@ -4,8 +4,14 @@ import axios from 'axios'; import * as path from 'path'; import { Project, RepoCredentials, Workitem } from '../types'; +import { GeminiService } from './gemini-service'; export class PullRequestService { + private geminiService: GeminiService; + + constructor() { + this.geminiService = new GeminiService(); + } /** * Create a pull request for changes in a repository * @param project Project information @@ -26,7 +32,7 @@ export class PullRequestService { // Generate PR title and description const title = `Update workitems: ${new Date().toISOString().split('T')[0]}`; - const description = this.generatePullRequestDescription(processedWorkitems); + const description = await this.generatePullRequestDescription(processedWorkitems); // Determine the repository host type and create PR accordingly if (project.repoHost.includes('github.com')) { @@ -61,11 +67,11 @@ export class PullRequestService { // Create the pull request const apiUrl = `https://api.github.com/repos/${owner}/${repo}/pulls`; - + const headers: Record = { 'Accept': 'application/vnd.github.v3+json', }; - + if (credentials.type === 'token' && credentials.token) { headers['Authorization'] = `token ${credentials.token}`; } else if (credentials.type === 'username-password' && credentials.username && credentials.password) { @@ -112,12 +118,12 @@ export class PullRequestService { // Create the pull request const apiUrl = `${project.repoHost}/api/v1/repos/${owner}/${repo}/pulls`; - + const headers: Record = { 'Accept': 'application/json', 'Content-Type': 'application/json', }; - + if (credentials.type === 'token' && credentials.token) { headers['Authorization'] = `token ${credentials.token}`; } else if (credentials.type === 'username-password' && credentials.username && credentials.password) { @@ -142,53 +148,14 @@ export class PullRequestService { } /** - * Generate a description for the pull request + * Generate a description for the pull request using Gemini * @param processedWorkitems List of processed workitems * @returns Pull request description */ - private generatePullRequestDescription( + private async generatePullRequestDescription( processedWorkitems: { workitem: Workitem; success: boolean; error?: string }[] - ): string { - const added: string[] = []; - const updated: string[] = []; - const deleted: string[] = []; - const failed: string[] = []; - - for (const item of processedWorkitems) { - const { workitem, success, error } = item; - - if (!success) { - failed.push(`- ${workitem.name}: ${error}`); - continue; - } - - if (!workitem.isActive) { - deleted.push(`- ${workitem.name}`); - } else if (workitem.implementation) { - updated.push(`- ${workitem.name}`); - } else { - added.push(`- ${workitem.name}`); - } - } - - let description = 'This PR was automatically generated by the prompts-to-test-spec function.\n\n'; - - if (added.length > 0) { - description += '## Added\n' + added.join('\n') + '\n\n'; - } - - if (updated.length > 0) { - description += '## Updated\n' + updated.join('\n') + '\n\n'; - } - - if (deleted.length > 0) { - description += '## Deleted\n' + deleted.join('\n') + '\n\n'; - } - - if (failed.length > 0) { - description += '## Failed\n' + failed.join('\n') + '\n\n'; - } - - return description; + ): Promise { + // Use Gemini to generate the pull request description + return await this.geminiService.generatePullRequestDescription(processedWorkitems); } } diff --git a/src/functions/prompts-to-test-spec/src/types.ts b/src/functions/prompts-to-test-spec/src/types.ts index 4fffe0e..aa9bfe5 100644 --- a/src/functions/prompts-to-test-spec/src/types.ts +++ b/src/functions/prompts-to-test-spec/src/types.ts @@ -35,4 +35,5 @@ export interface ProcessResult { error?: string; }[]; pullRequestUrl?: string; + error?: string; }