This commit is contained in:
cghislai 2025-06-08 01:20:00 +02:00
parent f32c78a94b
commit d6ddd8aa45
9 changed files with 268 additions and 139 deletions

View File

@ -1,2 +1,3 @@
node_modules/
dist/
.env

View File

@ -29,17 +29,21 @@ 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) {
@ -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',

View File

@ -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
}

View File

@ -80,6 +80,28 @@ 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', () => {

View File

@ -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<string> {
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;
}

View File

@ -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) {

View File

@ -14,22 +14,38 @@ export class ProjectService {
async findProjects(promptsDir: string): Promise<Project[]> {
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,6 +59,7 @@ export class ProjectService {
*/
async readProjectInfo(projectPath: string, projectName: string): Promise<Project> {
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
@ -50,13 +67,20 @@ export class ProjectService {
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;
}
/**

View File

@ -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')) {
@ -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<string> {
// Use Gemini to generate the pull request description
return await this.geminiService.generatePullRequestDescription(processedWorkitems);
}
}

View File

@ -35,4 +35,5 @@ export interface ProcessResult {
error?: string;
}[];
pullRequestUrl?: string;
error?: string;
}