├── vscode ├── src │ ├── getNonce.ts │ ├── extension.ts │ ├── utils.ts │ └── TestGPTProvider.ts ├── webview-ui │ ├── src │ │ ├── global.d.ts │ │ ├── main.ts │ │ ├── types.ts │ │ ├── lib │ │ │ ├── Dropdown.svelte │ │ │ ├── RemoveButton.svelte │ │ │ ├── TextArea.svelte │ │ │ ├── utils.ts │ │ │ └── Logo.svelte │ │ ├── utilities │ │ │ ├── vscode.js.map │ │ │ ├── vscode.ts │ │ │ └── vscode.js │ │ ├── App.svelte │ │ └── Advanced.svelte │ ├── .gitignore │ ├── tsconfig.json │ ├── README.md │ ├── package.json │ └── rollup.config.js ├── media │ ├── logo.png │ └── icon.svg ├── tsconfig.json ├── .prettierrc ├── .prettierignore ├── .eslintrc.json ├── .vscodeignore ├── LICENSE ├── package.json └── resources │ └── default.yaml ├── .gitignore ├── show.gif ├── media ├── logo.png ├── show.gif └── icon.svg ├── src ├── const.ts ├── index.ts ├── __tests__ │ ├── const.test.ts │ ├── types.test.ts │ ├── index.test.ts │ ├── command.test.ts │ ├── autoTest.test.ts │ └── utils.test.ts ├── types.ts ├── command.ts └── utils.ts ├── vitest.config.ts ├── tsconfig.json ├── LICENSE ├── package.json ├── testgpt.config.yaml └── README.md /vscode/src/getNonce.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bin 3 | .vscode 4 | vscode/out -------------------------------------------------------------------------------- /vscode/webview-ui/src/global.d.ts: -------------------------------------------------------------------------------- 1 | /// -------------------------------------------------------------------------------- /show.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fayez-nazzal/TestGPT/HEAD/show.gif -------------------------------------------------------------------------------- /media/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fayez-nazzal/TestGPT/HEAD/media/logo.png -------------------------------------------------------------------------------- /media/show.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fayez-nazzal/TestGPT/HEAD/media/show.gif -------------------------------------------------------------------------------- /vscode/media/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fayez-nazzal/TestGPT/HEAD/vscode/media/logo.png -------------------------------------------------------------------------------- /src/const.ts: -------------------------------------------------------------------------------- 1 | export const CONFIG_FILE_NAME = "testgpt.config.yaml"; 2 | export const DEFAULT_MODEL = "gpt-3.5-turbo-16k"; 3 | -------------------------------------------------------------------------------- /vscode/webview-ui/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /public/build/ 3 | dist 4 | dist-ssr 5 | build 6 | build-ssr 7 | 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { parseCommand, executeCommand } from "./command"; 3 | 4 | const options = parseCommand(); 5 | 6 | executeCommand(options); 7 | -------------------------------------------------------------------------------- /vscode/webview-ui/src/main.ts: -------------------------------------------------------------------------------- 1 | import App from "./App.svelte"; 2 | 3 | const app = new (App as any)({ 4 | target: document.body, 5 | }); 6 | 7 | export default app; 8 | -------------------------------------------------------------------------------- /vscode/webview-ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | "include": ["src/**/*"], 4 | "exclude": ["node_modules/*", "__sapper__/*", "public/*"], 5 | "compilerOptions": { 6 | "noImplicitAny": false 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /vscode/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": ["es6", "dom"], 7 | "sourceMap": true, 8 | "rootDir": "src", 9 | "strict": true 10 | }, 11 | "exclude": ["node_modules", ".vscode-test", "webview-ui"] 12 | } 13 | -------------------------------------------------------------------------------- /vscode/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 110, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": false, 7 | "quoteProps": "consistent", 8 | "jsxSingleQuote": false, 9 | "trailingComma": "es5", 10 | "bracketSpacing": true, 11 | "jsxBracketSameLine": true, 12 | "arrowParens": "always" 13 | } 14 | -------------------------------------------------------------------------------- /vscode/.prettierignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | /dist 12 | 13 | # misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | environment: 'node', 6 | include: [ 7 | 'src/**/*.test.ts', 8 | 'src/**/__tests__/**/*.test.ts' 9 | ], 10 | coverage: { 11 | reporter: ['text', 'html'], 12 | } 13 | } 14 | }) 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/__tests__/const.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest' 2 | import { CONFIG_FILE_NAME, DEFAULT_MODEL } from '../const' 3 | 4 | describe('const', () => { 5 | it('exposes constants', () => { 6 | expect(CONFIG_FILE_NAME).toBe('testgpt.config.yaml') 7 | expect(DEFAULT_MODEL).toBe('gpt-3.5-turbo-16k') 8 | }) 9 | }) 10 | 11 | 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "Node", 5 | "esModuleInterop": true, 6 | "target": "ES6", 7 | "sourceMap": true, 8 | "outDir": "bin", 9 | "strictNullChecks": true, 10 | }, 11 | "include": ["src/**/*"], 12 | "exclude": ["node_modules", "bin", "src/**/*.test.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /vscode/src/extension.ts: -------------------------------------------------------------------------------- 1 | import { window, ExtensionContext } from "vscode"; 2 | import { TestGPTWebviewProvider } from "./TestGPTProvider"; 3 | 4 | export const activate = async (context: ExtensionContext) => { 5 | const provider = new TestGPTWebviewProvider(context.extensionUri, context); 6 | 7 | context.subscriptions.push(window.registerWebviewViewProvider(TestGPTWebviewProvider.viewType, provider)); 8 | }; 9 | -------------------------------------------------------------------------------- /vscode/webview-ui/README.md: -------------------------------------------------------------------------------- 1 | # `webview-ui` Directory 2 | 3 | This directory contains all of the code that will be executed within the webview context. It can be thought of as the place where all the "frontend" code of a webview is contained. 4 | 5 | Types of content that can be contained here: 6 | 7 | - Frontend framework code (i.e. Svelte, Vue, SolidJS, React, etc.) 8 | - JavaScript files 9 | - CSS files 10 | - Assets / resources (i.e. images, illustrations, etc.) 11 | -------------------------------------------------------------------------------- /vscode/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": ["@typescript-eslint"], 9 | "rules": { 10 | "@typescript-eslint/naming-convention": "warn", 11 | "@typescript-eslint/semi": "warn", 12 | "curly": "warn", 13 | "eqeqeq": "warn", 14 | "no-throw-literal": "warn", 15 | "semi": "off" 16 | }, 17 | "ignorePatterns": ["webview-ui/**"] 18 | } 19 | -------------------------------------------------------------------------------- /vscode/webview-ui/src/types.ts: -------------------------------------------------------------------------------- 1 | export interface IPreset { 2 | name: string; 3 | config: { 4 | model: string; 5 | streaming: boolean; 6 | systemMessage: string; 7 | promptTemplate: string; 8 | instructions: string; 9 | autoTechs?: boolean; 10 | techs?: string[]; 11 | examples?: { 12 | fileName: string; 13 | code: string; 14 | tests: string; 15 | }[]; 16 | }; 17 | } 18 | 19 | export interface IVscodeState { 20 | presets: IPreset[]; 21 | activePreset: IPreset; 22 | advanced: boolean; 23 | } 24 | -------------------------------------------------------------------------------- /vscode/webview-ui/src/lib/Dropdown.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | {#each options as option} 16 | {option} 17 | {/each} 18 | 19 | -------------------------------------------------------------------------------- /vscode/webview-ui/src/lib/RemoveButton.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | 17 |
18 | -------------------------------------------------------------------------------- /vscode/webview-ui/src/lib/TextArea.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | {label} 18 | 19 | -------------------------------------------------------------------------------- /src/__tests__/types.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest' 2 | import { examplesSchema, ERole } from '../types' 3 | 4 | describe('types', () => { 5 | it('examplesSchema describes an array of examples', () => { 6 | expect(examplesSchema.type).toBe('array') 7 | expect(examplesSchema.items?.type).toBe('object') 8 | expect((examplesSchema.items as any).required).toContain('fileName') 9 | }) 10 | 11 | it('ERole enum has expected values', () => { 12 | expect(ERole.User).toBe('user') 13 | expect(ERole.System).toBe('system') 14 | expect(ERole.Assistant).toBe('assistant') 15 | }) 16 | }) 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi } from 'vitest' 2 | 3 | vi.mock('../command', () => { 4 | return { 5 | parseCommand: vi.fn(() => ({ mocked: true } as any)), 6 | executeCommand: vi.fn(() => undefined) 7 | } 8 | }) 9 | 10 | describe('index entry', () => { 11 | it('imports and calls executeCommand', async () => { 12 | const mockExit = vi.spyOn(process, 'exit').mockImplementation(((code?: number) => { throw new Error(`exit:${code}`) }) as any) 13 | let failed = false 14 | try { 15 | await import('../index') 16 | } catch (e) { 17 | failed = true 18 | } 19 | expect(failed).toBe(false) 20 | mockExit.mockRestore() 21 | }) 22 | }) 23 | 24 | 25 | -------------------------------------------------------------------------------- /vscode/.vscodeignore: -------------------------------------------------------------------------------- 1 | # This file contains all the files/directories that should 2 | # be ignored (i.e. not included) in the final packaged extension. 3 | 4 | # Ignore extension configs 5 | .vscode/** 6 | 7 | # Ignore test files 8 | .vscode-test/** 9 | out/test/** 10 | 11 | # Ignore source code 12 | src/** 13 | 14 | # Ignore all webview-ui files except the build directory 15 | webview-ui/src/** 16 | webview-ui/scripts/** 17 | webview-ui/index.html 18 | webview-ui/README.md 19 | webview-ui/package.json 20 | webview-ui/package-lock.json 21 | webview-ui/node_modules/** 22 | 23 | # Ignore Misc 24 | .yarnrc 25 | vsc-extension-quickstart.md 26 | **/.gitignore 27 | **/tsconfig.json 28 | **/vite.config.ts 29 | **/.eslintrc.json 30 | **/*.map 31 | **/*.ts 32 | -------------------------------------------------------------------------------- /vscode/webview-ui/src/utilities/vscode.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"vscode.js","sourceRoot":"","sources":["vscode.ts"],"names":[],"mappings":";;;AAEA;;;;;;;;GAQG;AACH,MAAM,gBAAgB;IAGpB;QACE,2EAA2E;QAC3E,2DAA2D;QAC3D,IAAI,OAAO,gBAAgB,KAAK,UAAU,EAAE;YAC1C,IAAI,CAAC,SAAS,GAAG,gBAAgB,EAAE,CAAC;SACrC;IACH,CAAC;IAED;;;;;;;OAOG;IACI,WAAW,CAAC,OAAgB;QACjC,IAAI,IAAI,CAAC,SAAS,EAAE;YAClB,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;SACrC;aAAM;YACL,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;SACtB;IACH,CAAC;IAED;;;;;;;OAOG;IACI,QAAQ;QACb,IAAI,IAAI,CAAC,SAAS,EAAE;YAClB,OAAO,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,CAAC;SAClC;aAAM;YACL,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;YAClD,OAAO,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;SAC9C;IACH,CAAC;IAED;;;;;;;;;;OAUG;IACI,QAAQ,CAAgC,QAAW;QACxD,IAAI,IAAI,CAAC,SAAS,EAAE;YAClB,OAAO,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;SAC1C;aAAM;YACL,YAAY,CAAC,OAAO,CAAC,aAAa,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC;YAC9D,OAAO,QAAQ,CAAC;SACjB;IACH,CAAC;CACF;AAED,+EAA+E;AAClE,QAAA,MAAM,GAAG,IAAI,gBAAgB,EAAE,CAAC"} -------------------------------------------------------------------------------- /vscode/webview-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testgpt-app", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "build": "rollup -c", 7 | "dev": "rollup -c -w", 8 | "start": "sirv public --no-clear", 9 | "check": "svelte-check --tsconfig ./tsconfig.json" 10 | }, 11 | "dependencies": { 12 | "@tsconfig/svelte": "^5.0.2", 13 | "@vscode/webview-ui-toolkit": "^1.2.2", 14 | "sirv-cli": "^2.0.0" 15 | }, 16 | "devDependencies": { 17 | "@rollup/plugin-commonjs": "^17.0.0", 18 | "@rollup/plugin-node-resolve": "^11.0.0", 19 | "@rollup/plugin-typescript": "^8.0.0", 20 | "@types/vscode-webview": "^1.57.0", 21 | "rollup": "^2.3.4", 22 | "rollup-plugin-css-only": "^3.1.0", 23 | "rollup-plugin-livereload": "^2.0.0", 24 | "rollup-plugin-svelte": "^7.1.6", 25 | "rollup-plugin-terser": "^7.0.0", 26 | "svelte": "^4.2.0", 27 | "svelte-check": "^3.5.0", 28 | "svelte-preprocess": "^5.0.4", 29 | "tslib": "^2.6.2", 30 | "typescript": "^5.2.2" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /vscode/webview-ui/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import type { IPreset } from "../types"; 2 | import { vscode } from "../utilities/vscode"; 3 | 4 | export const getWebviewState = () => { 5 | let presets = ((window as any).presets as IPreset[]) || []; 6 | let activePreset = ((window as any).activePreset as IPreset) || { 7 | name: "Default Preset", 8 | config: {}, 9 | }; 10 | let advanced = ((window as any).advanced as boolean) || false; 11 | 12 | const newState = vscode.getState(); 13 | 14 | if (newState?.presets) { 15 | presets = newState.presets; 16 | } 17 | 18 | if (newState?.activePreset) { 19 | activePreset = newState.activePreset; 20 | } 21 | 22 | if (newState?.advanced) { 23 | advanced = newState.advanced; 24 | } 25 | 26 | return { 27 | presets, 28 | activePreset, 29 | advanced, 30 | }; 31 | }; 32 | 33 | export const setWebviewState = (key: string, value: any) => { 34 | vscode.setState({ 35 | ...getWebviewState(), 36 | [key]: value, 37 | }); 38 | 39 | window[key] = value; 40 | }; 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Fayez Nazzal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testgpt", 3 | "version": "4.1.0", 4 | "description": "", 5 | "main": "index.js", 6 | "types": "bin/index.d.ts", 7 | "bin": { 8 | "testgpt": "bin/index.js" 9 | }, 10 | "scripts": { 11 | "build": "tsc", 12 | "test": "vitest run", 13 | "prepublishOnly": "npm run build" 14 | }, 15 | "keywords": [ 16 | "gpt", 17 | "openai", 18 | "cli", 19 | "command-line", 20 | "command-line-tool", 21 | "cmd", 22 | "unit testing", 23 | "testing", 24 | "test", 25 | "auto", 26 | "automation" 27 | ], 28 | "author": "Fayez Nazzal", 29 | "license": "MIT", 30 | "devDependencies": { 31 | "@types/node": "^18.14.0", 32 | "typescript": "^4.9.5", 33 | "vitest": "^2.0.5" 34 | }, 35 | "dependencies": { 36 | "ajv": "^8.12.0", 37 | "axios": "^1.3.4", 38 | "chalk": "^4.1.0", 39 | "commander": "^10.0.0", 40 | "openai": "^4.4.0", 41 | "tree-sitter-cli": "^0.20.7", 42 | "yaml": "^2.3.1" 43 | }, 44 | "repository": "https://github.com/fayez-nazzal/testgpt", 45 | "bugs": "https://github.com/fayez-nazzal/testgpt/issues" 46 | } 47 | -------------------------------------------------------------------------------- /vscode/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Fayez Nazzal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /vscode/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { Uri, Webview } from "vscode"; 2 | 3 | /** 4 | * A helper function which will get the webview URI of a given file or resource. 5 | * 6 | * @remarks This URI can be used within a webview's HTML as a link to the 7 | * given file/resource. 8 | * 9 | * @param webview A reference to the extension webview 10 | * @param extensionUri The URI of the directory containing the extension 11 | * @param pathList An array of strings representing the path to a file/resource 12 | * @returns A URI pointing to the file/resource 13 | */ 14 | export function getUri(webview: Webview, extensionUri: Uri, pathList: string[]) { 15 | return webview.asWebviewUri(Uri.joinPath(extensionUri, ...pathList)); 16 | } 17 | 18 | /** 19 | * A helper function that returns a unique alphanumeric identifier called a nonce. 20 | * 21 | * @remarks This function is primarily used to help enforce content security 22 | * policies for resources/scripts being executed in a webview context. 23 | * 24 | * @returns A nonce 25 | */ 26 | export function getNonce() { 27 | let text = ""; 28 | const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 29 | for (let i = 0; i < 32; i++) { 30 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 31 | } 32 | return text; 33 | } 34 | -------------------------------------------------------------------------------- /src/__tests__/command.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' 2 | import fs from 'fs' 3 | import path from 'path' 4 | import { parseCommand, executeCommand, executeForFile } from '../command' 5 | 6 | const mockExit = vi.spyOn(process, 'exit').mockImplementation(((code?: number) => { throw new Error(`exit:${code}`) }) as any) 7 | const mockCwd = vi.spyOn(process, 'cwd') 8 | 9 | describe('command', () => { 10 | const tmpDir = path.join(process.cwd(), '.vitest-tmp-cmd') 11 | 12 | beforeEach(() => { 13 | vi.restoreAllMocks() 14 | if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir) 15 | mockCwd.mockReturnValue(tmpDir) 16 | }) 17 | 18 | afterEach(() => { 19 | if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true }) 20 | }) 21 | 22 | it('executeForFile errors without inputFile', async () => { 23 | await expect(async () => { 24 | await executeForFile({ 25 | inputFile: '' as any, 26 | outputFile: path.join(tmpDir, 'out.test.ts'), 27 | apiKey: 'k', 28 | model: 'gpt-3.5-turbo-16k', 29 | systemMessage: '', 30 | promptTemplate: '', 31 | techs: '', 32 | instructions: '', 33 | examples: [], 34 | stream: false as any, 35 | modelEndpoint: '' 36 | } as any) 37 | }).rejects.toThrow() 38 | }) 39 | }) 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/__tests__/autoTest.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' 2 | import fs from 'fs' 3 | import path from 'path' 4 | import { autoTest } from '../utils' 5 | 6 | describe('autoTest', () => { 7 | const tmpDir = path.join(process.cwd(), '.vitest-tmp-auto') 8 | 9 | beforeEach(() => { 10 | if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir) 11 | }) 12 | 13 | afterEach(() => { 14 | if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true }) 15 | }) 16 | 17 | it('writes response text when using modelEndpoint', async () => { 18 | const input = path.join(tmpDir, 'in.ts') 19 | const output = path.join(tmpDir, 'out.test.ts') 20 | fs.writeFileSync(input, 'export const x = 1') 21 | 22 | const mockFetch = vi.spyOn(global, 'fetch' as any).mockResolvedValue({ 23 | text: async () => 'TEST_CONTENT' 24 | } as any) 25 | 26 | await autoTest({ 27 | inputFile: input, 28 | outputFile: output, 29 | apiKey: 'k', 30 | model: 'gpt-3.5-turbo-16k', 31 | systemMessage: undefined, 32 | promptTemplate: undefined, 33 | examples: [], 34 | techs: [], 35 | instructions: [], 36 | stream: false, 37 | modelEndpoint: 'http://local/mock' 38 | }) 39 | 40 | expect(fs.readFileSync(output, 'utf-8')).toBe('TEST_CONTENT') 41 | mockFetch.mockRestore() 42 | }) 43 | }) 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchemaType } from "ajv"; 2 | 3 | export interface IConfig { 4 | [key: `.${string}`]: { 5 | techs: string[]; 6 | instructions: string[]; 7 | examples: IExample[]; 8 | }; 9 | } 10 | 11 | export interface IGetPromptArgs { 12 | content: string; 13 | fileName: string; 14 | techs?: string[]; 15 | instructions?: string[]; 16 | promptTemplate?: string; 17 | } 18 | 19 | export interface IExample { 20 | fileName: string; 21 | code: string; 22 | tests: string; 23 | } 24 | 25 | export const examplesSchema: JSONSchemaType = { 26 | type: "array", 27 | items: { 28 | type: "object", 29 | required: ["fileName", "code", "tests"], 30 | properties: { 31 | fileName: { 32 | type: "string", 33 | }, 34 | code: { 35 | type: "string", 36 | }, 37 | tests: { 38 | type: "string", 39 | }, 40 | }, 41 | }, 42 | }; 43 | 44 | export enum ERole { 45 | User = "user", 46 | System = "system", 47 | Assistant = "assistant", 48 | } 49 | 50 | export interface IMessage { 51 | role: ERole; 52 | content: string; 53 | } 54 | 55 | export interface ICommandArgs { 56 | inputFile: string; 57 | outputFile?: string; 58 | apiKey: string; 59 | model: string; 60 | systemMessage: string; 61 | promptTemplate: string; 62 | techs: string; 63 | instructions: string; 64 | examples: IExample[]; 65 | config?: string; 66 | stream: boolean; 67 | modelEndpoint: string; 68 | help: boolean; 69 | } 70 | -------------------------------------------------------------------------------- /vscode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testgpt", 3 | "displayName": "TestGPT", 4 | "description": "Automatic AI Powered Testing for your code.", 5 | "icon": "media/logo.png", 6 | "version": "0.0.10", 7 | "publisher": "FayezNazzal", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/fayez-nazzal/testgpt" 11 | }, 12 | "engines": { 13 | "vscode": "^1.76.0" 14 | }, 15 | "main": "./out/extension.js", 16 | "contributes": { 17 | "commands": [ 18 | { 19 | "command": "testgpt.autoTest", 20 | "title": "Run TestGPT for this file" 21 | } 22 | ], 23 | "configuration": [ 24 | { 25 | "title": "TestGPT", 26 | "properties": { 27 | "testgpt.apiKey": { 28 | "type": "string", 29 | "default": "", 30 | "description": "OpenAI API Key" 31 | }, 32 | "testgpt.advancedMode": { 33 | "type": "boolean", 34 | "default": false, 35 | "description": "Advanced mode" 36 | } 37 | } 38 | } 39 | ], 40 | "viewsContainers": { 41 | "activitybar": [ 42 | { 43 | "id": "testgpt", 44 | "title": "TestGPT", 45 | "icon": "media/icon.svg" 46 | } 47 | ] 48 | }, 49 | "views": { 50 | "testgpt": [ 51 | { 52 | "type": "webview", 53 | "id": "testgpt", 54 | "name": "TestGPT" 55 | } 56 | ] 57 | } 58 | }, 59 | "scripts": { 60 | "install:all": "npm install && cd webview-ui && npm install", 61 | "start:webview": "cd webview-ui && npm run dev", 62 | "build:webview": "cd webview-ui && npm run build", 63 | "vscode:prepublish": "npm run compile", 64 | "compile": "npm run build:webview && tsc -p ./", 65 | "watch": "tsc -watch -p ./", 66 | "pretest": "npm run compile && npm run lint", 67 | "lint": "eslint src --ext ts" 68 | }, 69 | "devDependencies": { 70 | "@types/glob": "^7.1.3", 71 | "@types/node": "^12.11.7", 72 | "@types/vscode": "^1.46.0", 73 | "@typescript-eslint/eslint-plugin": "^4.14.1", 74 | "@typescript-eslint/parser": "^4.14.1", 75 | "eslint": "^7.19.0", 76 | "glob": "^7.1.6", 77 | "prettier": "^2.2.1", 78 | "typescript": "^4.1.3", 79 | "vscode-test": "^1.5.0" 80 | }, 81 | "dependencies": { 82 | "yaml": "^2.3.2" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /vscode/webview-ui/rollup.config.js: -------------------------------------------------------------------------------- 1 | import svelte from 'rollup-plugin-svelte'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import resolve from '@rollup/plugin-node-resolve'; 4 | import livereload from 'rollup-plugin-livereload'; 5 | import { terser } from 'rollup-plugin-terser'; 6 | import sveltePreprocess from 'svelte-preprocess'; 7 | import typescript from '@rollup/plugin-typescript'; 8 | import css from 'rollup-plugin-css-only'; 9 | 10 | const production = !process.env.ROLLUP_WATCH; 11 | 12 | function serve() { 13 | let server; 14 | 15 | function toExit() { 16 | if (server) server.kill(0); 17 | } 18 | 19 | return { 20 | writeBundle() { 21 | if (server) return; 22 | server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], { 23 | stdio: ['ignore', 'inherit', 'inherit'], 24 | shell: true 25 | }); 26 | 27 | process.on('SIGTERM', toExit); 28 | process.on('exit', toExit); 29 | } 30 | }; 31 | } 32 | 33 | export default { 34 | input: 'src/main.ts', 35 | output: { 36 | sourcemap: true, 37 | format: 'iife', 38 | name: 'app', 39 | file: 'public/build/bundle.js' 40 | }, 41 | plugins: [ 42 | svelte({ 43 | preprocess: sveltePreprocess({ sourceMap: !production }), 44 | compilerOptions: { 45 | // enable run-time checks when not in production 46 | dev: !production 47 | } 48 | }), 49 | // we'll extract any component CSS out into 50 | // a separate file - better for performance 51 | css({ output: 'bundle.css' }), 52 | 53 | // If you have external dependencies installed from 54 | // npm, you'll most likely need these plugins. In 55 | // some cases you'll need additional configuration - 56 | // consult the documentation for details: 57 | // https://github.com/rollup/plugins/tree/master/packages/commonjs 58 | resolve({ 59 | browser: true, 60 | dedupe: ['svelte'] 61 | }), 62 | commonjs(), 63 | typescript({ 64 | sourceMap: !production, 65 | inlineSources: !production 66 | }), 67 | 68 | // In dev mode, call `npm run start` once 69 | // the bundle has been generated 70 | !production && serve(), 71 | 72 | // Watch the `public` directory and refresh the 73 | // browser on changes when not in production 74 | !production && livereload('public'), 75 | 76 | // If we're building for production (npm run build 77 | // instead of npm run dev), minify 78 | production && terser() 79 | ], 80 | watch: { 81 | clearScreen: false 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /vscode/webview-ui/src/App.svelte: -------------------------------------------------------------------------------- 1 | 33 | 34 |
35 | {#if !advanced} 36 |
37 | Choose a preset 38 | p.name)} setValue={onPresetChange} /> 39 |
40 | 41 |
42 | Generate tests for the active file 43 | Generate Tests 44 |
45 | {:else} 46 | 47 | {/if} 48 | 49 | 50 | 51 | 52 | {#if !advanced} 53 | Advanced Mode 54 | {:else} 55 | Simple Mode 56 | {/if} 57 | 58 | 59 | 60 | 61 |
62 | 63 | Enjoy the automation! 64 |
65 |
66 | 67 | 91 | -------------------------------------------------------------------------------- /vscode/webview-ui/src/utilities/vscode.ts: -------------------------------------------------------------------------------- 1 | import type { WebviewApi } from "vscode-webview"; 2 | import type { IVscodeState } from "../types"; 3 | 4 | /** 5 | * A utility wrapper around the acquireVsCodeApi() function, which enables 6 | * message passing and state management between the webview and extension 7 | * contexts. 8 | * 9 | * This utility also enables webview code to be run in a web browser-based 10 | * dev server by using native web browser features that mock the functionality 11 | * enabled by acquireVsCodeApi. 12 | */ 13 | class VSCodeAPIWrapper { 14 | private readonly vsCodeApi: WebviewApi | undefined; 15 | 16 | constructor() { 17 | // Check if the acquireVsCodeApi function exists in the current development 18 | // context (i.e. VS Code development window or web browser) 19 | if (typeof acquireVsCodeApi === "function") { 20 | this.vsCodeApi = acquireVsCodeApi(); 21 | } 22 | } 23 | 24 | /** 25 | * Post a message (i.e. send arbitrary data) to the owner of the webview. 26 | * 27 | * @remarks When running webview code inside a web browser, postMessage will instead 28 | * log the given message to the console. 29 | * 30 | * @param message Abitrary data (must be JSON serializable) to send to the extension context. 31 | */ 32 | public postMessage(message: unknown) { 33 | if (this.vsCodeApi) { 34 | this.vsCodeApi.postMessage(message); 35 | } else { 36 | console.log(message); 37 | } 38 | } 39 | 40 | /** 41 | * Get the persistent state stored for this webview. 42 | * 43 | * @remarks When running webview source code inside a web browser, getState will retrieve state 44 | * from local storage (https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). 45 | * 46 | * @return The current state or `undefined` if no state has been set. 47 | */ 48 | public getState(): IVscodeState { 49 | return this.vsCodeApi?.getState() as IVscodeState; 50 | } 51 | 52 | /** 53 | * Set the persistent state stored for this webview. 54 | * 55 | * @remarks When running webview source code inside a web browser, setState will set the given 56 | * state using local storage (https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). 57 | * 58 | * @param newState New persisted state. This must be a JSON serializable object. Can be retrieved 59 | * using {@link getState}. 60 | * 61 | * @return The new state. 62 | */ 63 | public setState(newState: T) { 64 | this.vsCodeApi?.setState(newState); 65 | } 66 | } 67 | 68 | // Exports class singleton to prevent multiple invocations of acquireVsCodeApi. 69 | export const vscode = new VSCodeAPIWrapper(); 70 | -------------------------------------------------------------------------------- /src/__tests__/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' 2 | import fs from 'fs' 3 | import path from 'path' 4 | import { 5 | readFile, 6 | writeToFile, 7 | divideFileName, 8 | getFileType, 9 | EFileType, 10 | toList, 11 | parseTemplatePrompt, 12 | getPrompt, 13 | getExampleMessages, 14 | readYamlFile, 15 | getMessages, 16 | } from '../utils' 17 | 18 | describe('utils', () => { 19 | const tmpDir = path.join(process.cwd(), '.vitest-tmp') 20 | 21 | beforeEach(() => { 22 | if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir) 23 | }) 24 | 25 | afterEach(() => { 26 | if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true }) 27 | }) 28 | 29 | it('readFile returns file content', () => { 30 | const file = path.join(tmpDir, 'a.txt') 31 | fs.writeFileSync(file, 'hello') 32 | expect(readFile(file)).toBe('hello') 33 | }) 34 | 35 | it('writeToFile writes content', () => { 36 | const file = path.join(tmpDir, 'b.txt') 37 | writeToFile(file, 'world') 38 | expect(fs.readFileSync(file, 'utf-8')).toBe('world') 39 | }) 40 | 41 | it('divideFileName returns name and extension', () => { 42 | expect(divideFileName('/x/y/file.ts')).toEqual({ name: 'file', extension: '.ts' }) 43 | }) 44 | 45 | it('getFileType returns file or directory', () => { 46 | const dir = path.join(tmpDir, 'dir') 47 | const file = path.join(tmpDir, 'file.txt') 48 | fs.mkdirSync(dir) 49 | fs.writeFileSync(file, '') 50 | expect(getFileType(dir)).toBe(EFileType.Directory) 51 | expect(getFileType(file)).toBe(EFileType.File) 52 | }) 53 | 54 | it('toList formats list', () => { 55 | expect(toList(['a', 'b'])).toBe('1. a\r\n2. b') 56 | }) 57 | 58 | it('parseTemplatePrompt replaces placeholders', () => { 59 | expect(parseTemplatePrompt('a {x} b', { x: 1 })).toBe('a 1 b') 60 | }) 61 | 62 | it('getPrompt builds prompt with defaults', () => { 63 | const prompt = getPrompt({ content: 'CODE', fileName: 'f.ts' }) 64 | expect(prompt).toContain('f.ts') 65 | expect(prompt).toContain('CODE') 66 | }) 67 | 68 | it('getExampleMessages turns examples into messages', () => { 69 | const messages = getExampleMessages( 70 | { content: 'c', fileName: 'f.ts' }, 71 | [{ fileName: 'e.ts', code: 'x', tests: 'y' }] 72 | ) 73 | expect(messages.length).toBe(2) 74 | expect(messages[0].role).toBe('user') 75 | expect(messages[1].role).toBe('assistant') 76 | }) 77 | 78 | it('readYamlFile parses yaml', () => { 79 | const file = path.join(tmpDir, 'c.yaml') 80 | fs.writeFileSync(file, 'a: 1') 81 | expect(readYamlFile(file)).toEqual({ a: 1 }) 82 | }) 83 | 84 | it('getMessages builds message array', () => { 85 | const messages = getMessages(undefined, 'hi', []) 86 | expect(messages[0].role).toBe('system') 87 | expect(messages[messages.length - 1].content).toBe('hi') 88 | }) 89 | }) 90 | 91 | 92 | -------------------------------------------------------------------------------- /vscode/webview-ui/src/utilities/vscode.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.vscode = void 0; 4 | /** 5 | * A utility wrapper around the acquireVsCodeApi() function, which enables 6 | * message passing and state management between the webview and extension 7 | * contexts. 8 | * 9 | * This utility also enables webview code to be run in a web browser-based 10 | * dev server by using native web browser features that mock the functionality 11 | * enabled by acquireVsCodeApi. 12 | */ 13 | class VSCodeAPIWrapper { 14 | constructor() { 15 | // Check if the acquireVsCodeApi function exists in the current development 16 | // context (i.e. VS Code development window or web browser) 17 | if (typeof acquireVsCodeApi === "function") { 18 | this.vsCodeApi = acquireVsCodeApi(); 19 | } 20 | } 21 | /** 22 | * Post a message (i.e. send arbitrary data) to the owner of the webview. 23 | * 24 | * @remarks When running webview code inside a web browser, postMessage will instead 25 | * log the given message to the console. 26 | * 27 | * @param message Abitrary data (must be JSON serializable) to send to the extension context. 28 | */ 29 | postMessage(message) { 30 | if (this.vsCodeApi) { 31 | this.vsCodeApi.postMessage(message); 32 | } 33 | else { 34 | console.log(message); 35 | } 36 | } 37 | /** 38 | * Get the persistent state stored for this webview. 39 | * 40 | * @remarks When running webview source code inside a web browser, getState will retrieve state 41 | * from local storage (https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). 42 | * 43 | * @return The current state or `undefined` if no state has been set. 44 | */ 45 | getState() { 46 | if (this.vsCodeApi) { 47 | return this.vsCodeApi.getState(); 48 | } 49 | else { 50 | const state = localStorage.getItem("vscodeState"); 51 | return state ? JSON.parse(state) : undefined; 52 | } 53 | } 54 | /** 55 | * Set the persistent state stored for this webview. 56 | * 57 | * @remarks When running webview source code inside a web browser, setState will set the given 58 | * state using local storage (https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). 59 | * 60 | * @param newState New persisted state. This must be a JSON serializable object. Can be retrieved 61 | * using {@link getState}. 62 | * 63 | * @return The new state. 64 | */ 65 | setState(newState) { 66 | if (this.vsCodeApi) { 67 | return this.vsCodeApi.setState(newState); 68 | } 69 | else { 70 | localStorage.setItem("vscodeState", JSON.stringify(newState)); 71 | return newState; 72 | } 73 | } 74 | } 75 | // Exports class singleton to prevent multiple invocations of acquireVsCodeApi. 76 | exports.vscode = new VSCodeAPIWrapper(); 77 | //# sourceMappingURL=vscode.js.map -------------------------------------------------------------------------------- /testgpt.config.yaml: -------------------------------------------------------------------------------- 1 | .ts: 2 | techs: 3 | - jest 4 | instructions: 5 | - use 2 spaces for indentation 6 | - wrap each group of tests in a describe block 7 | - Don't forget to import the things you need. 8 | examples: 9 | - fileName: array_utils.test.ts 10 | code: | 11 | export const addItem = (array: any[], item: any) => { 12 | return [...array, item]; 13 | }; 14 | 15 | export const removeItem = (array: any[], item: any) => { 16 | return array.filter((i) => i !== item); 17 | }; 18 | 19 | export const toggleItem = (array: any[], item: any) => { 20 | return array.includes(item) ? removeItem(array, item) : addItem(array, item); 21 | }; 22 | 23 | export const toggleItemInArray = (array: any[], item: any) => { 24 | return array.includes(item) ? removeItem(array, item) : addItem(array, item); 25 | }; 26 | tests: | 27 | import { 28 | addItem, 29 | removeItem, 30 | toggleItem, 31 | toggleItemInArray, 32 | } from "./array_utils"; 33 | 34 | describe("array_utils", () => { 35 | it("should add item to the array", () => { 36 | const array = ["item1", "item2"]; 37 | const itemToAdd = "item3"; 38 | 39 | expect(addItem(array, itemToAdd)).toEqual([...array, itemToAdd]); 40 | }); 41 | 42 | describe("removeItem", () => { 43 | it("should remove item from the array", () => { 44 | const array = ["item1", "item2", "item3"]; 45 | const itemToRemove = "item2"; 46 | 47 | expect(removeItem(array, itemToRemove)).toEqual(["item1", "item3"]); 48 | }); 49 | }); 50 | 51 | describe("toggleItem", () => { 52 | it("should remove the item if it already exists in the array", () => { 53 | const array = ["item1", "item2"]; 54 | const itemToToggle = "item2"; 55 | 56 | expect(toggleItem(array, itemToToggle)).toEqual(["item1"]); 57 | }); 58 | 59 | it("should add the item if it doesn't exist in the array", () => { 60 | const array = ["item1", "item2"]; 61 | const itemToToggle = "item3"; 62 | 63 | expect(toggleItem(array, itemToToggle)).toEqual([...array, itemToToggle]); 64 | }); 65 | }); 66 | 67 | describe("toggleItemInArray", () => { 68 | it("should remove the item if it already exists in the array", () => { 69 | const array = ["item1", "item2"]; 70 | const itemToToggle = "item2"; 71 | 72 | expect(toggleItemInArray(array, itemToToggle)).toEqual(["item1"]); 73 | }); 74 | 75 | it("should add the item if it doesn't exist in the array", () => { 76 | const array = ["item1", "item2"]; 77 | const itemToToggle = "item3"; 78 | 79 | expect(toggleItemInArray(array, itemToToggle)).toEqual([ 80 | ...array, 81 | itemToToggle, 82 | ]); 83 | }); 84 | }); 85 | }); 86 | - fileName: data.ts 87 | code: | 88 | import axios from "axios"; 89 | import chalk from "chalk"; 90 | 91 | export const fetchDogNames = async () => { 92 | console.log(chalk.blue("Fetching dog names...")); 93 | const response = await fetch("https://dog.ceo/api/breeds/list/all"); 94 | const data = await response.json(); 95 | return Object.keys(data.message); 96 | }; 97 | 98 | export const fetchCatNames = async () => { 99 | console.log(chalk.blue("Fetching cat names...")); 100 | const response = await axios.get("https://api.thecatapi.com/v1/breeds"); 101 | return response.data.map((cat: any) => cat.name); 102 | }; 103 | 104 | export const fetchDogImage = async (breed: string) => { 105 | console.log(chalk.green(`Fetching ${breed} image...`)); 106 | const response = await fetch( 107 | `https://dog.ceo/api/breed/${breed}/images/random` 108 | ); 109 | const data = await response.json(); 110 | return data.message; 111 | }; 112 | tests: | 113 | import axios from "axios"; 114 | import { fetchDogImage, fetchCatNames, fetchDogNames } from "./data"; 115 | 116 | jest.mock("chalk", () => ({ 117 | blue: jest.fn((text) => text), 118 | green: jest.fn((text) => text), 119 | })); 120 | 121 | (global as any).fetch = jest.fn(); 122 | 123 | jest.mock("axios", () => ({ 124 | get: jest.fn(), 125 | })); 126 | 127 | describe("data", () => { 128 | it("should fetch dog names", async () => { 129 | (global as any).fetch.mockImplementationOnce(() => 130 | Promise.resolve({ 131 | json: () => Promise.resolve({ message: { bulldog: [], poodle: [] } }), 132 | }) 133 | ); 134 | 135 | const dogNames = await fetchDogNames(); 136 | expect(dogNames).toEqual(["bulldog", "poodle"]); 137 | }); 138 | 139 | it("should fetch cat names", async () => { 140 | (axios.get as jest.Mock).mockImplementationOnce(() => 141 | Promise.resolve({ 142 | data: [{ name: "catie" }, { name: "kitty" }], 143 | }) 144 | ); 145 | 146 | const catNames = await fetchCatNames(); 147 | expect(catNames).toEqual(["catie", "kitty"]); 148 | }); 149 | 150 | it("should fetch dog image", async () => { 151 | const link = "https://dog.ceo/api/breed/bulldog/images/random"; 152 | 153 | (global as any).fetch.mockImplementationOnce(() => 154 | Promise.resolve({ 155 | json: () => 156 | Promise.resolve({ 157 | message: link, 158 | }), 159 | }) 160 | ); 161 | 162 | const dogImage = await fetchDogImage("bulldog"); 163 | expect(dogImage).toEqual(link); 164 | }); 165 | }); 166 | -------------------------------------------------------------------------------- /src/command.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | 4 | import { program } from "commander"; 5 | import chalk from "chalk"; 6 | 7 | import { CONFIG_FILE_NAME, DEFAULT_MODEL } from "./const"; 8 | import { ICommandArgs, IConfig, examplesSchema } from "./types"; 9 | import { 10 | autoTest, 11 | divideFileName, 12 | EFileType, 13 | getFileType, 14 | IModel, 15 | readYamlFile, 16 | } from "./utils"; 17 | import Ajv from "ajv"; 18 | 19 | export const parseCommand = () => { 20 | program 21 | .option("-i, --inputFile ") 22 | .option("-o, --outputFile ") 23 | .option("-k, --apiKey ") 24 | .option("-m, --model ") 25 | .option("-p, --promptTemplate ") 26 | .option("-y, --systemMessage ") 27 | .option("-t, --techs ") 28 | .option("-n, --instructions ") 29 | .option("-x, --examples ") 30 | .option("-c, --config ") 31 | .option("-s, --stream") 32 | .option("-e, --modelEndpoint ") 33 | .option("-h, --help"); 34 | 35 | program.parse(); 36 | 37 | const options = program.opts(); 38 | 39 | if (options.help) { 40 | console.log( 41 | chalk.blue( 42 | `Usage: testgpt -i -o -k -m -t -p -c ` 43 | ) 44 | ); 45 | 46 | console.log( 47 | chalk.green( 48 | "\r\nAll fields are optional except for the input file. If no output file is provided, the default will be used." 49 | ) 50 | ); 51 | 52 | // exit the program 53 | process.exit(0); 54 | } 55 | 56 | return options as ICommandArgs; 57 | }; 58 | 59 | export const executeCommand = async (args: ICommandArgs) => { 60 | const { help, inputFile, outputFile } = args; 61 | if (help) { 62 | console.log( 63 | chalk.blue( 64 | `Usage: testgpt -i -o -k -m -t -p -c ` 65 | ) 66 | ); 67 | 68 | console.log( 69 | chalk.green( 70 | "\r\nAll fields are optional except inputFile. If no inputFile is provided, the default will be used." 71 | ) 72 | ); 73 | 74 | // exit the program 75 | process.exit(0); 76 | } 77 | 78 | const isInputDirectory = getFileType(inputFile) === EFileType.Directory; 79 | const isOutputDirectory = 80 | outputFile && getFileType(outputFile) === EFileType.Directory; 81 | 82 | if (isInputDirectory && outputFile && !isOutputDirectory) { 83 | console.error( 84 | chalk.red( 85 | "If inputFile is a directory, outputFile must also be a directory" 86 | ) 87 | ); 88 | process.exit(1); 89 | } 90 | 91 | if (isInputDirectory) { 92 | // if outputDirectory is not provided, use the inputDirectory 93 | const outputDirectory = outputFile || inputFile; 94 | 95 | const files = fs.readdirSync(inputFile); 96 | 97 | for (const file of files) { 98 | const inputFilePath = path.join(inputFile, file); 99 | const { name: inputFileName, extension } = divideFileName(inputFilePath); 100 | const outputFilePath = path.join( 101 | outputDirectory, 102 | `${inputFileName}.test${extension}` 103 | ); 104 | 105 | await executeForFile({ 106 | ...args, 107 | inputFile: inputFilePath, 108 | outputFile: outputFilePath, 109 | }); 110 | } 111 | } else { 112 | await executeForFile(args); 113 | } 114 | 115 | process.exit(0); 116 | }; 117 | 118 | export const executeForFile = async ({ 119 | inputFile, 120 | outputFile, 121 | apiKey, 122 | model, 123 | systemMessage, 124 | promptTemplate, 125 | techs, 126 | instructions, 127 | examples, 128 | config, 129 | stream, 130 | modelEndpoint, 131 | }: Omit) => { 132 | if (examples && typeof examples === "string") { 133 | const ajv = new Ajv(); 134 | const validate = ajv.compile(examplesSchema); 135 | 136 | examples = JSON.parse(examples); 137 | 138 | const valid = validate(examples); 139 | if (!valid) { 140 | console.error(chalk.red("Invalid examples format")); 141 | console.error(chalk.red(JSON.stringify(validate.errors, null, 2))); 142 | process.exit(1); 143 | } 144 | } 145 | 146 | let { extension: inputFileExtension } = divideFileName(inputFile); 147 | 148 | if (!inputFile) { 149 | console.error(chalk.red("Please provide an input file")); 150 | process.exit(1); 151 | } 152 | 153 | console.log(chalk.blue(`Reading ${CONFIG_FILE_NAME}...`)); 154 | 155 | const configFilePath = path.join(process.cwd(), `${CONFIG_FILE_NAME}`); 156 | 157 | let testGPTConfig: IConfig = {}; 158 | 159 | if (fs.existsSync(configFilePath)) { 160 | console.log(chalk.green("Config file found, using..")); 161 | 162 | testGPTConfig = readYamlFile( 163 | config || path.join(process.cwd(), `${CONFIG_FILE_NAME}`) 164 | ); 165 | } else { 166 | if (techs) { 167 | console.log(chalk.blue(`Config file not found, using passed config`)); 168 | 169 | testGPTConfig = { 170 | [inputFileExtension]: { 171 | techs: techs?.split(",") || [], 172 | instructions: instructions?.split(",") || [], 173 | examples: [], 174 | }, 175 | }; 176 | } else { 177 | console.log( 178 | chalk.blue(`Config file not found, continuing with default config`) 179 | ); 180 | } 181 | } 182 | 183 | if (!outputFile) { 184 | const inputFileWithoutExtension = inputFile.replace(inputFileExtension, ""); 185 | 186 | outputFile = `${inputFileWithoutExtension}.test${inputFileExtension}`; 187 | 188 | console.log(chalk.blue("No output file provided, using default.")); 189 | 190 | console.log(chalk.yellow(`Output file: ${outputFile}`)); 191 | } 192 | 193 | const parsedTechs = testGPTConfig?.[inputFileExtension]?.techs; 194 | const parsedInstructions = testGPTConfig?.[inputFileExtension]?.instructions; 195 | examples ??= testGPTConfig?.[inputFileExtension]?.examples; 196 | apiKey ??= process.env.OPENAI_API_KEY as string; 197 | model ??= DEFAULT_MODEL; 198 | 199 | await autoTest({ 200 | inputFile, 201 | outputFile, 202 | apiKey, 203 | model: model as IModel, 204 | systemMessage, 205 | promptTemplate, 206 | examples, 207 | techs: parsedTechs, 208 | instructions: parsedInstructions, 209 | stream, 210 | modelEndpoint, 211 | }); 212 | }; 213 | -------------------------------------------------------------------------------- /vscode/resources/default.yaml: -------------------------------------------------------------------------------- 1 | - name: Auto Preset 2 | config: 3 | model: gpt-3.5-turbo-16k 4 | streaming: true 5 | systemMessage: You are my unit testing assistant, you will help write unit tests 6 | for the files I provide, your reply will only include the code, nothing 7 | more. 8 | promptTemplate: |- 9 | Please provide unit tests for the file {fileName} using {techs} 10 | {instructions} 11 | 12 | Here is the file content: 13 | ```{content}``` 14 | instructions: Only respond with the code directly, don't include any additional 15 | information. don't use markdown or any other formatting. 16 | techs: 17 | autoTechs: true 18 | examples: 19 | - fileName: greetings.js 20 | code: >- 21 | function sayHello() { 22 | return 'Hello'; 23 | } 24 | 25 | function sayOhayo() { 26 | return 'Ohayo'; 27 | } 28 | 29 | function sayGoodbye() { 30 | return 'Goodbye'; 31 | } 32 | 33 | function saySomething(thing) { 34 | return thing; 35 | } 36 | 37 | module.exports = { 38 | sayHello, 39 | sayOhayo, 40 | sayGoodbye, 41 | saySomething, 42 | }; 43 | tests: >- 44 | const { sayHello, sayOhayo, sayGoodbye, saySomething } = require('./greetings'); 45 | 46 | test('should say Hello', () => { 47 | expect(sayHello()).toBe('Hello'); 48 | }); 49 | 50 | test('should say Ohayo', () => { 51 | expect(sayOhayo()).toBe('Ohayo'); 52 | }); 53 | 54 | test('should say Goodbye', () => { 55 | expect(sayGoodbye()).toBe('Goodbye'); 56 | }); 57 | 58 | test('should say something', () => { 59 | expect(saySomething('Apple')).toBe('Apple'); 60 | }); 61 | - fileName: greetings.py 62 | code: |- 63 | def say_hello(): 64 | return 'Hello' 65 | 66 | def say_ohayo(): 67 | return 'Ohayo' 68 | 69 | def say_goodbye(): 70 | return 'Goodbye' 71 | 72 | def say_something(thing): 73 | return thing 74 | tests: |- 75 | from greetings import say_hello, say_ohayo, say_goodbye, say_something 76 | 77 | def test_say_hello(): 78 | assert say_hello() == 'Hello' 79 | 80 | def test_say_ohayo(): 81 | assert say_ohayo() == 'Ohayo' 82 | 83 | def test_say_goodbye(): 84 | assert say_goodbye() == 'Goodbye' 85 | 86 | def test_say_something(): 87 | assert say_something('Apple') == 'Apple' 88 | - name: React & Jest 89 | config: 90 | model: gpt-3.5-turbo-16k 91 | streaming: true 92 | systemMessage: You are my unit testing assistant, you will help write unit tests 93 | for the files I provide, your reply will only include the code, nothing 94 | more. 95 | promptTemplate: |- 96 | Please provide unit tests for the file {fileName} using {techs} 97 | {instructions} 98 | 99 | Here is the file content: 100 | ```{content}``` 101 | instructions: Follow best practices. Follow my guidelines. Only respond with the code. 102 | techs: [] 103 | examples: 104 | - fileName: Header.tsx 105 | code: |- 106 | import React from 'react'; 107 | 108 | interface HeaderProps { 109 | title: string; 110 | subtitle: string; 111 | } 112 | 113 | const Header: React.FC = ({ title, subtitle }) => { 114 | return ( 115 |
116 |

{title}

117 |

{subtitle}

118 |
119 | ); 120 | }; 121 | 122 | export default Header; 123 | tests: |- 124 | import React from 'react'; 125 | import { render } from '@testing-library/react'; 126 | import Header from './Header'; 127 | 128 | test('renders title and subtitle', () => { 129 | const { getByText } = render(
); 130 | 131 | const titleElement = getByText(/My Title/i); 132 | const subtitleElement = getByText(/My Subtitle/i); 133 | 134 | expect(titleElement).toBeInTheDocument(); 135 | expect(subtitleElement).toBeInTheDocument(); 136 | }); 137 | - fileName: Dropdown.tsx 138 | code: >- 139 | import React, { useState } from 'react'; 140 | 141 | interface DropdownProps { 142 | options: string[]; 143 | defaultOption: string; 144 | } 145 | 146 | const Dropdown: React.FC = ({ options, defaultOption }) => { 147 | const [selectedOption, setSelectedOption] = useState(defaultOption); 148 | 149 | const handleChange = (e: React.ChangeEvent) => { 150 | setSelectedOption(e.target.value); 151 | }; 152 | 153 | return ( 154 | 161 | ); 162 | }; 163 | 164 | export default Dropdown; 165 | tests: >- 166 | import React from 'react'; 167 | import { render, fireEvent } from '@testing-library/react'; 168 | import Dropdown from './Dropdown'; 169 | 170 | test('renders options and handles selection', () => { 171 | const options = ['Apple', 'Banana', 'Cherry']; 172 | const defaultOption = 'Apple'; 173 | 174 | const { getByDisplayValue } = render( 175 | 176 | ); 177 | 178 | // Check initial default value 179 | expect(getByDisplayValue(defaultOption)).toBeInTheDocument(); 180 | 181 | // Change selection and verify 182 | const dropdown = getByDisplayValue(defaultOption) as HTMLSelectElement; 183 | fireEvent.change(dropdown, { target: { value: 'Banana' } }); 184 | 185 | expect(getByDisplayValue('Banana')).toBeInTheDocument(); 186 | }); 187 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import fs from "fs"; 3 | import path from "path"; 4 | import OpenAI from "openai"; 5 | import { parse } from "yaml"; 6 | import { ERole, IExample, IMessage, IGetPromptArgs } from "./types"; 7 | 8 | export const readFile = (path: string) => { 9 | try { 10 | const fileContent: string = fs.readFileSync(path, "utf-8"); 11 | return fileContent; 12 | } catch (err) { 13 | console.error(`Error reading file: ${err}`); 14 | return ""; 15 | } 16 | }; 17 | 18 | export const writeToFile = ( 19 | path: string, 20 | content: string, 21 | append?: boolean 22 | ) => { 23 | try { 24 | fs.writeFileSync(path, content, { 25 | flag: append ? "a" : "w", 26 | }); 27 | console.log(chalk.green(`Successfully wrote to file: ${path}`)); 28 | } catch (err) { 29 | console.error(`Error writing to file: ${err}`); 30 | } 31 | }; 32 | 33 | export const divideFileName = (fileName: string) => { 34 | const extension = path.extname(fileName); 35 | const name = path.basename(fileName, extension); 36 | 37 | return { name, extension }; 38 | }; 39 | 40 | export enum EFileType { 41 | File, 42 | Directory, 43 | } 44 | 45 | export const getFileType = (path: string) => { 46 | try { 47 | const isDirectory = fs.lstatSync(path).isDirectory(); 48 | 49 | return isDirectory ? EFileType.Directory : EFileType.File; 50 | } catch (err) { 51 | console.error(`Error getting file type: ${err}`); 52 | return EFileType.File; 53 | } 54 | }; 55 | 56 | export const toList = (arr: string[]) => 57 | arr.map((tip, index) => `${index + 1}. ${tip}`).join("\r\n"); 58 | 59 | export const parseTemplatePrompt = (template: string, args: any) => { 60 | const regex = /{(\w+)}/g; 61 | 62 | return template.replace(regex, (_, key) => { 63 | return args[key]; 64 | }); 65 | }; 66 | 67 | export const getPrompt = ({ 68 | content, 69 | fileName, 70 | techs, 71 | instructions, 72 | promptTemplate, 73 | }: IGetPromptArgs) => { 74 | let prompt = 75 | promptTemplate ?? 76 | `Please provide unit tests for the file {fileName} using {techs} 77 | {instructions} 78 | 79 | Please begin your response with \`\`\` and end it with \`\`\` directly. 80 | 81 | Here is the file content: 82 | \`\`\`{content}\`\`\``; 83 | 84 | const techsCotent = techs?.length ? toList(techs) : "same techs as the file"; 85 | const instructionsContent = instructions?.join("\r\n") || ""; 86 | 87 | const resultPrompt = parseTemplatePrompt(prompt, { 88 | content, 89 | fileName, 90 | techs: techsCotent, 91 | instructions: instructionsContent, 92 | }); 93 | 94 | return resultPrompt; 95 | }; 96 | 97 | export const getExampleMessages = ( 98 | promptArgs: IGetPromptArgs, 99 | examples?: IExample[] 100 | ) => { 101 | if (!examples) { 102 | return []; 103 | } 104 | 105 | const messages = examples 106 | .map((g) => { 107 | const prompt = getPrompt({ 108 | ...promptArgs, 109 | content: g.code, 110 | fileName: g.fileName, 111 | }); 112 | 113 | return [ 114 | { 115 | role: ERole.User, 116 | content: prompt, 117 | }, 118 | { 119 | role: ERole.Assistant, 120 | content: g.tests, 121 | }, 122 | ]; 123 | }) 124 | .flat(); 125 | 126 | return messages as IMessage[]; 127 | }; 128 | 129 | export const readYamlFile = (path: string) => { 130 | const content = readFile(path); 131 | return parse(content); 132 | }; 133 | 134 | export type IModel = "gpt-3.5-turbo" | "gpt-3.5-turbo-16k" | "gpt-4"; 135 | 136 | export const initOpenAI = async (apiKey: string) => { 137 | const openai = new OpenAI({ 138 | apiKey: apiKey, 139 | }); 140 | 141 | return openai; 142 | }; 143 | 144 | export const getMessages = ( 145 | systemMessage: string | undefined, 146 | prompt: string, 147 | examples: IMessage[] 148 | ) => { 149 | systemMessage ??= 150 | "You are my unit testing assistant, you will help me write unit tests for the files I provide, your reply will only include the unit tests without any additional information, starting your response with ``` and ending it with ``` directly will help me understand your response better."; 151 | 152 | return [ 153 | { 154 | role: ERole.System, 155 | content: systemMessage, 156 | }, 157 | ...examples, 158 | { 159 | role: ERole.User, 160 | content: prompt, 161 | }, 162 | ]; 163 | }; 164 | 165 | interface IGetTestContentArgs { 166 | model: IModel; 167 | messages: IMessage[]; 168 | openai: OpenAI; 169 | } 170 | 171 | export const getTestContent = async ({ 172 | model, 173 | messages, 174 | openai, 175 | }: IGetTestContentArgs) => { 176 | const response = await openai.chat.completions.create({ 177 | model, 178 | messages, 179 | }); 180 | 181 | // remove lines that start with ``` (markdown code block) 182 | const regex = /^```.*$/gm; 183 | return response.choices[0].message?.content?.replace(regex, ""); 184 | }; 185 | 186 | export interface IStreamTestContentArgs { 187 | model: IModel; 188 | messages: IMessage[]; 189 | openai: OpenAI; 190 | onToken: (token: string) => void; 191 | } 192 | 193 | export const streamTestContent = async ({ 194 | model, 195 | messages, 196 | openai, 197 | onToken, 198 | }: IStreamTestContentArgs) => { 199 | const response = await openai.chat.completions.create({ 200 | model, 201 | messages, 202 | stream: true, 203 | }); 204 | 205 | for await (const part of response) { 206 | const content = part.choices[0].delta.content; 207 | 208 | if (content) { 209 | onToken(content); 210 | } 211 | } 212 | }; 213 | 214 | interface IAutoTestArgs { 215 | inputFile: string; 216 | outputFile: string; 217 | apiKey: string; 218 | model: IModel; 219 | systemMessage?: string; 220 | promptTemplate?: string; 221 | modelEndpoint?: string; 222 | examples?: IExample[]; 223 | techs?: string[]; 224 | instructions?: string[]; 225 | stream?: boolean; 226 | } 227 | 228 | export const autoTest = async ({ 229 | inputFile, 230 | outputFile, 231 | apiKey, 232 | model, 233 | systemMessage, 234 | promptTemplate, 235 | examples, 236 | techs, 237 | instructions, 238 | stream, 239 | modelEndpoint, 240 | }: IAutoTestArgs) => { 241 | console.log(chalk.blue("Reading input file...")); 242 | 243 | let content: string; 244 | try { 245 | content = readFile(inputFile); 246 | } catch (err) { 247 | console.error(chalk.red(`Error reading file: ${err}`)); 248 | process.exit(1); 249 | } 250 | 251 | console.log(chalk.blue("Generating tests...")); 252 | 253 | const promptArgs = { 254 | content, 255 | fileName: inputFile, 256 | techs, 257 | instructions, 258 | promptTemplate, 259 | }; 260 | 261 | const prompt = getPrompt(promptArgs); 262 | const exampleMessages = getExampleMessages(promptArgs, examples); 263 | 264 | if (modelEndpoint) { 265 | console.log("Found model endpoint, using it instead of OpenAI API"); 266 | const response = await fetch(modelEndpoint, { 267 | method: "POST", 268 | body: JSON.stringify({ 269 | prompt, 270 | examples: exampleMessages, 271 | }), 272 | headers: { 273 | "Content-Type": "application/json", 274 | }, 275 | }); 276 | 277 | const text = await response.text(); 278 | writeToFile(outputFile, text); 279 | return; 280 | } 281 | 282 | const messages = getMessages(systemMessage, prompt, exampleMessages); 283 | 284 | const openai = await initOpenAI(apiKey); 285 | 286 | if (stream) { 287 | const onToken = (token) => { 288 | writeToFile(outputFile, token, true); 289 | }; 290 | 291 | await streamTestContent({ 292 | openai, 293 | model, 294 | messages, 295 | onToken, 296 | }); 297 | } else { 298 | const testContent = await getTestContent({ 299 | openai, 300 | model, 301 | messages, 302 | }); 303 | 304 | if (!testContent) { 305 | console.error(chalk.red("Error generating tests - No tests content")); 306 | process.exit(1); 307 | } 308 | 309 | writeToFile(outputFile, testContent); 310 | } 311 | }; 312 | -------------------------------------------------------------------------------- /vscode/webview-ui/src/Advanced.svelte: -------------------------------------------------------------------------------- 1 | 148 | 149 |
150 | p.name)} value={activePreset.name} setValue={onPresetChange} /> 151 |
152 | Add New 153 | Delete 154 |
155 | 156 | 157 | 158 |
159 |
160 | Generate tests for the active file 161 | Generate Tests 162 |
163 | 164 | (model = val)} 168 | /> 169 | 170 | Enable Streaming 171 |