initial commit

This commit is contained in:
cghislai 2025-06-08 00:51:51 +02:00
commit f32c78a94b
27 changed files with 7504 additions and 0 deletions

10
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,10 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Environment-dependent path to Maven home directory
/mavenHomeManager.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

6
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_23" default="true" project-jdk-name="temurin-23" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/test-ai-code-agents.iml" filepath="$PROJECT_DIR$/.idea/test-ai-code-agents.iml" />
</modules>
</component>
</project>

9
.idea/test-ai-code-agents.iml generated Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

126
package-lock.json generated Normal file
View File

@ -0,0 +1,126 @@
{
"name": "test-ai-code-agents",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"devDependencies": {
"@types/express": "^5.0.3"
}
},
"node_modules/@types/body-parser": {
"version": "1.19.6",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
"integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/connect": "*",
"@types/node": "*"
}
},
"node_modules/@types/connect": {
"version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/express": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz",
"integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^5.0.0",
"@types/serve-static": "*"
}
},
"node_modules/@types/express-serve-static-core": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz",
"integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
"@types/qs": "*",
"@types/range-parser": "*",
"@types/send": "*"
}
},
"node_modules/@types/http-errors": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
"integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.15.30",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.30.tgz",
"integrity": "sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/range-parser": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/send": {
"version": "0.17.5",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz",
"integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/mime": "^1",
"@types/node": "*"
}
},
"node_modules/@types/serve-static": {
"version": "1.15.8",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz",
"integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/http-errors": "*",
"@types/node": "*",
"@types/send": "*"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
}
}
}

5
package.json Normal file
View File

@ -0,0 +1,5 @@
{
"devDependencies": {
"@types/express": "^5.0.3"
}
}

View File

@ -0,0 +1,27 @@
# Main repository configuration
MAIN_REPO_URL=https://github.com/Ebitda-SRL/test-ai-code-agents.git
# Use either token or username/password for main repo
MAIN_REPO_TOKEN=your_token_here
# OR
MAIN_REPO_USERNAME=your_username_here
MAIN_REPO_PASSWORD=your_password_here
# GitHub credentials
# Use either token or username/password for GitHub
GITHUB_TOKEN=your_github_token_here
# OR
GITHUB_USERNAME=your_github_username_here
GITHUB_PASSWORD=your_github_password_here
# Gitea credentials
GITEA_USERNAME=your_gitea_username_here
GITEA_PASSWORD=your_gitea_password_here
# Google Cloud configuration
GOOGLE_CLOUD_PROJECT_ID=your_gcp_project_id_here
GOOGLE_CLOUD_LOCATION=us-central1
GEMINI_MODEL=gemini-1.5-pro
# Function configuration
# Set to 'true' to enable debug logging
DEBUG=false

View File

@ -0,0 +1,2 @@
node_modules/
dist/

View File

@ -0,0 +1,135 @@
# Prompts to Test Spec
A Google Cloud Function that processes workitem prompts and generates test specifications.
## Overview
This function:
1. Clones the main repository containing prompts
2. Iterates over each project in the prompts/ directory
3. Clones the project repository
4. Uses the Gemini API to apply guidelines from the project's AI.md file
5. Creates a pull request in the project repository with the generated test specifications
## Prerequisites
- Node.js 20 or later
- Google Cloud CLI
- Google Cloud account with access to:
- Cloud Functions
- Vertex AI (for Gemini API)
- Git credentials for the repositories
## Setup
1. Clone this repository
2. Navigate to the function directory:
```
cd src/functions/prompts-to-test-spec
```
3. Install dependencies:
```
npm install
```
4. Create a `.env` file based on the `.env.example` template:
```
cp .env.example .env
```
5. Edit the `.env` file with your credentials and configuration
## Environment Variables
The function requires several environment variables to be set:
### Main Repository Configuration
- `MAIN_REPO_URL`: URL of the main repository containing prompts
- `MAIN_REPO_TOKEN` or `MAIN_REPO_USERNAME`/`MAIN_REPO_PASSWORD`: Credentials for the main repository
### GitHub Credentials
- `GITHUB_TOKEN` or `GITHUB_USERNAME`/`GITHUB_PASSWORD`: Credentials for GitHub repositories
### Gitea Credentials
- `GITEA_USERNAME`/`GITEA_PASSWORD`: Credentials for Gitea repositories
### Google Cloud Configuration
- `GOOGLE_CLOUD_PROJECT_ID`: Your Google Cloud project ID
- `GOOGLE_CLOUD_LOCATION`: Google Cloud region (default: us-central1)
- `GEMINI_MODEL`: Gemini model to use (default: gemini-1.5-pro)
## Local Development
To run the function locally:
1. Build the function:
```
npm run build
```
2. Start the function:
```
npm start
```
3. Test the HTTP endpoint:
```
curl http://localhost:8080
```
## Testing
Run the tests:
```
npm test
```
Run tests in watch mode:
```
npm run test:watch
```
## Deployment
### HTTP Trigger
Deploy the function with an HTTP trigger:
```
npm run deploy
```
### Event Trigger
Deploy the function with a Cloud Storage event trigger:
```
npm run deploy:event
```
Note: You'll need to update the `YOUR_BUCKET_NAME` in the package.json file with your actual Cloud Storage bucket name.
## Architecture
The function is organized into several services:
- **RepositoryService**: Handles Git operations like cloning repositories and creating branches
- **ProjectService**: Finds and processes projects and workitems
- **GeminiService**: Interacts with the Gemini API to generate test specifications
- **PullRequestService**: Creates pull requests in project repositories
- **ProcessorService**: Orchestrates the entire process
## Project Structure
```
src/
├── index.ts # Main entry point
├── types.ts # Type definitions
└── services/ # Service modules
├── repository-service.ts # Git operations
├── project-service.ts # Project and workitem processing
├── gemini-service.ts # Gemini API integration
├── pull-request-service.ts # Pull request creation
├── processor-service.ts # Process orchestration
└── __tests__/ # Unit tests
```
## License
This project is licensed under the MIT License.

