@codebakers/cli 3.9.15 โ 3.9.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mcp/server.js +310 -1
- package/package.json +1 -1
- package/src/mcp/server.ts +328 -3
package/dist/mcp/server.js
CHANGED
|
@@ -798,6 +798,28 @@ class CodeBakersServer {
|
|
|
798
798
|
},
|
|
799
799
|
},
|
|
800
800
|
},
|
|
801
|
+
{
|
|
802
|
+
name: 'generate_tests',
|
|
803
|
+
description: 'Generate test stubs for a file or feature. Creates a test file with happy path and error case templates based on the source code. Reduces friction for adding tests. Use when user needs help writing tests.',
|
|
804
|
+
inputSchema: {
|
|
805
|
+
type: 'object',
|
|
806
|
+
properties: {
|
|
807
|
+
file: {
|
|
808
|
+
type: 'string',
|
|
809
|
+
description: 'Source file to generate tests for (e.g., "src/components/LoginForm.tsx", "src/app/api/users/route.ts")',
|
|
810
|
+
},
|
|
811
|
+
feature: {
|
|
812
|
+
type: 'string',
|
|
813
|
+
description: 'Feature name if generating tests for a feature rather than a specific file',
|
|
814
|
+
},
|
|
815
|
+
testType: {
|
|
816
|
+
type: 'string',
|
|
817
|
+
enum: ['unit', 'integration', 'e2e'],
|
|
818
|
+
description: 'Type of test to generate (default: unit for components/functions, integration for API routes)',
|
|
819
|
+
},
|
|
820
|
+
},
|
|
821
|
+
},
|
|
822
|
+
},
|
|
801
823
|
{
|
|
802
824
|
name: 'validate_complete',
|
|
803
825
|
description: 'MANDATORY: Call this BEFORE saying "done" or "complete" on any feature. Validates that tests exist, tests pass, and TypeScript compiles. Returns { valid: true } or { valid: false, missing: [...] }. You are NOT ALLOWED to complete a feature without calling this first.',
|
|
@@ -813,6 +835,15 @@ class CodeBakersServer {
|
|
|
813
835
|
items: { type: 'string' },
|
|
814
836
|
description: 'Files that were created/modified for this feature',
|
|
815
837
|
},
|
|
838
|
+
envVarsAdded: {
|
|
839
|
+
type: 'array',
|
|
840
|
+
items: { type: 'string' },
|
|
841
|
+
description: 'New environment variables added during implementation (e.g., ["PAYPAL_CLIENT_ID", "PAYPAL_SECRET"])',
|
|
842
|
+
},
|
|
843
|
+
schemaModified: {
|
|
844
|
+
type: 'boolean',
|
|
845
|
+
description: 'Set to true if database schema (db/schema.ts) was modified',
|
|
846
|
+
},
|
|
816
847
|
},
|
|
817
848
|
required: ['feature'],
|
|
818
849
|
},
|
|
@@ -1516,6 +1547,8 @@ class CodeBakersServer {
|
|
|
1516
1547
|
return this.handleProjectStatus();
|
|
1517
1548
|
case 'run_tests':
|
|
1518
1549
|
return this.handleRunTests(args);
|
|
1550
|
+
case 'generate_tests':
|
|
1551
|
+
return this.handleGenerateTests(args);
|
|
1519
1552
|
case 'validate_complete':
|
|
1520
1553
|
return this.handleValidateComplete(args);
|
|
1521
1554
|
case 'discover_patterns':
|
|
@@ -3629,17 +3662,290 @@ Just describe what you want to build! I'll automatically:
|
|
|
3629
3662
|
}],
|
|
3630
3663
|
};
|
|
3631
3664
|
}
|
|
3665
|
+
/**
|
|
3666
|
+
* Generate test stubs for a file or feature
|
|
3667
|
+
* Analyzes source code and creates appropriate test templates
|
|
3668
|
+
*/
|
|
3669
|
+
handleGenerateTests(args) {
|
|
3670
|
+
const { file, feature, testType } = args;
|
|
3671
|
+
const cwd = process.cwd();
|
|
3672
|
+
if (!file && !feature) {
|
|
3673
|
+
return {
|
|
3674
|
+
content: [{
|
|
3675
|
+
type: 'text',
|
|
3676
|
+
text: 'โ Please provide either a file path or feature name to generate tests for.',
|
|
3677
|
+
}],
|
|
3678
|
+
isError: true,
|
|
3679
|
+
};
|
|
3680
|
+
}
|
|
3681
|
+
// Detect test framework
|
|
3682
|
+
let testFramework = 'vitest';
|
|
3683
|
+
try {
|
|
3684
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
3685
|
+
if (fs.existsSync(pkgPath)) {
|
|
3686
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
3687
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
3688
|
+
if (deps['jest'])
|
|
3689
|
+
testFramework = 'jest';
|
|
3690
|
+
else if (deps['@playwright/test'])
|
|
3691
|
+
testFramework = 'playwright';
|
|
3692
|
+
else if (deps['vitest'])
|
|
3693
|
+
testFramework = 'vitest';
|
|
3694
|
+
}
|
|
3695
|
+
}
|
|
3696
|
+
catch {
|
|
3697
|
+
// Use default
|
|
3698
|
+
}
|
|
3699
|
+
let response = `# ๐งช Test Stub Generator\n\n`;
|
|
3700
|
+
if (file) {
|
|
3701
|
+
// Generate tests for a specific file
|
|
3702
|
+
const filePath = path.isAbsolute(file) ? file : path.join(cwd, file);
|
|
3703
|
+
if (!fs.existsSync(filePath)) {
|
|
3704
|
+
return {
|
|
3705
|
+
content: [{
|
|
3706
|
+
type: 'text',
|
|
3707
|
+
text: `โ File not found: ${file}`,
|
|
3708
|
+
}],
|
|
3709
|
+
isError: true,
|
|
3710
|
+
};
|
|
3711
|
+
}
|
|
3712
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
3713
|
+
const fileName = path.basename(file);
|
|
3714
|
+
const fileExt = path.extname(file);
|
|
3715
|
+
const isApiRoute = file.includes('/api/') || file.includes('\\api\\');
|
|
3716
|
+
const isComponent = fileExt === '.tsx' && !isApiRoute;
|
|
3717
|
+
const isService = file.includes('/services/') || file.includes('\\services\\') || file.includes('/lib/') || file.includes('\\lib\\');
|
|
3718
|
+
// Detect exported functions/components
|
|
3719
|
+
const exportedItems = [];
|
|
3720
|
+
const exportDefaultMatch = content.match(/export\s+default\s+(function\s+)?(\w+)/);
|
|
3721
|
+
const exportMatches = content.matchAll(/export\s+(async\s+)?(function|const)\s+(\w+)/g);
|
|
3722
|
+
if (exportDefaultMatch && exportDefaultMatch[2]) {
|
|
3723
|
+
exportedItems.push(exportDefaultMatch[2]);
|
|
3724
|
+
}
|
|
3725
|
+
for (const match of exportMatches) {
|
|
3726
|
+
if (match[3])
|
|
3727
|
+
exportedItems.push(match[3]);
|
|
3728
|
+
}
|
|
3729
|
+
// HTTP methods for API routes
|
|
3730
|
+
const httpMethods = [];
|
|
3731
|
+
if (isApiRoute) {
|
|
3732
|
+
if (content.includes('export async function GET') || content.includes('export function GET'))
|
|
3733
|
+
httpMethods.push('GET');
|
|
3734
|
+
if (content.includes('export async function POST') || content.includes('export function POST'))
|
|
3735
|
+
httpMethods.push('POST');
|
|
3736
|
+
if (content.includes('export async function PUT') || content.includes('export function PUT'))
|
|
3737
|
+
httpMethods.push('PUT');
|
|
3738
|
+
if (content.includes('export async function PATCH') || content.includes('export function PATCH'))
|
|
3739
|
+
httpMethods.push('PATCH');
|
|
3740
|
+
if (content.includes('export async function DELETE') || content.includes('export function DELETE'))
|
|
3741
|
+
httpMethods.push('DELETE');
|
|
3742
|
+
}
|
|
3743
|
+
response += `**Source:** \`${file}\`\n`;
|
|
3744
|
+
response += `**Type:** ${isApiRoute ? 'API Route' : isComponent ? 'React Component' : isService ? 'Service/Utility' : 'Module'}\n`;
|
|
3745
|
+
response += `**Test Framework:** ${testFramework}\n\n`;
|
|
3746
|
+
// Determine test file path
|
|
3747
|
+
const testFileName = fileName.replace(/\.(ts|tsx)$/, '.test$1');
|
|
3748
|
+
let testFilePath;
|
|
3749
|
+
if (isApiRoute) {
|
|
3750
|
+
// API routes: tests/api/[route].test.ts
|
|
3751
|
+
const routePath = file.replace(/.*\/api\//, '').replace(/\/route\.(ts|tsx)$/, '').replace(/\\/g, '/');
|
|
3752
|
+
testFilePath = `tests/api/${routePath}.test.ts`;
|
|
3753
|
+
}
|
|
3754
|
+
else if (isComponent) {
|
|
3755
|
+
// Components: alongside the file
|
|
3756
|
+
testFilePath = file.replace(/\.(tsx)$/, '.test.tsx');
|
|
3757
|
+
}
|
|
3758
|
+
else {
|
|
3759
|
+
// Services/utils: tests/services/
|
|
3760
|
+
testFilePath = `tests/${fileName.replace(/\.(ts|tsx)$/, '.test.ts')}`;
|
|
3761
|
+
}
|
|
3762
|
+
response += `**Test File:** \`${testFilePath}\`\n\n`;
|
|
3763
|
+
response += `---\n\n`;
|
|
3764
|
+
// Generate test stub based on file type
|
|
3765
|
+
if (isApiRoute && httpMethods.length > 0) {
|
|
3766
|
+
response += `## API Route Test Stub\n\n`;
|
|
3767
|
+
response += '```typescript\n';
|
|
3768
|
+
response += `import { describe, it, expect, beforeEach } from '${testFramework}';\n\n`;
|
|
3769
|
+
response += `describe('${file.replace(/.*\/api\//, '/api/').replace(/\/route\.(ts|tsx)$/, '')}', () => {\n`;
|
|
3770
|
+
for (const method of httpMethods) {
|
|
3771
|
+
response += ` describe('${method}', () => {\n`;
|
|
3772
|
+
response += ` it('should handle successful request', async () => {\n`;
|
|
3773
|
+
response += ` // Arrange: Set up test data\n`;
|
|
3774
|
+
response += ` const request = new Request('http://localhost/api/...', {\n`;
|
|
3775
|
+
response += ` method: '${method}',\n`;
|
|
3776
|
+
if (method !== 'GET' && method !== 'DELETE') {
|
|
3777
|
+
response += ` body: JSON.stringify({ /* test data */ }),\n`;
|
|
3778
|
+
response += ` headers: { 'Content-Type': 'application/json' },\n`;
|
|
3779
|
+
}
|
|
3780
|
+
response += ` });\n\n`;
|
|
3781
|
+
response += ` // Act: Call the handler\n`;
|
|
3782
|
+
response += ` // const response = await ${method}(request);\n`;
|
|
3783
|
+
response += ` // const data = await response.json();\n\n`;
|
|
3784
|
+
response += ` // Assert: Check response\n`;
|
|
3785
|
+
response += ` // expect(response.status).toBe(200);\n`;
|
|
3786
|
+
response += ` // expect(data).toMatchObject({ /* expected */ });\n`;
|
|
3787
|
+
response += ` });\n\n`;
|
|
3788
|
+
response += ` it('should handle validation errors', async () => {\n`;
|
|
3789
|
+
response += ` // Test with invalid input\n`;
|
|
3790
|
+
response += ` // expect(response.status).toBe(400);\n`;
|
|
3791
|
+
response += ` });\n\n`;
|
|
3792
|
+
response += ` it('should handle unauthorized access', async () => {\n`;
|
|
3793
|
+
response += ` // Test without auth\n`;
|
|
3794
|
+
response += ` // expect(response.status).toBe(401);\n`;
|
|
3795
|
+
response += ` });\n`;
|
|
3796
|
+
response += ` });\n\n`;
|
|
3797
|
+
}
|
|
3798
|
+
response += `});\n`;
|
|
3799
|
+
response += '```\n\n';
|
|
3800
|
+
}
|
|
3801
|
+
else if (isComponent) {
|
|
3802
|
+
const componentName = exportedItems[0] || fileName.replace(/\.(tsx)$/, '');
|
|
3803
|
+
response += `## Component Test Stub\n\n`;
|
|
3804
|
+
response += '```typescript\n';
|
|
3805
|
+
response += `import { render, screen, fireEvent } from '@testing-library/react';\n`;
|
|
3806
|
+
response += `import { describe, it, expect } from '${testFramework}';\n`;
|
|
3807
|
+
response += `import { ${componentName} } from './${fileName.replace(/\.tsx$/, '')}';\n\n`;
|
|
3808
|
+
response += `describe('${componentName}', () => {\n`;
|
|
3809
|
+
response += ` it('renders correctly', () => {\n`;
|
|
3810
|
+
response += ` render(<${componentName} />);\n`;
|
|
3811
|
+
response += ` // expect(screen.getByRole('...')).toBeInTheDocument();\n`;
|
|
3812
|
+
response += ` });\n\n`;
|
|
3813
|
+
response += ` it('handles user interaction', async () => {\n`;
|
|
3814
|
+
response += ` render(<${componentName} />);\n`;
|
|
3815
|
+
response += ` // const button = screen.getByRole('button', { name: /.../ });\n`;
|
|
3816
|
+
response += ` // await fireEvent.click(button);\n`;
|
|
3817
|
+
response += ` // expect(screen.getByText('...')).toBeInTheDocument();\n`;
|
|
3818
|
+
response += ` });\n\n`;
|
|
3819
|
+
response += ` it('displays loading state', () => {\n`;
|
|
3820
|
+
response += ` // Test loading state\n`;
|
|
3821
|
+
response += ` });\n\n`;
|
|
3822
|
+
response += ` it('handles errors gracefully', () => {\n`;
|
|
3823
|
+
response += ` // Test error state\n`;
|
|
3824
|
+
response += ` });\n`;
|
|
3825
|
+
response += `});\n`;
|
|
3826
|
+
response += '```\n\n';
|
|
3827
|
+
}
|
|
3828
|
+
else {
|
|
3829
|
+
// Generic function/service tests
|
|
3830
|
+
response += `## Unit Test Stub\n\n`;
|
|
3831
|
+
response += '```typescript\n';
|
|
3832
|
+
response += `import { describe, it, expect, vi } from '${testFramework}';\n`;
|
|
3833
|
+
if (exportedItems.length > 0) {
|
|
3834
|
+
response += `import { ${exportedItems.join(', ')} } from '${file.replace(/\.(ts|tsx)$/, '')}';\n`;
|
|
3835
|
+
}
|
|
3836
|
+
response += `\n`;
|
|
3837
|
+
response += `describe('${fileName.replace(/\.(ts|tsx)$/, '')}', () => {\n`;
|
|
3838
|
+
for (const item of exportedItems.slice(0, 5)) { // Limit to first 5
|
|
3839
|
+
response += ` describe('${item}', () => {\n`;
|
|
3840
|
+
response += ` it('should work correctly with valid input', async () => {\n`;
|
|
3841
|
+
response += ` // Arrange\n`;
|
|
3842
|
+
response += ` const input = { /* test data */ };\n\n`;
|
|
3843
|
+
response += ` // Act\n`;
|
|
3844
|
+
response += ` // const result = await ${item}(input);\n\n`;
|
|
3845
|
+
response += ` // Assert\n`;
|
|
3846
|
+
response += ` // expect(result).toBe(/* expected */);\n`;
|
|
3847
|
+
response += ` });\n\n`;
|
|
3848
|
+
response += ` it('should handle edge cases', async () => {\n`;
|
|
3849
|
+
response += ` // Test with empty/null/undefined inputs\n`;
|
|
3850
|
+
response += ` });\n\n`;
|
|
3851
|
+
response += ` it('should throw on invalid input', async () => {\n`;
|
|
3852
|
+
response += ` // expect(() => ${item}(invalid)).toThrow();\n`;
|
|
3853
|
+
response += ` });\n`;
|
|
3854
|
+
response += ` });\n\n`;
|
|
3855
|
+
}
|
|
3856
|
+
response += `});\n`;
|
|
3857
|
+
response += '```\n\n';
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
else if (feature) {
|
|
3861
|
+
// Generate feature-based test structure
|
|
3862
|
+
response += `**Feature:** ${feature}\n`;
|
|
3863
|
+
response += `**Test Framework:** ${testFramework}\n\n`;
|
|
3864
|
+
response += `---\n\n`;
|
|
3865
|
+
response += `## Feature Test Structure\n\n`;
|
|
3866
|
+
response += `For feature "${feature}", create the following test files:\n\n`;
|
|
3867
|
+
response += `### 1. Unit Tests (\`tests/unit/${feature.toLowerCase().replace(/\s+/g, '-')}.test.ts\`)\n\n`;
|
|
3868
|
+
response += '```typescript\n';
|
|
3869
|
+
response += `import { describe, it, expect } from '${testFramework}';\n\n`;
|
|
3870
|
+
response += `describe('${feature} - Unit Tests', () => {\n`;
|
|
3871
|
+
response += ` describe('Core Logic', () => {\n`;
|
|
3872
|
+
response += ` it('should handle happy path', () => {\n`;
|
|
3873
|
+
response += ` // Test the main success scenario\n`;
|
|
3874
|
+
response += ` });\n\n`;
|
|
3875
|
+
response += ` it('should validate input', () => {\n`;
|
|
3876
|
+
response += ` // Test input validation\n`;
|
|
3877
|
+
response += ` });\n\n`;
|
|
3878
|
+
response += ` it('should handle errors', () => {\n`;
|
|
3879
|
+
response += ` // Test error handling\n`;
|
|
3880
|
+
response += ` });\n`;
|
|
3881
|
+
response += ` });\n`;
|
|
3882
|
+
response += `});\n`;
|
|
3883
|
+
response += '```\n\n';
|
|
3884
|
+
response += `### 2. Integration Tests (\`tests/integration/${feature.toLowerCase().replace(/\s+/g, '-')}.test.ts\`)\n\n`;
|
|
3885
|
+
response += '```typescript\n';
|
|
3886
|
+
response += `import { describe, it, expect, beforeAll, afterAll } from '${testFramework}';\n\n`;
|
|
3887
|
+
response += `describe('${feature} - Integration Tests', () => {\n`;
|
|
3888
|
+
response += ` beforeAll(async () => {\n`;
|
|
3889
|
+
response += ` // Set up test database, mock services, etc.\n`;
|
|
3890
|
+
response += ` });\n\n`;
|
|
3891
|
+
response += ` afterAll(async () => {\n`;
|
|
3892
|
+
response += ` // Clean up\n`;
|
|
3893
|
+
response += ` });\n\n`;
|
|
3894
|
+
response += ` it('should complete the full flow', async () => {\n`;
|
|
3895
|
+
response += ` // Test the complete feature flow\n`;
|
|
3896
|
+
response += ` });\n`;
|
|
3897
|
+
response += `});\n`;
|
|
3898
|
+
response += '```\n\n';
|
|
3899
|
+
if (testType === 'e2e' || testFramework === 'playwright') {
|
|
3900
|
+
response += `### 3. E2E Tests (\`e2e/${feature.toLowerCase().replace(/\s+/g, '-')}.spec.ts\`)\n\n`;
|
|
3901
|
+
response += '```typescript\n';
|
|
3902
|
+
response += `import { test, expect } from '@playwright/test';\n\n`;
|
|
3903
|
+
response += `test.describe('${feature}', () => {\n`;
|
|
3904
|
+
response += ` test('user can complete the flow', async ({ page }) => {\n`;
|
|
3905
|
+
response += ` // Navigate to the feature\n`;
|
|
3906
|
+
response += ` await page.goto('/...');\n\n`;
|
|
3907
|
+
response += ` // Interact with the UI\n`;
|
|
3908
|
+
response += ` // await page.click('button');\n`;
|
|
3909
|
+
response += ` // await page.fill('input', 'value');\n\n`;
|
|
3910
|
+
response += ` // Verify the result\n`;
|
|
3911
|
+
response += ` // await expect(page.getByText('...')).toBeVisible();\n`;
|
|
3912
|
+
response += ` });\n`;
|
|
3913
|
+
response += `});\n`;
|
|
3914
|
+
response += '```\n\n';
|
|
3915
|
+
}
|
|
3916
|
+
}
|
|
3917
|
+
response += `---\n\n`;
|
|
3918
|
+
response += `**Next Steps:**\n`;
|
|
3919
|
+
response += `1. Create the test file at the suggested path\n`;
|
|
3920
|
+
response += `2. Uncomment and fill in the test implementations\n`;
|
|
3921
|
+
response += `3. Run tests: \`npm test\`\n`;
|
|
3922
|
+
return {
|
|
3923
|
+
content: [{
|
|
3924
|
+
type: 'text',
|
|
3925
|
+
text: response,
|
|
3926
|
+
}],
|
|
3927
|
+
};
|
|
3928
|
+
}
|
|
3632
3929
|
/**
|
|
3633
3930
|
* MANDATORY: Validate that a feature is complete before AI can say "done" (v6.0 Server-Side)
|
|
3634
3931
|
* Runs local checks (tests, TypeScript), then validates with server
|
|
3635
3932
|
*/
|
|
3636
3933
|
async handleValidateComplete(args) {
|
|
3637
|
-
const { feature, files = [] } = args;
|
|
3934
|
+
const { feature, files = [], envVarsAdded = [], schemaModified: schemaModifiedArg } = args;
|
|
3638
3935
|
const cwd = process.cwd();
|
|
3639
3936
|
let testsExist = false;
|
|
3640
3937
|
let testsPass = false;
|
|
3641
3938
|
let typescriptPass = false;
|
|
3642
3939
|
const testsWritten = [];
|
|
3940
|
+
// v3.9.17: Auto-detect schema modifications if not explicitly provided
|
|
3941
|
+
let schemaModified = schemaModifiedArg;
|
|
3942
|
+
if (schemaModified === undefined) {
|
|
3943
|
+
// Check if schema file was in the modified files list
|
|
3944
|
+
schemaModified = files.some(f => f.includes('schema.ts') ||
|
|
3945
|
+
f.includes('schema/') ||
|
|
3946
|
+
f.includes('db/schema') ||
|
|
3947
|
+
f.includes('drizzle/'));
|
|
3948
|
+
}
|
|
3643
3949
|
// v6.1: Code analysis for compliance scoring
|
|
3644
3950
|
const codeAnalysis = {};
|
|
3645
3951
|
// Step 1: Get session token (from memory or state file)
|
|
@@ -3817,6 +4123,9 @@ Just describe what you want to build! I'll automatically:
|
|
|
3817
4123
|
testsPassed: testsPass,
|
|
3818
4124
|
typescriptPassed: typescriptPass,
|
|
3819
4125
|
codeAnalysis, // v6.1: Send code analysis for compliance scoring
|
|
4126
|
+
// v3.9.17: Environment and schema validation
|
|
4127
|
+
envVarsAdded: envVarsAdded.length > 0 ? envVarsAdded : undefined,
|
|
4128
|
+
schemaModified,
|
|
3820
4129
|
}),
|
|
3821
4130
|
});
|
|
3822
4131
|
const result = await response.json();
|
package/package.json
CHANGED
package/src/mcp/server.ts
CHANGED
|
@@ -878,6 +878,29 @@ class CodeBakersServer {
|
|
|
878
878
|
},
|
|
879
879
|
},
|
|
880
880
|
},
|
|
881
|
+
{
|
|
882
|
+
name: 'generate_tests',
|
|
883
|
+
description:
|
|
884
|
+
'Generate test stubs for a file or feature. Creates a test file with happy path and error case templates based on the source code. Reduces friction for adding tests. Use when user needs help writing tests.',
|
|
885
|
+
inputSchema: {
|
|
886
|
+
type: 'object' as const,
|
|
887
|
+
properties: {
|
|
888
|
+
file: {
|
|
889
|
+
type: 'string',
|
|
890
|
+
description: 'Source file to generate tests for (e.g., "src/components/LoginForm.tsx", "src/app/api/users/route.ts")',
|
|
891
|
+
},
|
|
892
|
+
feature: {
|
|
893
|
+
type: 'string',
|
|
894
|
+
description: 'Feature name if generating tests for a feature rather than a specific file',
|
|
895
|
+
},
|
|
896
|
+
testType: {
|
|
897
|
+
type: 'string',
|
|
898
|
+
enum: ['unit', 'integration', 'e2e'],
|
|
899
|
+
description: 'Type of test to generate (default: unit for components/functions, integration for API routes)',
|
|
900
|
+
},
|
|
901
|
+
},
|
|
902
|
+
},
|
|
903
|
+
},
|
|
881
904
|
{
|
|
882
905
|
name: 'validate_complete',
|
|
883
906
|
description:
|
|
@@ -894,6 +917,15 @@ class CodeBakersServer {
|
|
|
894
917
|
items: { type: 'string' },
|
|
895
918
|
description: 'Files that were created/modified for this feature',
|
|
896
919
|
},
|
|
920
|
+
envVarsAdded: {
|
|
921
|
+
type: 'array',
|
|
922
|
+
items: { type: 'string' },
|
|
923
|
+
description: 'New environment variables added during implementation (e.g., ["PAYPAL_CLIENT_ID", "PAYPAL_SECRET"])',
|
|
924
|
+
},
|
|
925
|
+
schemaModified: {
|
|
926
|
+
type: 'boolean',
|
|
927
|
+
description: 'Set to true if database schema (db/schema.ts) was modified',
|
|
928
|
+
},
|
|
897
929
|
},
|
|
898
930
|
required: ['feature'],
|
|
899
931
|
},
|
|
@@ -1659,8 +1691,11 @@ class CodeBakersServer {
|
|
|
1659
1691
|
case 'run_tests':
|
|
1660
1692
|
return this.handleRunTests(args as { filter?: string; watch?: boolean });
|
|
1661
1693
|
|
|
1694
|
+
case 'generate_tests':
|
|
1695
|
+
return this.handleGenerateTests(args as { file?: string; feature?: string; testType?: 'unit' | 'integration' | 'e2e' });
|
|
1696
|
+
|
|
1662
1697
|
case 'validate_complete':
|
|
1663
|
-
return this.handleValidateComplete(args as { feature: string; files?: string[] });
|
|
1698
|
+
return this.handleValidateComplete(args as { feature: string; files?: string[]; envVarsAdded?: string[]; schemaModified?: boolean });
|
|
1664
1699
|
|
|
1665
1700
|
case 'discover_patterns':
|
|
1666
1701
|
return this.handleDiscoverPatterns(args as { task: string; files?: string[]; keywords?: string[] });
|
|
@@ -4079,18 +4114,305 @@ Just describe what you want to build! I'll automatically:
|
|
|
4079
4114
|
};
|
|
4080
4115
|
}
|
|
4081
4116
|
|
|
4117
|
+
/**
|
|
4118
|
+
* Generate test stubs for a file or feature
|
|
4119
|
+
* Analyzes source code and creates appropriate test templates
|
|
4120
|
+
*/
|
|
4121
|
+
private handleGenerateTests(args: { file?: string; feature?: string; testType?: 'unit' | 'integration' | 'e2e' }) {
|
|
4122
|
+
const { file, feature, testType } = args;
|
|
4123
|
+
const cwd = process.cwd();
|
|
4124
|
+
|
|
4125
|
+
if (!file && !feature) {
|
|
4126
|
+
return {
|
|
4127
|
+
content: [{
|
|
4128
|
+
type: 'text' as const,
|
|
4129
|
+
text: 'โ Please provide either a file path or feature name to generate tests for.',
|
|
4130
|
+
}],
|
|
4131
|
+
isError: true,
|
|
4132
|
+
};
|
|
4133
|
+
}
|
|
4134
|
+
|
|
4135
|
+
// Detect test framework
|
|
4136
|
+
let testFramework = 'vitest';
|
|
4137
|
+
try {
|
|
4138
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
4139
|
+
if (fs.existsSync(pkgPath)) {
|
|
4140
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
4141
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
4142
|
+
if (deps['jest']) testFramework = 'jest';
|
|
4143
|
+
else if (deps['@playwright/test']) testFramework = 'playwright';
|
|
4144
|
+
else if (deps['vitest']) testFramework = 'vitest';
|
|
4145
|
+
}
|
|
4146
|
+
} catch {
|
|
4147
|
+
// Use default
|
|
4148
|
+
}
|
|
4149
|
+
|
|
4150
|
+
let response = `# ๐งช Test Stub Generator\n\n`;
|
|
4151
|
+
|
|
4152
|
+
if (file) {
|
|
4153
|
+
// Generate tests for a specific file
|
|
4154
|
+
const filePath = path.isAbsolute(file) ? file : path.join(cwd, file);
|
|
4155
|
+
|
|
4156
|
+
if (!fs.existsSync(filePath)) {
|
|
4157
|
+
return {
|
|
4158
|
+
content: [{
|
|
4159
|
+
type: 'text' as const,
|
|
4160
|
+
text: `โ File not found: ${file}`,
|
|
4161
|
+
}],
|
|
4162
|
+
isError: true,
|
|
4163
|
+
};
|
|
4164
|
+
}
|
|
4165
|
+
|
|
4166
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
4167
|
+
const fileName = path.basename(file);
|
|
4168
|
+
const fileExt = path.extname(file);
|
|
4169
|
+
const isApiRoute = file.includes('/api/') || file.includes('\\api\\');
|
|
4170
|
+
const isComponent = fileExt === '.tsx' && !isApiRoute;
|
|
4171
|
+
const isService = file.includes('/services/') || file.includes('\\services\\') || file.includes('/lib/') || file.includes('\\lib\\');
|
|
4172
|
+
|
|
4173
|
+
// Detect exported functions/components
|
|
4174
|
+
const exportedItems: string[] = [];
|
|
4175
|
+
const exportDefaultMatch = content.match(/export\s+default\s+(function\s+)?(\w+)/);
|
|
4176
|
+
const exportMatches = content.matchAll(/export\s+(async\s+)?(function|const)\s+(\w+)/g);
|
|
4177
|
+
|
|
4178
|
+
if (exportDefaultMatch && exportDefaultMatch[2]) {
|
|
4179
|
+
exportedItems.push(exportDefaultMatch[2]);
|
|
4180
|
+
}
|
|
4181
|
+
for (const match of exportMatches) {
|
|
4182
|
+
if (match[3]) exportedItems.push(match[3]);
|
|
4183
|
+
}
|
|
4184
|
+
|
|
4185
|
+
// HTTP methods for API routes
|
|
4186
|
+
const httpMethods: string[] = [];
|
|
4187
|
+
if (isApiRoute) {
|
|
4188
|
+
if (content.includes('export async function GET') || content.includes('export function GET')) httpMethods.push('GET');
|
|
4189
|
+
if (content.includes('export async function POST') || content.includes('export function POST')) httpMethods.push('POST');
|
|
4190
|
+
if (content.includes('export async function PUT') || content.includes('export function PUT')) httpMethods.push('PUT');
|
|
4191
|
+
if (content.includes('export async function PATCH') || content.includes('export function PATCH')) httpMethods.push('PATCH');
|
|
4192
|
+
if (content.includes('export async function DELETE') || content.includes('export function DELETE')) httpMethods.push('DELETE');
|
|
4193
|
+
}
|
|
4194
|
+
|
|
4195
|
+
response += `**Source:** \`${file}\`\n`;
|
|
4196
|
+
response += `**Type:** ${isApiRoute ? 'API Route' : isComponent ? 'React Component' : isService ? 'Service/Utility' : 'Module'}\n`;
|
|
4197
|
+
response += `**Test Framework:** ${testFramework}\n\n`;
|
|
4198
|
+
|
|
4199
|
+
// Determine test file path
|
|
4200
|
+
const testFileName = fileName.replace(/\.(ts|tsx)$/, '.test$1');
|
|
4201
|
+
let testFilePath: string;
|
|
4202
|
+
|
|
4203
|
+
if (isApiRoute) {
|
|
4204
|
+
// API routes: tests/api/[route].test.ts
|
|
4205
|
+
const routePath = file.replace(/.*\/api\//, '').replace(/\/route\.(ts|tsx)$/, '').replace(/\\/g, '/');
|
|
4206
|
+
testFilePath = `tests/api/${routePath}.test.ts`;
|
|
4207
|
+
} else if (isComponent) {
|
|
4208
|
+
// Components: alongside the file
|
|
4209
|
+
testFilePath = file.replace(/\.(tsx)$/, '.test.tsx');
|
|
4210
|
+
} else {
|
|
4211
|
+
// Services/utils: tests/services/
|
|
4212
|
+
testFilePath = `tests/${fileName.replace(/\.(ts|tsx)$/, '.test.ts')}`;
|
|
4213
|
+
}
|
|
4214
|
+
|
|
4215
|
+
response += `**Test File:** \`${testFilePath}\`\n\n`;
|
|
4216
|
+
response += `---\n\n`;
|
|
4217
|
+
|
|
4218
|
+
// Generate test stub based on file type
|
|
4219
|
+
if (isApiRoute && httpMethods.length > 0) {
|
|
4220
|
+
response += `## API Route Test Stub\n\n`;
|
|
4221
|
+
response += '```typescript\n';
|
|
4222
|
+
response += `import { describe, it, expect, beforeEach } from '${testFramework}';\n\n`;
|
|
4223
|
+
response += `describe('${file.replace(/.*\/api\//, '/api/').replace(/\/route\.(ts|tsx)$/, '')}', () => {\n`;
|
|
4224
|
+
|
|
4225
|
+
for (const method of httpMethods) {
|
|
4226
|
+
response += ` describe('${method}', () => {\n`;
|
|
4227
|
+
response += ` it('should handle successful request', async () => {\n`;
|
|
4228
|
+
response += ` // Arrange: Set up test data\n`;
|
|
4229
|
+
response += ` const request = new Request('http://localhost/api/...', {\n`;
|
|
4230
|
+
response += ` method: '${method}',\n`;
|
|
4231
|
+
if (method !== 'GET' && method !== 'DELETE') {
|
|
4232
|
+
response += ` body: JSON.stringify({ /* test data */ }),\n`;
|
|
4233
|
+
response += ` headers: { 'Content-Type': 'application/json' },\n`;
|
|
4234
|
+
}
|
|
4235
|
+
response += ` });\n\n`;
|
|
4236
|
+
response += ` // Act: Call the handler\n`;
|
|
4237
|
+
response += ` // const response = await ${method}(request);\n`;
|
|
4238
|
+
response += ` // const data = await response.json();\n\n`;
|
|
4239
|
+
response += ` // Assert: Check response\n`;
|
|
4240
|
+
response += ` // expect(response.status).toBe(200);\n`;
|
|
4241
|
+
response += ` // expect(data).toMatchObject({ /* expected */ });\n`;
|
|
4242
|
+
response += ` });\n\n`;
|
|
4243
|
+
response += ` it('should handle validation errors', async () => {\n`;
|
|
4244
|
+
response += ` // Test with invalid input\n`;
|
|
4245
|
+
response += ` // expect(response.status).toBe(400);\n`;
|
|
4246
|
+
response += ` });\n\n`;
|
|
4247
|
+
response += ` it('should handle unauthorized access', async () => {\n`;
|
|
4248
|
+
response += ` // Test without auth\n`;
|
|
4249
|
+
response += ` // expect(response.status).toBe(401);\n`;
|
|
4250
|
+
response += ` });\n`;
|
|
4251
|
+
response += ` });\n\n`;
|
|
4252
|
+
}
|
|
4253
|
+
response += `});\n`;
|
|
4254
|
+
response += '```\n\n';
|
|
4255
|
+
|
|
4256
|
+
} else if (isComponent) {
|
|
4257
|
+
const componentName = exportedItems[0] || fileName.replace(/\.(tsx)$/, '');
|
|
4258
|
+
response += `## Component Test Stub\n\n`;
|
|
4259
|
+
response += '```typescript\n';
|
|
4260
|
+
response += `import { render, screen, fireEvent } from '@testing-library/react';\n`;
|
|
4261
|
+
response += `import { describe, it, expect } from '${testFramework}';\n`;
|
|
4262
|
+
response += `import { ${componentName} } from './${fileName.replace(/\.tsx$/, '')}';\n\n`;
|
|
4263
|
+
response += `describe('${componentName}', () => {\n`;
|
|
4264
|
+
response += ` it('renders correctly', () => {\n`;
|
|
4265
|
+
response += ` render(<${componentName} />);\n`;
|
|
4266
|
+
response += ` // expect(screen.getByRole('...')).toBeInTheDocument();\n`;
|
|
4267
|
+
response += ` });\n\n`;
|
|
4268
|
+
response += ` it('handles user interaction', async () => {\n`;
|
|
4269
|
+
response += ` render(<${componentName} />);\n`;
|
|
4270
|
+
response += ` // const button = screen.getByRole('button', { name: /.../ });\n`;
|
|
4271
|
+
response += ` // await fireEvent.click(button);\n`;
|
|
4272
|
+
response += ` // expect(screen.getByText('...')).toBeInTheDocument();\n`;
|
|
4273
|
+
response += ` });\n\n`;
|
|
4274
|
+
response += ` it('displays loading state', () => {\n`;
|
|
4275
|
+
response += ` // Test loading state\n`;
|
|
4276
|
+
response += ` });\n\n`;
|
|
4277
|
+
response += ` it('handles errors gracefully', () => {\n`;
|
|
4278
|
+
response += ` // Test error state\n`;
|
|
4279
|
+
response += ` });\n`;
|
|
4280
|
+
response += `});\n`;
|
|
4281
|
+
response += '```\n\n';
|
|
4282
|
+
|
|
4283
|
+
} else {
|
|
4284
|
+
// Generic function/service tests
|
|
4285
|
+
response += `## Unit Test Stub\n\n`;
|
|
4286
|
+
response += '```typescript\n';
|
|
4287
|
+
response += `import { describe, it, expect, vi } from '${testFramework}';\n`;
|
|
4288
|
+
if (exportedItems.length > 0) {
|
|
4289
|
+
response += `import { ${exportedItems.join(', ')} } from '${file.replace(/\.(ts|tsx)$/, '')}';\n`;
|
|
4290
|
+
}
|
|
4291
|
+
response += `\n`;
|
|
4292
|
+
response += `describe('${fileName.replace(/\.(ts|tsx)$/, '')}', () => {\n`;
|
|
4293
|
+
|
|
4294
|
+
for (const item of exportedItems.slice(0, 5)) { // Limit to first 5
|
|
4295
|
+
response += ` describe('${item}', () => {\n`;
|
|
4296
|
+
response += ` it('should work correctly with valid input', async () => {\n`;
|
|
4297
|
+
response += ` // Arrange\n`;
|
|
4298
|
+
response += ` const input = { /* test data */ };\n\n`;
|
|
4299
|
+
response += ` // Act\n`;
|
|
4300
|
+
response += ` // const result = await ${item}(input);\n\n`;
|
|
4301
|
+
response += ` // Assert\n`;
|
|
4302
|
+
response += ` // expect(result).toBe(/* expected */);\n`;
|
|
4303
|
+
response += ` });\n\n`;
|
|
4304
|
+
response += ` it('should handle edge cases', async () => {\n`;
|
|
4305
|
+
response += ` // Test with empty/null/undefined inputs\n`;
|
|
4306
|
+
response += ` });\n\n`;
|
|
4307
|
+
response += ` it('should throw on invalid input', async () => {\n`;
|
|
4308
|
+
response += ` // expect(() => ${item}(invalid)).toThrow();\n`;
|
|
4309
|
+
response += ` });\n`;
|
|
4310
|
+
response += ` });\n\n`;
|
|
4311
|
+
}
|
|
4312
|
+
response += `});\n`;
|
|
4313
|
+
response += '```\n\n';
|
|
4314
|
+
}
|
|
4315
|
+
|
|
4316
|
+
} else if (feature) {
|
|
4317
|
+
// Generate feature-based test structure
|
|
4318
|
+
response += `**Feature:** ${feature}\n`;
|
|
4319
|
+
response += `**Test Framework:** ${testFramework}\n\n`;
|
|
4320
|
+
response += `---\n\n`;
|
|
4321
|
+
|
|
4322
|
+
response += `## Feature Test Structure\n\n`;
|
|
4323
|
+
response += `For feature "${feature}", create the following test files:\n\n`;
|
|
4324
|
+
|
|
4325
|
+
response += `### 1. Unit Tests (\`tests/unit/${feature.toLowerCase().replace(/\s+/g, '-')}.test.ts\`)\n\n`;
|
|
4326
|
+
response += '```typescript\n';
|
|
4327
|
+
response += `import { describe, it, expect } from '${testFramework}';\n\n`;
|
|
4328
|
+
response += `describe('${feature} - Unit Tests', () => {\n`;
|
|
4329
|
+
response += ` describe('Core Logic', () => {\n`;
|
|
4330
|
+
response += ` it('should handle happy path', () => {\n`;
|
|
4331
|
+
response += ` // Test the main success scenario\n`;
|
|
4332
|
+
response += ` });\n\n`;
|
|
4333
|
+
response += ` it('should validate input', () => {\n`;
|
|
4334
|
+
response += ` // Test input validation\n`;
|
|
4335
|
+
response += ` });\n\n`;
|
|
4336
|
+
response += ` it('should handle errors', () => {\n`;
|
|
4337
|
+
response += ` // Test error handling\n`;
|
|
4338
|
+
response += ` });\n`;
|
|
4339
|
+
response += ` });\n`;
|
|
4340
|
+
response += `});\n`;
|
|
4341
|
+
response += '```\n\n';
|
|
4342
|
+
|
|
4343
|
+
response += `### 2. Integration Tests (\`tests/integration/${feature.toLowerCase().replace(/\s+/g, '-')}.test.ts\`)\n\n`;
|
|
4344
|
+
response += '```typescript\n';
|
|
4345
|
+
response += `import { describe, it, expect, beforeAll, afterAll } from '${testFramework}';\n\n`;
|
|
4346
|
+
response += `describe('${feature} - Integration Tests', () => {\n`;
|
|
4347
|
+
response += ` beforeAll(async () => {\n`;
|
|
4348
|
+
response += ` // Set up test database, mock services, etc.\n`;
|
|
4349
|
+
response += ` });\n\n`;
|
|
4350
|
+
response += ` afterAll(async () => {\n`;
|
|
4351
|
+
response += ` // Clean up\n`;
|
|
4352
|
+
response += ` });\n\n`;
|
|
4353
|
+
response += ` it('should complete the full flow', async () => {\n`;
|
|
4354
|
+
response += ` // Test the complete feature flow\n`;
|
|
4355
|
+
response += ` });\n`;
|
|
4356
|
+
response += `});\n`;
|
|
4357
|
+
response += '```\n\n';
|
|
4358
|
+
|
|
4359
|
+
if (testType === 'e2e' || testFramework === 'playwright') {
|
|
4360
|
+
response += `### 3. E2E Tests (\`e2e/${feature.toLowerCase().replace(/\s+/g, '-')}.spec.ts\`)\n\n`;
|
|
4361
|
+
response += '```typescript\n';
|
|
4362
|
+
response += `import { test, expect } from '@playwright/test';\n\n`;
|
|
4363
|
+
response += `test.describe('${feature}', () => {\n`;
|
|
4364
|
+
response += ` test('user can complete the flow', async ({ page }) => {\n`;
|
|
4365
|
+
response += ` // Navigate to the feature\n`;
|
|
4366
|
+
response += ` await page.goto('/...');\n\n`;
|
|
4367
|
+
response += ` // Interact with the UI\n`;
|
|
4368
|
+
response += ` // await page.click('button');\n`;
|
|
4369
|
+
response += ` // await page.fill('input', 'value');\n\n`;
|
|
4370
|
+
response += ` // Verify the result\n`;
|
|
4371
|
+
response += ` // await expect(page.getByText('...')).toBeVisible();\n`;
|
|
4372
|
+
response += ` });\n`;
|
|
4373
|
+
response += `});\n`;
|
|
4374
|
+
response += '```\n\n';
|
|
4375
|
+
}
|
|
4376
|
+
}
|
|
4377
|
+
|
|
4378
|
+
response += `---\n\n`;
|
|
4379
|
+
response += `**Next Steps:**\n`;
|
|
4380
|
+
response += `1. Create the test file at the suggested path\n`;
|
|
4381
|
+
response += `2. Uncomment and fill in the test implementations\n`;
|
|
4382
|
+
response += `3. Run tests: \`npm test\`\n`;
|
|
4383
|
+
|
|
4384
|
+
return {
|
|
4385
|
+
content: [{
|
|
4386
|
+
type: 'text' as const,
|
|
4387
|
+
text: response,
|
|
4388
|
+
}],
|
|
4389
|
+
};
|
|
4390
|
+
}
|
|
4391
|
+
|
|
4082
4392
|
/**
|
|
4083
4393
|
* MANDATORY: Validate that a feature is complete before AI can say "done" (v6.0 Server-Side)
|
|
4084
4394
|
* Runs local checks (tests, TypeScript), then validates with server
|
|
4085
4395
|
*/
|
|
4086
|
-
private async handleValidateComplete(args: { feature: string; files?: string[] }) {
|
|
4087
|
-
const { feature, files = [] } = args;
|
|
4396
|
+
private async handleValidateComplete(args: { feature: string; files?: string[]; envVarsAdded?: string[]; schemaModified?: boolean }) {
|
|
4397
|
+
const { feature, files = [], envVarsAdded = [], schemaModified: schemaModifiedArg } = args;
|
|
4088
4398
|
const cwd = process.cwd();
|
|
4089
4399
|
let testsExist = false;
|
|
4090
4400
|
let testsPass = false;
|
|
4091
4401
|
let typescriptPass = false;
|
|
4092
4402
|
const testsWritten: string[] = [];
|
|
4093
4403
|
|
|
4404
|
+
// v3.9.17: Auto-detect schema modifications if not explicitly provided
|
|
4405
|
+
let schemaModified = schemaModifiedArg;
|
|
4406
|
+
if (schemaModified === undefined) {
|
|
4407
|
+
// Check if schema file was in the modified files list
|
|
4408
|
+
schemaModified = files.some(f =>
|
|
4409
|
+
f.includes('schema.ts') ||
|
|
4410
|
+
f.includes('schema/') ||
|
|
4411
|
+
f.includes('db/schema') ||
|
|
4412
|
+
f.includes('drizzle/')
|
|
4413
|
+
);
|
|
4414
|
+
}
|
|
4415
|
+
|
|
4094
4416
|
// v6.1: Code analysis for compliance scoring
|
|
4095
4417
|
const codeAnalysis: {
|
|
4096
4418
|
hasErrorHandling?: boolean;
|
|
@@ -4283,6 +4605,9 @@ Just describe what you want to build! I'll automatically:
|
|
|
4283
4605
|
testsPassed: testsPass,
|
|
4284
4606
|
typescriptPassed: typescriptPass,
|
|
4285
4607
|
codeAnalysis, // v6.1: Send code analysis for compliance scoring
|
|
4608
|
+
// v3.9.17: Environment and schema validation
|
|
4609
|
+
envVarsAdded: envVarsAdded.length > 0 ? envVarsAdded : undefined,
|
|
4610
|
+
schemaModified,
|
|
4286
4611
|
}),
|
|
4287
4612
|
});
|
|
4288
4613
|
|