WIP
This commit is contained in:
parent
c160d9bc5e
commit
9e4b0c2abb
@ -1,262 +0,0 @@
|
|||||||
/**
|
|
||||||
* Service for handling Gemini API operations
|
|
||||||
*/
|
|
||||||
import {
|
|
||||||
FunctionDeclarationSchemaType,
|
|
||||||
GenerateContentCandidate,
|
|
||||||
GenerateContentRequest,
|
|
||||||
Tool,
|
|
||||||
VertexAI
|
|
||||||
} from '@google-cloud/vertexai';
|
|
||||||
import {Workitem} from '../types';
|
|
||||||
import {DRY_RUN_SKIP_GEMINI, GEMINI_MODEL, GOOGLE_CLOUD_LOCATION, GOOGLE_CLOUD_PROJECT_ID} from '../config';
|
|
||||||
import {GeminiProjectProcessor} from './gemini-project-processor';
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface for the model response format
|
|
||||||
*/
|
|
||||||
interface ModelResponse {
|
|
||||||
decision: 'create' | 'update' | 'delete' | 'skip';
|
|
||||||
reason: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface for the result returned by generateFeatureFile
|
|
||||||
*/
|
|
||||||
interface GenerateFeatureFileResult {
|
|
||||||
text: string;
|
|
||||||
decision?: ModelResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface for function arguments
|
|
||||||
*/
|
|
||||||
interface FunctionArgs {
|
|
||||||
filePath?: string;
|
|
||||||
content?: string;
|
|
||||||
dirPath?: string;
|
|
||||||
searchString?: string;
|
|
||||||
filePattern?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class GeminiService {
|
|
||||||
private vertexAI: VertexAI;
|
|
||||||
private model: string;
|
|
||||||
private projectId: string;
|
|
||||||
private location: string;
|
|
||||||
private fileOperationTools: Tool[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new GeminiService instance
|
|
||||||
* @param projectId Google Cloud project ID (defaults to GOOGLE_CLOUD_PROJECT_ID from config)
|
|
||||||
* @param location Google Cloud location (defaults to GOOGLE_CLOUD_LOCATION from config)
|
|
||||||
* @param model Gemini model to use (defaults to GEMINI_MODEL from config)
|
|
||||||
*/
|
|
||||||
constructor(projectId?: string, location?: string, model?: string) {
|
|
||||||
this.projectId = projectId || GOOGLE_CLOUD_PROJECT_ID;
|
|
||||||
this.location = location || GOOGLE_CLOUD_LOCATION;
|
|
||||||
this.model = model || GEMINI_MODEL;
|
|
||||||
|
|
||||||
if (!this.projectId) {
|
|
||||||
throw new Error('Google Cloud Project ID is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize VertexAI with default authentication
|
|
||||||
// Authentication is handled by the Google Auth library, which supports:
|
|
||||||
// 1. API keys via GOOGLE_API_KEY environment variable
|
|
||||||
// 2. Service accounts via GOOGLE_APPLICATION_CREDENTIALS environment variable
|
|
||||||
// 3. Application Default Credentials
|
|
||||||
// See: https://cloud.google.com/vertex-ai/docs/authentication
|
|
||||||
this.vertexAI = new VertexAI({
|
|
||||||
project: this.projectId,
|
|
||||||
location: this.location,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define file operation functions
|
|
||||||
this.fileOperationTools = [
|
|
||||||
{
|
|
||||||
function_declarations: [
|
|
||||||
{
|
|
||||||
name: "getFileContent",
|
|
||||||
description: "Get the content of a file in the project repository",
|
|
||||||
parameters: {
|
|
||||||
type: FunctionDeclarationSchemaType.OBJECT,
|
|
||||||
properties: {
|
|
||||||
filePath: {
|
|
||||||
type: FunctionDeclarationSchemaType.STRING,
|
|
||||||
description: "Path to the file relative to the project repository root"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
required: ["filePath"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "writeFileContent",
|
|
||||||
description: "Write content to a file in the project repository",
|
|
||||||
parameters: {
|
|
||||||
type: FunctionDeclarationSchemaType.OBJECT,
|
|
||||||
properties: {
|
|
||||||
filePath: {
|
|
||||||
type: FunctionDeclarationSchemaType.STRING,
|
|
||||||
description: "Path to the file relative to the project repository root"
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
type: FunctionDeclarationSchemaType.STRING,
|
|
||||||
description: "Content to write to the file"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
required: ["filePath", "content"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "fileExists",
|
|
||||||
description: "Check if a file exists in the project repository",
|
|
||||||
parameters: {
|
|
||||||
type: FunctionDeclarationSchemaType.OBJECT,
|
|
||||||
properties: {
|
|
||||||
filePath: {
|
|
||||||
type: FunctionDeclarationSchemaType.STRING,
|
|
||||||
description: "Path to the file relative to the project repository root"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
required: ["filePath"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "listFiles",
|
|
||||||
description: "List files in a directory in the project repository",
|
|
||||||
parameters: {
|
|
||||||
type: FunctionDeclarationSchemaType.OBJECT,
|
|
||||||
properties: {
|
|
||||||
dirPath: {
|
|
||||||
type: FunctionDeclarationSchemaType.STRING,
|
|
||||||
description: "Path to the directory relative to the project repository root"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
required: ["dirPath"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "grepFiles",
|
|
||||||
description: "Search for a string in project files",
|
|
||||||
parameters: {
|
|
||||||
type: FunctionDeclarationSchemaType.OBJECT,
|
|
||||||
properties: {
|
|
||||||
searchString: {
|
|
||||||
type: FunctionDeclarationSchemaType.STRING,
|
|
||||||
description: "String to search for in project files"
|
|
||||||
},
|
|
||||||
filePattern: {
|
|
||||||
type: FunctionDeclarationSchemaType.STRING,
|
|
||||||
description: "Optional file pattern to limit the search (e.g., '*.ts', 'src/*.java')"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
required: ["searchString"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "deleteFile",
|
|
||||||
description: "Delete a file from the project repository",
|
|
||||||
parameters: {
|
|
||||||
type: FunctionDeclarationSchemaType.OBJECT,
|
|
||||||
properties: {
|
|
||||||
filePath: {
|
|
||||||
type: FunctionDeclarationSchemaType.STRING,
|
|
||||||
description: "Path to the file relative to the project repository root"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
required: ["filePath"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a pull request description using Gemini API
|
|
||||||
* @param processedWorkitems List of processed workitems with their status
|
|
||||||
* @param gitPatch Optional git patch showing code changes
|
|
||||||
* @returns Generated pull request description in markdown format
|
|
||||||
* @example
|
|
||||||
* const geminiService = new GeminiService();
|
|
||||||
* const prDescription = await geminiService.generatePullRequestDescription(processedWorkitems, gitPatch);
|
|
||||||
*/
|
|
||||||
async generatePullRequestDescription(
|
|
||||||
processedWorkitems: { workitem: Workitem; success: boolean; error?: string }[],
|
|
||||||
gitPatch?: string
|
|
||||||
): Promise<string> {
|
|
||||||
// Prepare workitem data for the prompt
|
|
||||||
const all: string[] = [];
|
|
||||||
|
|
||||||
for (const item of processedWorkitems) {
|
|
||||||
const {workitem, success, error} = item;
|
|
||||||
|
|
||||||
// Add all workitems to the all list regardless of status
|
|
||||||
all.push(`- ${workitem.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a structured summary of changes
|
|
||||||
let workitemSummary = '';
|
|
||||||
|
|
||||||
if (all.length > 0) {
|
|
||||||
workitemSummary += 'All workitems:\n' + all.join('\n') + '\n\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
// If dry run is enabled, return a mock PR description
|
|
||||||
if (DRY_RUN_SKIP_GEMINI) {
|
|
||||||
console.log(`[DRY RUN] Skipping Gemini API call for generating pull request description`);
|
|
||||||
return `# Automated PR: Update Workitems (DRY RUN)
|
|
||||||
|
|
||||||
This pull request was automatically generated by the prompts-to-test-spec function in dry run mode.
|
|
||||||
|
|
||||||
## Changes Summary
|
|
||||||
|
|
||||||
${workitemSummary}
|
|
||||||
|
|
||||||
*Note: This is a mock PR description generated during dry run. No actual Gemini API call was made.*`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const generativeModel = this.vertexAI.getGenerativeModel({
|
|
||||||
model: this.model,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Prepare the git patch section if available
|
|
||||||
let gitPatchSection = '';
|
|
||||||
if (gitPatch && gitPatch !== "No changes detected.") {
|
|
||||||
gitPatchSection = `
|
|
||||||
## Code Changes
|
|
||||||
The following code changes were made in this pull request:
|
|
||||||
|
|
||||||
\`\`\`diff
|
|
||||||
${gitPatch}
|
|
||||||
\`\`\`
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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}
|
|
||||||
${gitPatch && gitPatch !== "No changes detected." ? gitPatchSection : ''}
|
|
||||||
|
|
||||||
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. If code changes are provided, analyze them and include a summary of the key changes
|
|
||||||
4. Uses markdown formatting for better readability
|
|
||||||
5. Keeps the description concise but informative
|
|
||||||
|
|
||||||
The pull request description should be ready to use without further editing.
|
|
||||||
`;
|
|
||||||
|
|
||||||
const result = await generativeModel.generateContent(prompt);
|
|
||||||
|
|
||||||
const response = await result.response;
|
|
||||||
const generatedText = response.candidates[0]?.content?.parts[0]?.text || '';
|
|
||||||
|
|
||||||
return generatedText;
|
|
||||||
}
|
|
||||||
}
|
|
@ -8,9 +8,9 @@ import {
|
|||||||
Tool,
|
Tool,
|
||||||
VertexAI
|
VertexAI
|
||||||
} from '@google-cloud/vertexai';
|
} from '@google-cloud/vertexai';
|
||||||
import { Workitem } from '../types';
|
import {Workitem} from '../types';
|
||||||
import { DRY_RUN_SKIP_GEMINI, GEMINI_MODEL, GOOGLE_CLOUD_LOCATION, GOOGLE_CLOUD_PROJECT_ID } from '../config';
|
import {DRY_RUN_SKIP_GEMINI, GEMINI_MODEL, GOOGLE_CLOUD_LOCATION, GOOGLE_CLOUD_PROJECT_ID} from '../config';
|
||||||
import { GeminiProjectProcessor } from './gemini-project-processor';
|
import {GeminiProjectProcessor} from './gemini-project-processor';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for the model response format
|
* Interface for the model response format
|
||||||
@ -238,18 +238,7 @@ export class ModelStreamService {
|
|||||||
// If dry run is enabled, return a mock result
|
// If dry run is enabled, return a mock result
|
||||||
if (this.dryRun || DRY_RUN_SKIP_GEMINI) {
|
if (this.dryRun || DRY_RUN_SKIP_GEMINI) {
|
||||||
console.log(`[DRY RUN] Skipping Gemini API call for processing workitem ${this.workitem.name}`);
|
console.log(`[DRY RUN] Skipping Gemini API call for processing workitem ${this.workitem.name}`);
|
||||||
const mockText = `# Generated by prompts-to-test-spec on ${currentDate} (DRY RUN)
|
const mockText = `# Generated by prompts-to-test-spec on ${currentDate} (DRY RUN)`;
|
||||||
# Source: ${this.workitem.name}
|
|
||||||
|
|
||||||
Feature: ${this.workitem.name} (DRY RUN)
|
|
||||||
This is a mock feature file generated during dry run.
|
|
||||||
No actual Gemini API call was made.
|
|
||||||
|
|
||||||
Scenario: Mock scenario
|
|
||||||
Given a dry run is enabled
|
|
||||||
When the feature file is generated
|
|
||||||
Then a mock feature file is returned
|
|
||||||
`;
|
|
||||||
return {
|
return {
|
||||||
text: mockText,
|
text: mockText,
|
||||||
decision: {
|
decision: {
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Service for handling pull request operations
|
* Service for handling pull request operations
|
||||||
*/
|
*/
|
||||||
import {PullRequestService as SharedPullRequestService, Project, RepoCredentials, Workitem} from 'shared-functions';
|
import {PullRequestService as SharedPullRequestService, Project, RepoCredentials, Workitem, GeminiService} from 'shared-functions';
|
||||||
import {GeminiService} from './gemini-service';
|
import {GOOGLE_CLOUD_PROJECT_ID, GOOGLE_CLOUD_LOCATION, GEMINI_MODEL, DRY_RUN_SKIP_GEMINI} from '../config';
|
||||||
|
|
||||||
export class PullRequestService {
|
export class PullRequestService {
|
||||||
private sharedPullRequestService: SharedPullRequestService;
|
private sharedPullRequestService: SharedPullRequestService;
|
||||||
@ -10,7 +10,12 @@ export class PullRequestService {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.sharedPullRequestService = new SharedPullRequestService();
|
this.sharedPullRequestService = new SharedPullRequestService();
|
||||||
this.geminiService = new GeminiService();
|
this.geminiService = new GeminiService(
|
||||||
|
GOOGLE_CLOUD_PROJECT_ID,
|
||||||
|
GOOGLE_CLOUD_LOCATION,
|
||||||
|
GEMINI_MODEL,
|
||||||
|
DRY_RUN_SKIP_GEMINI
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
260
src/functions/shared/package-lock.json
generated
260
src/functions/shared/package-lock.json
generated
@ -8,6 +8,7 @@
|
|||||||
"name": "shared-functions",
|
"name": "shared-functions",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@google-cloud/vertexai": "^0.5.0",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
"simple-git": "^3.20.0"
|
"simple-git": "^3.20.0"
|
||||||
},
|
},
|
||||||
@ -596,6 +597,18 @@
|
|||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@google-cloud/vertexai": {
|
||||||
|
"version": "0.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@google-cloud/vertexai/-/vertexai-0.5.0.tgz",
|
||||||
|
"integrity": "sha512-qIFHYTXA5UCLdm9JG+Xf1suomCXxRqa1PKdYjqXuhZsCm8mn37Rb0Tf8djlhDzuRVWyWoQTmsWpsk28ZTmbqJg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"google-auth-library": "^9.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanwhocodes/config-array": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.13.0",
|
"version": "0.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
|
||||||
@ -1333,6 +1346,15 @@
|
|||||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/agent-base": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "6.12.6",
|
"version": "6.12.6",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
@ -1573,6 +1595,35 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/base64-js": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/bignumber.js": {
|
||||||
|
"version": "9.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.0.tgz",
|
||||||
|
"integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||||
@ -1653,6 +1704,12 @@
|
|||||||
"node-int64": "^0.4.0"
|
"node-int64": "^0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer-equal-constant-time": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/buffer-from": {
|
"node_modules/buffer-from": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||||
@ -1985,6 +2042,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ecdsa-sig-formatter": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ejs": {
|
"node_modules/ejs": {
|
||||||
"version": "3.1.10",
|
"version": "3.1.10",
|
||||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
|
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
|
||||||
@ -2321,6 +2387,12 @@
|
|||||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/extend": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
@ -2527,6 +2599,36 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/gaxios": {
|
||||||
|
"version": "6.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
|
||||||
|
"integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"extend": "^3.0.2",
|
||||||
|
"https-proxy-agent": "^7.0.1",
|
||||||
|
"is-stream": "^2.0.0",
|
||||||
|
"node-fetch": "^2.6.9",
|
||||||
|
"uuid": "^9.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gcp-metadata": {
|
||||||
|
"version": "6.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz",
|
||||||
|
"integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"gaxios": "^6.1.1",
|
||||||
|
"google-logging-utils": "^0.0.2",
|
||||||
|
"json-bigint": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/gensync": {
|
"node_modules/gensync": {
|
||||||
"version": "1.0.0-beta.2",
|
"version": "1.0.0-beta.2",
|
||||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||||
@ -2658,6 +2760,32 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/google-auth-library": {
|
||||||
|
"version": "9.15.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz",
|
||||||
|
"integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.0",
|
||||||
|
"ecdsa-sig-formatter": "^1.0.11",
|
||||||
|
"gaxios": "^6.1.1",
|
||||||
|
"gcp-metadata": "^6.1.0",
|
||||||
|
"gtoken": "^7.0.0",
|
||||||
|
"jws": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/google-logging-utils": {
|
||||||
|
"version": "0.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz",
|
||||||
|
"integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/gopd": {
|
"node_modules/gopd": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
@ -2684,6 +2812,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/gtoken": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"gaxios": "^6.0.0",
|
||||||
|
"jws": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/has-flag": {
|
"node_modules/has-flag": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
@ -2740,6 +2881,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/https-proxy-agent": {
|
||||||
|
"version": "7.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||||
|
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "^7.1.2",
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/human-signals": {
|
"node_modules/human-signals": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
||||||
@ -2916,7 +3070,6 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||||
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@ -3664,6 +3817,15 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/json-bigint": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bignumber.js": "^9.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/json-buffer": {
|
"node_modules/json-buffer": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||||
@ -3705,6 +3867,27 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jwa": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-equal-constant-time": "^1.0.1",
|
||||||
|
"ecdsa-sig-formatter": "1.0.11",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jws": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jwa": "^2.0.0",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/keyv": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
@ -3929,6 +4112,26 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/node-fetch": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-url": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "4.x || >=6.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"encoding": "^0.1.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"encoding": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-int64": {
|
"node_modules/node-int64": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
||||||
@ -4459,6 +4662,26 @@
|
|||||||
"queue-microtask": "^1.2.2"
|
"queue-microtask": "^1.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
@ -4725,6 +4948,12 @@
|
|||||||
"node": ">=8.0"
|
"node": ">=8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tr46": {
|
||||||
|
"version": "0.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||||
|
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ts-jest": {
|
"node_modules/ts-jest": {
|
||||||
"version": "29.3.4",
|
"version": "29.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.4.tgz",
|
||||||
@ -4899,6 +5128,19 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "9.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||||
|
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/v8-to-istanbul": {
|
"node_modules/v8-to-istanbul": {
|
||||||
"version": "9.3.0",
|
"version": "9.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
|
||||||
@ -4924,6 +5166,22 @@
|
|||||||
"makeerror": "1.0.12"
|
"makeerror": "1.0.12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/webidl-conversions": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-url": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tr46": "~0.0.3",
|
||||||
|
"webidl-conversions": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
"lint": "eslint src/**/*.ts"
|
"lint": "eslint src/**/*.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@google-cloud/vertexai": "^0.5.0",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
"simple-git": "^3.20.0"
|
"simple-git": "^3.20.0"
|
||||||
},
|
},
|
||||||
|
@ -9,3 +9,4 @@ export * from './types';
|
|||||||
export { ProjectService } from './services/project-service';
|
export { ProjectService } from './services/project-service';
|
||||||
export { RepositoryService } from './services/repository-service';
|
export { RepositoryService } from './services/repository-service';
|
||||||
export { PullRequestService } from './services/pull-request-service';
|
export { PullRequestService } from './services/pull-request-service';
|
||||||
|
export { GeminiService } from './services/gemini-service';
|
||||||
|
@ -0,0 +1,162 @@
|
|||||||
|
/**
|
||||||
|
* 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.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
136
src/functions/shared/src/services/gemini-service.ts
Normal file
136
src/functions/shared/src/services/gemini-service.ts
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
/**
|
||||||
|
* Service for handling Gemini API operations
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
GenerateContentCandidate,
|
||||||
|
VertexAI
|
||||||
|
} from '@google-cloud/vertexai';
|
||||||
|
import { Workitem } from '../types';
|
||||||
|
|
||||||
|
export class GeminiService {
|
||||||
|
private vertexAI: VertexAI;
|
||||||
|
private model: string;
|
||||||
|
private projectId: string;
|
||||||
|
private location: string;
|
||||||
|
private dryRunSkipGemini: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new GeminiService instance
|
||||||
|
* @param projectId Google Cloud project ID
|
||||||
|
* @param location Google Cloud location
|
||||||
|
* @param model Gemini model to use
|
||||||
|
* @param dryRunSkipGemini Whether to skip Gemini API calls in dry run mode
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
projectId: string,
|
||||||
|
location: string = 'us-central1',
|
||||||
|
model: string = 'gemini-1.5-pro',
|
||||||
|
dryRunSkipGemini: boolean = false
|
||||||
|
) {
|
||||||
|
this.projectId = projectId;
|
||||||
|
this.location = location;
|
||||||
|
this.model = model;
|
||||||
|
this.dryRunSkipGemini = dryRunSkipGemini;
|
||||||
|
|
||||||
|
if (!this.projectId) {
|
||||||
|
throw new Error('Google Cloud Project ID is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize VertexAI with default authentication
|
||||||
|
// Authentication is handled by the Google Auth library, which supports:
|
||||||
|
// 1. API keys via GOOGLE_API_KEY environment variable
|
||||||
|
// 2. Service accounts via GOOGLE_APPLICATION_CREDENTIALS environment variable
|
||||||
|
// 3. Application Default Credentials
|
||||||
|
// See: https://cloud.google.com/vertex-ai/docs/authentication
|
||||||
|
this.vertexAI = new VertexAI({
|
||||||
|
project: this.projectId,
|
||||||
|
location: this.location,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a pull request description using Gemini API
|
||||||
|
* @param processedWorkitems List of processed workitems with their status
|
||||||
|
* @param gitPatch Optional git patch showing code changes
|
||||||
|
* @returns Generated pull request description in markdown format
|
||||||
|
* @example
|
||||||
|
* const geminiService = new GeminiService('my-project-id');
|
||||||
|
* const prDescription = await geminiService.generatePullRequestDescription(processedWorkitems, gitPatch);
|
||||||
|
*/
|
||||||
|
async generatePullRequestDescription(
|
||||||
|
processedWorkitems: { workitem: Workitem; success: boolean; error?: string }[],
|
||||||
|
gitPatch?: string
|
||||||
|
): Promise<string> {
|
||||||
|
// Prepare workitem data for the prompt
|
||||||
|
const all: string[] = [];
|
||||||
|
|
||||||
|
for (const item of processedWorkitems) {
|
||||||
|
const {workitem, success, error} = item;
|
||||||
|
|
||||||
|
// Add all workitems to the all list regardless of status
|
||||||
|
all.push(`- ${workitem.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a structured summary of changes
|
||||||
|
let workitemSummary = '';
|
||||||
|
|
||||||
|
if (all.length > 0) {
|
||||||
|
workitemSummary += 'All workitems:\n' + all.join('\n') + '\n\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// If dry run is enabled, return a mock PR description
|
||||||
|
if (this.dryRunSkipGemini) {
|
||||||
|
console.log(`[DRY RUN] Skipping Gemini API call for generating pull request description`);
|
||||||
|
return `# Automated PR: Update Workitems (DRY RUN)
|
||||||
|
|
||||||
|
This pull request was automatically generated in dry run mode.
|
||||||
|
|
||||||
|
## Changes Summary
|
||||||
|
|
||||||
|
${workitemSummary}
|
||||||
|
|
||||||
|
*Note: This is a mock PR description generated during dry run. No actual Gemini API call was made.*`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const generativeModel = this.vertexAI.getGenerativeModel({
|
||||||
|
model: this.model,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prepare the git patch section if available
|
||||||
|
let gitPatchSection = '';
|
||||||
|
if (gitPatch && gitPatch !== "No changes detected.") {
|
||||||
|
gitPatchSection = `
|
||||||
|
## Code Changes
|
||||||
|
The following code changes were made in this pull request:
|
||||||
|
|
||||||
|
\`\`\`diff
|
||||||
|
${gitPatch}
|
||||||
|
\`\`\`
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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}
|
||||||
|
${gitPatch && gitPatch !== "No changes detected." ? gitPatchSection : ''}
|
||||||
|
|
||||||
|
Create a clear, professional pull request description that:
|
||||||
|
1. Explains that this PR was automatically generated
|
||||||
|
2. Summarizes the changes (added, updated, deleted, and failed workitems)
|
||||||
|
3. If code changes are provided, analyze them and include a summary of the key changes
|
||||||
|
4. Uses markdown formatting for better readability
|
||||||
|
5. Keeps the description concise but informative
|
||||||
|
|
||||||
|
The pull request description should be ready to use without further editing.
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await generativeModel.generateContent(prompt);
|
||||||
|
|
||||||
|
const response = await result.response;
|
||||||
|
const generatedText = response.candidates[0]?.content?.parts[0]?.text || '';
|
||||||
|
|
||||||
|
return generatedText;
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user