View File

@ -0,0 +1,19 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/__tests__/**/*.test.ts'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/**/__tests__/**',
],
coverageDirectory: 'coverage',
transform: {
'^.+\\.tsx?$': ['ts-jest', {
tsconfig: 'tsconfig.json',
}],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
setupFiles: ['dotenv/config'],
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,36 @@
{
"name": "prompts-to-test-spec",
"version": "1.0.0",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"prestart": "npm run build",
"deploy": "gcloud functions deploy promptToTestSpecHttp --gen2 --runtime=nodejs20 --source=. --trigger-http --allow-unauthenticated",
"deploy:event": "gcloud functions deploy promptToTestSpecEvent --gen2 --runtime=nodejs20 --source=. --trigger-event=google.cloud.storage.object.v1.finalized --trigger-resource=YOUR_BUCKET_NAME",
"clean": "rm -rf dist",
"test": "jest",
"test:watch": "jest --watch"
},
"main": "dist/index.js",
"dependencies": {
"@google-cloud/functions-framework": "^3.0.0",
"@google-cloud/vertexai": "^0.5.0",
"axios": "^1.6.7",
"dotenv": "^16.4.5",
"simple-git": "^3.23.0"
},
"devDependencies": {
"@types/express": "^5.0.3",
"@types/jest": "^29.5.12",
"@types/node": "^20.11.30",
"jest": "^29.7.0",
"ts-jest": "^29.1.2",
"typescript": "^5.8.3"
},
"engines": {
"node": ">=20"
},
"files": [
"dist"
]
}

View File

