WIP
This commit is contained in:
parent
f32c78a94b
commit
d6ddd8aa45
@ -1,2 +1,3 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
|
@ -29,11 +29,14 @@ 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[] = [];
|
||||
|
||||
// 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');
|
||||
}
|
||||
@ -41,6 +44,7 @@ export function validateConfig(): void {
|
||||
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');
|
||||
@ -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',
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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', () => {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
// 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
|
||||
// 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}`);
|
||||
const mainRepoPath = await this.repositoryService.cloneMainRepository(
|
||||
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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -35,4 +35,5 @@ export interface ProcessResult {
|
||||
error?: string;
|
||||
}[];
|
||||
pullRequestUrl?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user