@ -0,0 +1,101 @@
/**
* Configuration module for loading environment variables
*/
import * as dotenv from 'dotenv';
import * as path from 'path';
// Load environment variables from .env file
dotenv.config({ path: path.resolve(__dirname, '../.env') });
// Main repository configuration
export const MAIN_REPO_URL = process.env.MAIN_REPO_URL || '';
export const MAIN_REPO_TOKEN = process.env.MAIN_REPO_TOKEN;
export const MAIN_REPO_USERNAME = process.env.MAIN_REPO_USERNAME;
export const MAIN_REPO_PASSWORD = process.env.MAIN_REPO_PASSWORD;
// GitHub credentials
export const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
export const GITHUB_USERNAME = process.env.GITHUB_USERNAME;
export const GITHUB_PASSWORD = process.env.GITHUB_PASSWORD;
// Gitea credentials
export const GITEA_USERNAME = process.env.GITEA_USERNAME;
export const GITEA_PASSWORD = process.env.GITEA_PASSWORD;
// Google Cloud configuration
export const GOOGLE_CLOUD_PROJECT_ID = process.env.GOOGLE_CLOUD_PROJECT_ID || '';
export const GOOGLE_CLOUD_LOCATION = process.env.GOOGLE_CLOUD_LOCATION || 'us-central1';
export const GEMINI_MODEL = process.env.GEMINI_MODEL || 'gemini-1.5-pro';
// Function configuration
export const DEBUG = process.env.DEBUG === 'true';
// Validate required configuration
export function validateConfig(): void {
const missingVars: string[] = [];
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 (!GOOGLE_CLOUD_PROJECT_ID) {
missingVars.push('GOOGLE_CLOUD_PROJECT_ID');
}
if (missingVars.length > 0) {
throw new Error(`Missing required environment variables: ${missingVars.join(', ')}`);
}
}
// Get repository credentials for the main repository
export function getMainRepoCredentials(): { type: 'username-password' | 'token'; username?: string; password?: string; token?: string } {
if (MAIN_REPO_TOKEN) {
return {
type: 'token',
token: MAIN_REPO_TOKEN
};
} else if (MAIN_REPO_USERNAME && MAIN_REPO_PASSWORD) {
return {
type: 'username-password',
username: MAIN_REPO_USERNAME,
password: MAIN_REPO_PASSWORD
};
}
throw new Error('No credentials available for the main repository');
}
// Get GitHub credentials
export function getGithubCredentials(): { type: 'username-password' | 'token'; username?: string; password?: string; token?: string } | undefined {
if (GITHUB_TOKEN) {
return {
type: 'token',
token: GITHUB_TOKEN
};
} else if (GITHUB_USERNAME && GITHUB_PASSWORD) {
return {
type: 'username-password',
username: GITHUB_USERNAME,
password: GITHUB_PASSWORD
};
}
return undefined;
}
// Get Gitea credentials
export function getGiteaCredentials(): { type: 'username-password'; username: string; password: string } | undefined {
if (GITEA_USERNAME && GITEA_PASSWORD) {
return {
type: 'username-password',
username: GITEA_USERNAME,
password: GITEA_PASSWORD
};
}
return undefined;
}

View File

@ -0,0 +1,47 @@
import {CloudEvent, cloudEvent, http} from '@google-cloud/functions-framework';
import { ProcessorService } from './services/processor-service';
import { validateConfig } from './config';
// Validate configuration on startup
try {
validateConfig();
} catch (error) {
console.error('Configuration error:', error.message);
// Don't throw here to allow the function to start, but it will fail when executed
}
/**
* HTTP endpoint for the prompts-to-test-spec function
*/
http('promptToTestSpecHttp', async (req, res): Promise<void> => {
try {
const processor = new ProcessorService();
const results = await processor.processProjects();
res.status(200).json({
success: true,
results
});
} catch (error) {
console.error('Error processing projects:', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : String(error)
});
}
});
/**
* Cloud Event handler for the prompts-to-test-spec function
*/
cloudEvent('promptToTestSpecEvent', async (event: CloudEvent<any>): Promise<void> => {
try {
console.log('Received event:', event.type);
const processor = new ProcessorService();
await processor.processProjects();
console.log('Processing completed successfully');
} catch (error) {
console.error('Error processing projects:', error);
throw error;
}
});

View File

@ -0,0 +1,206 @@
import * as fs from 'fs';
import * as path from 'path';
import { ProjectService } from '../project-service';
// Mock fs and path modules
jest.mock('fs');
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
(fs.readdirSync as jest.Mock).mockReturnValueOnce([
{ name: 'project1', isDirectory: () => true },
{ name: 'project2', isDirectory: () => true },
{ 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 {
name: projectName,
path: projectPath,
repoHost: 'https://github.com',
repoUrl: `https://github.com/org/${projectName}.git`,
jiraComponent: projectName
};
});
const projects = await projectService.findProjects('prompts');
expect(projects).toHaveLength(2);
expect(projects[0].name).toBe('project1');
expect(projects[1].name).toBe('project2');
expect(fs.readdirSync).toHaveBeenCalledWith('prompts', { withFileTypes: true });
expect(fs.existsSync).toHaveBeenCalledWith('prompts/project1/INFO.md');
expect(fs.existsSync).toHaveBeenCalledWith('prompts/project2/INFO.md');
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',
repoHost: 'https://github.com',
repoUrl: 'https://github.com/org/project.git',
jiraComponent: 'project-component'
});
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 {
name: fileName.replace('.md', ''),
path: workitemPath,
title: `Workitem ${fileName.replace('.md', '')}`,
description: 'Description',
jiraReference: 'JIRA-123',
implementation: '',
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.
- [x] Jira: JIRA-123
- [ ] 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',
title: 'Workitem Title',
description: 'This is a description of the workitem.\nIt has multiple lines.',
jiraReference: 'JIRA-123',
implementation: '',
isActive: true
});
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();
});
});
});

View File

@ -0,0 +1,155 @@
/**
* Service for handling Gemini API operations
*/
import { VertexAI } from '@google-cloud/vertexai';
import * as fs from 'fs';
import * as path from 'path';
import { Project, Workitem } from '../types';
import { GOOGLE_CLOUD_PROJECT_ID, GOOGLE_CLOUD_LOCATION, GEMINI_MODEL } from '../config';
export class GeminiService {
private vertexAI: VertexAI;
private model: string;
private projectId: string;
private location: string;
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');
}
this.vertexAI = new VertexAI({
project: this.projectId,
location: this.location,
});
}
/**
* Apply project guidelines to a workitem
* @param project Project information
* @param workitem Workitem to process
* @param projectRepoPath Path to the cloned project repository
* @returns Result of the processing
*/
async processWorkitem(
project: Project,
workitem: Workitem,
projectRepoPath: string
): Promise<{ success: boolean; error?: string }> {
try {
// Skip inactive workitems
if (!workitem.isActive) {
console.log(`Skipping inactive workitem: ${workitem.name}`);
// If the feature file exists, it should be deleted according to guidelines
const featureFileName = `${workitem.name}.feature`;
const featurePath = path.join(projectRepoPath, 'nitro-it', 'src', 'test', 'resources', 'workitems', featureFileName);
if (fs.existsSync(featurePath)) {
fs.unlinkSync(featurePath);
console.log(`Deleted feature file for inactive workitem: ${featurePath}`);
}
return { success: true };
}
// Read project guidelines
const projectGuidelines = await this.readProjectGuidelines(project.path);
// Read workitem content
const workitemContent = fs.readFileSync(workitem.path, 'utf-8');
// Generate feature file content using Gemini API
const featureContent = await this.generateFeatureFile(
projectGuidelines,
workitemContent,
workitem.name
);
// Ensure the target directory exists
const targetDir = path.join(projectRepoPath, 'nitro-it', 'src', 'test', 'resources', 'workitems');
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
// Write the feature file
const featureFileName = `${workitem.name}.feature`;
const featurePath = path.join(targetDir, featureFileName);
fs.writeFileSync(featurePath, featureContent);
console.log(`Created/updated feature file: ${featurePath}`);
return { success: true };
} catch (error) {
console.error(`Error processing workitem ${workitem.name}:`, error);
return {
success: false,
error: error instanceof Error ? error.message : String(error)
};
}
}
/**
* Read AI guidelines for a project
* @param projectPath Path to the project directory
* @returns AI guidelines content
*/
private async readProjectGuidelines(projectPath: string): Promise<string> {
const aiPath = path.join(projectPath, 'AI.md');
if (!fs.existsSync(aiPath)) {
return '';
}
return fs.readFileSync(aiPath, 'utf-8');
}
/**
* Generate feature file content using Gemini API
* @param guidelines Project guidelines
* @param workitemContent Workitem content
* @param workitemName Name of the workitem
* @returns Generated feature file content
*/
private async generateFeatureFile(
guidelines: string,
workitemContent: string,
workitemName: string
): Promise<string> {
const generativeModel = this.vertexAI.getGenerativeModel({
model: this.model,
});
const currentDate = new Date().toISOString();
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:
# 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({
contents: [{ role: 'user', parts: [{ text: prompt }] }],
});
const response = await result.response;
const generatedText = response.candidates[0].content.parts[0].text;
return generatedText;
}
}

View File

@ -0,0 +1,214 @@
/**
* Service for orchestrating the entire process
*/
import * as path from 'path';
import { Project, ProcessResult, RepoCredentials } from '../types';
import { RepositoryService } from './repository-service';
import { ProjectService } from './project-service';
import { GeminiService } from './gemini-service';
import { PullRequestService } from './pull-request-service';
import {
MAIN_REPO_URL,
validateConfig,
getMainRepoCredentials,
getGithubCredentials,
getGiteaCredentials,
GOOGLE_CLOUD_PROJECT_ID,
GOOGLE_CLOUD_LOCATION,
GEMINI_MODEL
} from '../config';
export class ProcessorService {
private repositoryService: RepositoryService;
private projectService: ProjectService;
private geminiService: GeminiService;
private pullRequestService: PullRequestService;
private mainRepoUrl: string;
private mainRepoCredentials: RepoCredentials;
private giteaCredentials?: RepoCredentials;
private githubCredentials?: RepoCredentials;
constructor() {
// Validate configuration
validateConfig();
// Initialize services
this.repositoryService = new RepositoryService();
this.projectService = new ProjectService();
this.geminiService = new GeminiService(
GOOGLE_CLOUD_PROJECT_ID,
GOOGLE_CLOUD_LOCATION,
GEMINI_MODEL
);
this.pullRequestService = new PullRequestService();
// Get main repository URL and credentials
this.mainRepoUrl = MAIN_REPO_URL;
this.mainRepoCredentials = getMainRepoCredentials();
// Initialize other credentials
this.githubCredentials = getGithubCredentials();
this.giteaCredentials = getGiteaCredentials();
}
/**
* Get credentials for a project based on its repository host
* @param project Project information
* @returns Credentials for the project repository
*/
private getCredentialsForProject(project: Project): RepoCredentials {
if (!project.repoHost) {
throw new Error(`Repository host not found for project ${project.name}`);
}
if (project.repoHost.includes('github.com')) {
if (!this.githubCredentials) {
throw new Error('GitHub credentials not found');
}
return this.githubCredentials;
} else if (project.repoHost.includes('gitea')) {
if (!this.giteaCredentials) {
throw new Error('Gitea credentials not found');
}
return this.giteaCredentials;
} else {
throw new Error(`Unsupported repository host: ${project.repoHost}`);
}
}
/**
* Process all projects in the main repository
* @returns Array of process results
*/
async processProjects(): Promise<ProcessResult[]> {
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
);
// Find all projects in the prompts directory
const promptsDir = path.join(mainRepoPath, 'src', 'prompts');
console.log(`Finding projects in: ${promptsDir}`);
const projects = await this.projectService.findProjects(promptsDir);
console.log(`Found ${projects.length} projects`);
// Process each project
for (const project of projects) {
try {
const result = await this.processProject(project);
results.push(result);
} catch (error) {
console.error(`Error processing project ${project.name}:`, error);
results.push({
project,
processedWorkitems: [],
error: error instanceof Error ? error.message : String(error)
});
}
}
return results;
} catch (error) {
console.error('Error processing projects:', error);
throw error;
}
}
/**
* Process a single project
* @param project Project information
* @returns Process result
*/
async processProject(project: Project): Promise<ProcessResult> {
console.log(`Processing project: ${project.name}`);
// Find all workitems in the project
const workitems = await this.projectService.findWorkitems(project.path);
console.log(`Found ${workitems.length} workitems in project ${project.name}`);
// Skip if no workitems found
if (workitems.length === 0) {
return {
project,
processedWorkitems: []
};
}
// Skip if no repository URL
if (!project.repoUrl) {
console.log(`Skipping project ${project.name}: No repository URL found`);
return {
project,
processedWorkitems: []
};
}
try {
// Get credentials for the project
const credentials = this.getCredentialsForProject(project);
// Clone the project repository
console.log(`Cloning project repository: ${project.repoUrl}`);
const projectRepoPath = await this.repositoryService.cloneProjectRepository(project, credentials);
// Create a new branch for changes
const branchName = `update-workitems-${new Date().toISOString().split('T')[0]}`;
await this.repositoryService.createBranch(projectRepoPath, branchName);
// Process each workitem
const processedWorkitems = [];
for (const workitem of workitems) {
console.log(`Processing workitem: ${workitem.name}`);
const result = await this.geminiService.processWorkitem(project, workitem, projectRepoPath);
processedWorkitems.push({ workitem, ...result });
}
// If no changes were made, return early
if (processedWorkitems.length === 0) {
console.log(`No workitems processed for project ${project.name}`);
return {
project,
processedWorkitems: []
};
}
// Commit changes
await this.repositoryService.commitChanges(
projectRepoPath,
`Update workitems: ${new Date().toISOString().split('T')[0]}`
);
// Push changes
await this.repositoryService.pushChanges(projectRepoPath, branchName, credentials);
// Create pull request
const pullRequestUrl = await this.pullRequestService.createPullRequest(
project,
branchName,
processedWorkitems,
credentials
);
console.log(`Created pull request: ${pullRequestUrl}`);
return {
project,
processedWorkitems,
pullRequestUrl
};
} catch (error) {
console.error(`Error processing project ${project.name}:`, error);
return {
project,
processedWorkitems: [],
error: error instanceof Error ? error.message : String(error)
};
}
}
}

View File

@ -0,0 +1,151 @@
/**
* Service for handling project operations
*/
import * as fs from 'fs';
import * as path from 'path';
import { Project, Workitem } from '../types';
export class ProjectService {
/**
* Find all projects in the prompts directory
* @param promptsDir Path to the prompts directory
* @returns Array of projects
*/
async findProjects(promptsDir: string): Promise<Project[]> {
const projects: Project[] = [];
// Get all directories in the prompts directory
const entries = fs.readdirSync(promptsDir, { withFileTypes: true });
const projectDirs = entries.filter(entry => entry.isDirectory());
for (const dir of projectDirs) {
const projectPath = path.join(promptsDir, dir.name);
const infoPath = path.join(projectPath, 'INFO.md');
// Skip directories without INFO.md
if (!fs.existsSync(infoPath)) {
continue;
}
// Read project info
const project = await this.readProjectInfo(projectPath, dir.name);
projects.push(project);
}
return projects;
}
/**
* Read project information from INFO.md
* @param projectPath Path to the project directory
* @param projectName Name of the project
* @returns Project information
*/
async readProjectInfo(projectPath: string, projectName: string): Promise<Project> {
const infoPath = path.join(projectPath, 'INFO.md');
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 {
name: projectName,
path: projectPath,
repoHost: repoHostMatch ? repoHostMatch[1].trim() : undefined,
repoUrl: repoUrlMatch ? repoUrlMatch[1].trim() : undefined,
jiraComponent: jiraComponentMatch ? jiraComponentMatch[1].trim() : undefined
};
}
/**
* Find all workitems in a project
* @param projectPath Path to the project directory
* @returns Array of workitems
*/
async findWorkitems(projectPath: string): Promise<Workitem[]> {
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;
}
/**
* Read workitem information from a markdown file
* @param workitemPath Path to the workitem file
* @param fileName Name of the workitem file
* @returns Workitem information
*/
async readWorkitemInfo(workitemPath: string, fileName: string): Promise<Workitem> {
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;
} else if (titleIndex !== -1 && metadataIndex === -1 && lines[i].startsWith('- [')) {
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,
title: titleMatch ? titleMatch[1].trim() : fileName,
description,
jiraReference: jiraMatch ? jiraMatch[1].trim() : undefined,
implementation: implementationMatch ? implementationMatch[1].trim() : undefined,
isActive
};
}
/**
* Read AI guidelines for a project
* @param projectPath Path to the project directory
* @returns AI guidelines content
*/
async readProjectGuidelines(projectPath: string): Promise<string> {
const aiPath = path.join(projectPath, 'AI.md');
if (!fs.existsSync(aiPath)) {
return '';
}
return fs.readFileSync(aiPath, 'utf-8');
}
}

View File

@ -0,0 +1,194 @@
/**
* Service for handling pull request operations
*/
import axios from 'axios';
import * as path from 'path';
import { Project, RepoCredentials, Workitem } from '../types';
export class PullRequestService {
/**
* Create a pull request for changes in a repository
* @param project Project information
* @param branchName Name of the branch with changes
* @param processedWorkitems List of processed workitems
* @param credentials Repository credentials
* @returns URL of the created pull request
*/
async createPullRequest(
project: Project,
branchName: string,
processedWorkitems: { workitem: Workitem; success: boolean; error?: string }[],
credentials: RepoCredentials
): Promise<string> {
if (!project.repoHost || !project.repoUrl) {
throw new Error(`Repository information not found for project ${project.name}`);
}
// Generate PR title and description
const title = `Update workitems: ${new Date().toISOString().split('T')[0]}`;
const description = this.generatePullRequestDescription(processedWorkitems);
// Determine the repository host type and create PR accordingly
if (project.repoHost.includes('github.com')) {
return this.createGithubPullRequest(project, branchName, title, description, credentials);
} else if (project.repoHost.includes('gitea')) {
return this.createGiteaPullRequest(project, branchName, title, description, credentials);
} else {
throw new Error(`Unsupported repository host: ${project.repoHost}`);
}
}
/**
* Create a pull request on GitHub
* @param project Project information
* @param branchName Name of the branch with changes
* @param title Pull request title
* @param description Pull request description
* @param credentials Repository credentials
* @returns URL of the created pull request
*/
private async createGithubPullRequest(
project: Project,
branchName: string,
title: string,
description: string,
credentials: RepoCredentials
): Promise<string> {
// Extract owner and repo from the repository URL
const repoUrlParts = project.repoUrl!.split('/');
const repo = path.basename(repoUrlParts[repoUrlParts.length - 1], '.git');
const owner = repoUrlParts[repoUrlParts.length - 2];
// Create the pull request
const apiUrl = `https://api.github.com/repos/${owner}/${repo}/pulls`;
const headers: Record<string, string> = {
'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) {
const auth = Buffer.from(`${credentials.username}:${credentials.password}`).toString('base64');
headers['Authorization'] = `Basic ${auth}`;
} else {
throw new Error('Invalid credentials for GitHub');
}
const response = await axios.post(
apiUrl,
{
title,
body: description,
head: branchName,
base: 'main', // Assuming the default branch is 'main'
},
{ headers }
);
return response.data.html_url;
}
/**
* Create a pull request on Gitea
* @param project Project information
* @param branchName Name of the branch with changes
* @param title Pull request title
* @param description Pull request description
* @param credentials Repository credentials
* @returns URL of the created pull request
*/
private async createGiteaPullRequest(
project: Project,
branchName: string,
title: string,
description: string,
credentials: RepoCredentials
): Promise<string> {
// Extract owner and repo from the repository URL
const repoUrlParts = project.repoUrl!.split('/');
const repo = path.basename(repoUrlParts[repoUrlParts.length - 1], '.git');
const owner = repoUrlParts[repoUrlParts.length - 2];
// Create the pull request
const apiUrl = `${project.repoHost}/api/v1/repos/${owner}/${repo}/pulls`;
const headers: Record<string, string> = {
'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) {
const auth = Buffer.from(`${credentials.username}:${credentials.password}`).toString('base64');
headers['Authorization'] = `Basic ${auth}`;
} else {
throw new Error('Invalid credentials for Gitea');
}
const response = await axios.post(
apiUrl,
{
title,
body: description,
head: branchName,
base: 'main', // Assuming the default branch is 'main'
},
{ headers }
);
return response.data.html_url;
}
/**
* Generate a description for the pull request
* @param processedWorkitems List of processed workitems
* @returns Pull request description
*/
private 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;
}
}

View File

@ -0,0 +1,131 @@
/**
* Service for handling repository operations
*/
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { simpleGit, SimpleGit } from 'simple-git';
import { Project, RepoCredentials } from '../types';
export class RepositoryService {
private baseDir: string;
constructor(baseDir?: string) {
this.baseDir = baseDir || path.join(os.tmpdir(), 'prompts-to-test-spec');
// Ensure base directory exists
if (!fs.existsSync(this.baseDir)) {
fs.mkdirSync(this.baseDir, { recursive: true });
}
}
/**
* Clone the main repository containing prompts
* @param repoUrl URL of the repository
* @param credentials Optional credentials for private repositories
* @returns Path to the cloned repository
*/
async cloneMainRepository(repoUrl: string, credentials?: RepoCredentials): Promise<string> {
const repoDir = path.join(this.baseDir, 'main-repo');
// Clean up existing directory if it exists
if (fs.existsSync(repoDir)) {
fs.rmSync(repoDir, { recursive: true, force: true });
}
fs.mkdirSync(repoDir, { recursive: true });
// Configure git with credentials if provided
const git = this.configureGit(repoDir, credentials);
// Clone the repository
await git.clone(repoUrl, repoDir);
return repoDir;
}
/**
* Clone a project repository
* @param project Project information
* @param credentials Optional credentials for private repositories
* @returns Path to the cloned repository
*/
async cloneProjectRepository(project: Project, credentials?: RepoCredentials): Promise<string> {
if (!project.repoUrl) {
throw new Error(`Repository URL not found for project ${project.name}`);
}
const projectRepoDir = path.join(this.baseDir, `project-${project.name}`);
// Clean up existing directory if it exists
if (fs.existsSync(projectRepoDir)) {
fs.rmSync(projectRepoDir, { recursive: true, force: true });
}
fs.mkdirSync(projectRepoDir, { recursive: true });
// Configure git with credentials if provided
const git = this.configureGit(projectRepoDir, credentials);
// Clone the repository
await git.clone(project.repoUrl, projectRepoDir);
return projectRepoDir;
}
/**
* Create a new branch in a repository
* @param repoDir Path to the repository
* @param branchName Name of the branch to create
*/
async createBranch(repoDir: string, branchName: string): Promise<void> {
const git = simpleGit(repoDir);
await git.checkoutLocalBranch(branchName);
}
/**
* Commit changes to a repository
* @param repoDir Path to the repository
* @param message Commit message
*/
async commitChanges(repoDir: string, message: string): Promise<void> {
const git = simpleGit(repoDir);
await git.add('.');
await git.commit(message);
}
/**
* Push changes to a repository
* @param repoDir Path to the repository
* @param branchName Name of the branch to push
* @param credentials Optional credentials for private repositories
*/
async pushChanges(repoDir: string, branchName: string, credentials?: RepoCredentials): Promise<void> {
const git = this.configureGit(repoDir, credentials);
await git.push('origin', branchName, ['--set-upstream']);
}
/**
* Configure git with credentials
* @param repoDir Path to the repository
* @param credentials Credentials for authentication
* @returns Configured SimpleGit instance
*/
private configureGit(repoDir: string, credentials?: RepoCredentials): SimpleGit {
const git = simpleGit(repoDir);
if (credentials) {
if (credentials.type === 'username-password' && credentials.username && credentials.password) {
// For HTTPS URLs with username/password
const credentialHelper = `!f() { echo "username=${credentials.username}"; echo "password=${credentials.password}"; }; f`;
git.addConfig('credential.helper', credentialHelper, false, 'global');
} else if (credentials.type === 'token' && credentials.token) {
// For HTTPS URLs with token
const credentialHelper = `!f() { echo "password=${credentials.token}"; }; f`;
git.addConfig('credential.helper', credentialHelper, false, 'global');
}
}
return git;
}
}

View File

@ -0,0 +1,38 @@
/**
* Type definitions for the prompts-to-test-spec function
*/
export interface Project {
name: string;
path: string;
repoHost?: string;
repoUrl?: string;
jiraComponent?: string;
}
export interface Workitem {
name: string;
path: string;
title: string;
description: string;
jiraReference?: string;
implementation?: string;
isActive: boolean;
}
export interface RepoCredentials {
type: 'username-password' | 'token';
username?: string;
password?: string;
token?: string;
}
export interface ProcessResult {
project: Project;
processedWorkitems: {
workitem: Workitem;
success: boolean;
error?: string;
}[];
pullRequestUrl?: string;
}

View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
]
}

66
src/prompts/AI.md Normal file
View File

@ -0,0 +1,66 @@
This file describes the AI guidelines for operations in this directory.
## Directory structure
- <project>/: A single project repository
- INFO.md: Project information, including where the code is hosted
- AI.md: AI guidelines for the project
- workitems/: A directory containing workitem prompts
- <date>-<workitem-name>.md: A prompt file for a project workitem
### File format
File format is markdown.
It contains checkboxes, that must only be checked if the information is available and provided.
#### Project info file format
A project info file follows the following format:
```markdown
## <Project name>
- [ ] Repo host: <repo host url, eg https://gitea.fteamdev.valuya.be/ or https://github.com/organizations/Ebitda-SRL>
- [ ] Repo url: <url of the project repository>
- [ ] Jira component: <component of the jira>
```
#### Work item prompt file format
A work item prompt file follows the following format:
```markdown
## <workitem name>
<short paragraphs describing the workitem, with line wrapping>
- [ ] Jira: <reference of the jira ticket with a link>
- [ ] Implementation: <reference of the implementation within the project repo, optionally with a link>
- [ ] Active
```
The active checkbox is optional and should be checked if the workitem is active. Inactive workitems should be ignored.
In the absence of the active checkbox, the workitem is assumed to be active.
### Credentials
This section describes credentials to use when interacting with various apis and services.
The actual credentials are provided in the environment variables.
#### Jira
#### Github
- [ ] host: https://github.com/organizations/Ebitda-SRL
#### Gitea
- [x] host: https://gitea.fteamdev.valuya.be
- credential type: username/password
- username variable: GITEA_USERNAME
- password variable: GITEA_PASSWORD

View File

@ -0,0 +1,12 @@
## Workitem implementation
- Implement the workitem as a cucumber feature spec file in the `nitro-it/src/test/resources/workitems/` folder.
- Use the workitem prompt file name as the feature file name.
- Add comments in the feature file indicating
- The date/time/execution info of the job that created the work item
- The work item prompt file in this directory
- Inactive work items should have their feature file deleted.
- Updates should be committed to a new branch and a pull request should be created.s
- The pull request should include a short description of the modified code
- The pull request description should include the list of work items that were added/updated/deleted

View File

@ -0,0 +1,7 @@
# Nitro-back
Nitro backend server in quarkus
- [x] Repo host: https://gitea.fteamdev.valuya.be/
- [x] Repo url: https://gitea.fteamdev.valuya.be/fiscalteam/nitro-back.git
- [x] Jira component: nitro

View File

@ -0,0 +1,9 @@
## Test
This is a test workitem.
The nitro-back backend should have a /test endpoint implemented returning the json object: {"test": "Hellow"}.
- [ ] Jira:
- [ ] Implementation:
- [ ] Active