@codebakers/cli 3.9.15 โ 3.9.16
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 +288 -0
- package/package.json +1 -1
- package/src/mcp/server.ts +301 -0
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.',
|
|
@@ -1516,6 +1538,8 @@ class CodeBakersServer {
|
|
|
1516
1538
|
return this.handleProjectStatus();
|
|
1517
1539
|
case 'run_tests':
|
|
1518
1540
|
return this.handleRunTests(args);
|
|
1541
|
+
case 'generate_tests':
|
|
1542
|
+
return this.handleGenerateTests(args);
|
|
1519
1543
|
case 'validate_complete':
|
|
1520
1544
|
return this.handleValidateComplete(args);
|
|
1521
1545
|
case 'discover_patterns':
|
|
@@ -3629,6 +3653,270 @@ Just describe what you want to build! I'll automatically:
|
|
|
3629
3653
|
}],
|
|
3630
3654
|
};
|
|
3631
3655
|
}
|
|
3656
|
+
/**
|
|
3657
|
+
* Generate test stubs for a file or feature
|
|
3658
|
+
* Analyzes source code and creates appropriate test templates
|
|
3659
|
+
*/
|
|
3660
|
+
handleGenerateTests(args) {
|
|
3661
|
+
const { file, feature, testType } = args;
|
|
3662
|
+
const cwd = process.cwd();
|
|
3663
|
+
if (!file && !feature) {
|
|
3664
|
+
return {
|
|
3665
|
+
content: [{
|
|
3666
|
+
type: 'text',
|
|
3667
|
+
text: 'โ Please provide either a file path or feature name to generate tests for.',
|
|
3668
|
+
}],
|
|
3669
|
+
isError: true,
|
|
3670
|
+
};
|
|
3671
|
+
}
|
|
3672
|
+
// Detect test framework
|
|
3673
|
+
let testFramework = 'vitest';
|
|
3674
|
+
try {
|
|
3675
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
3676
|
+
if (fs.existsSync(pkgPath)) {
|
|
3677
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
3678
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
3679
|
+
if (deps['jest'])
|
|
3680
|
+
testFramework = 'jest';
|
|
3681
|
+
else if (deps['@playwright/test'])
|
|
3682
|
+
testFramework = 'playwright';
|
|
3683
|
+
else if (deps['vitest'])
|
|
3684
|
+
testFramework = 'vitest';
|
|
3685
|
+
}
|
|
3686
|
+
}
|
|
3687
|
+
catch {
|
|
3688
|
+
// Use default
|
|
3689
|
+
}
|
|
3690
|
+
let response = `# ๐งช Test Stub Generator\n\n`;
|
|
3691
|
+
if (file) {
|
|
3692
|
+
// Generate tests for a specific file
|
|
3693
|
+
const filePath = path.isAbsolute(file) ? file : path.join(cwd, file);
|
|
3694
|
+
if (!fs.existsSync(filePath)) {
|
|
3695
|
+
return {
|
|
3696
|
+
content: [{
|
|
3697
|
+
type: 'text',
|
|
3698
|
+
text: `โ File not found: ${file}`,
|
|
3699
|
+
}],
|
|
3700
|
+
isError: true,
|
|
3701
|
+
};
|
|
3702
|
+
}
|
|
3703
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
3704
|
+
const fileName = path.basename(file);
|
|
3705
|
+
const fileExt = path.extname(file);
|
|
3706
|
+
const isApiRoute = file.includes('/api/') || file.includes('\\api\\');
|
|
3707
|
+
const isComponent = fileExt === '.tsx' && !isApiRoute;
|
|
3708
|
+
const isService = file.includes('/services/') || file.includes('\\services\\') || file.includes('/lib/') || file.includes('\\lib\\');
|
|
3709
|
+
// Detect exported functions/components
|
|
3710
|
+
const exportedItems = [];
|
|
3711
|
+
const exportDefaultMatch = content.match(/export\s+default\s+(function\s+)?(\w+)/);
|
|
3712
|
+
const exportMatches = content.matchAll(/export\s+(async\s+)?(function|const)\s+(\w+)/g);
|
|
3713
|
+
if (exportDefaultMatch && exportDefaultMatch[2]) {
|
|
3714
|
+
exportedItems.push(exportDefaultMatch[2]);
|
|
3715
|
+
}
|
|
3716
|
+
for (const match of exportMatches) {
|
|
3717
|
+
if (match[3])
|
|
3718
|
+
exportedItems.push(match[3]);
|
|
3719
|
+
}
|
|
3720
|
+
// HTTP methods for API routes
|
|
3721
|
+
const httpMethods = [];
|
|
3722
|
+
if (isApiRoute) {
|
|
3723
|
+
if (content.includes('export async function GET') || content.includes('export function GET'))
|
|
3724
|
+
httpMethods.push('GET');
|
|
3725
|
+
if (content.includes('export async function POST') || content.includes('export function POST'))
|
|
3726
|
+
httpMethods.push('POST');
|
|
3727
|
+
if (content.includes('export async function PUT') || content.includes('export function PUT'))
|
|
3728
|
+
httpMethods.push('PUT');
|
|
3729
|
+
if (content.includes('export async function PATCH') || content.includes('export function PATCH'))
|
|
3730
|
+
httpMethods.push('PATCH');
|
|
3731
|
+
if (content.includes('export async function DELETE') || content.includes('export function DELETE'))
|
|
3732
|
+
httpMethods.push('DELETE');
|
|
3733
|
+
}
|
|
3734
|
+
response += `**Source:** \`${file}\`\n`;
|
|
3735
|
+
response += `**Type:** ${isApiRoute ? 'API Route' : isComponent ? 'React Component' : isService ? 'Service/Utility' : 'Module'}\n`;
|
|
3736
|
+
response += `**Test Framework:** ${testFramework}\n\n`;
|
|
3737
|
+
// Determine test file path
|
|
3738
|
+
const testFileName = fileName.replace(/\.(ts|tsx)$/, '.test$1');
|
|
3739
|
+
let testFilePath;
|
|
3740
|
+
if (isApiRoute) {
|
|
3741
|
+
// API routes: tests/api/[route].test.ts
|
|
3742
|
+
const routePath = file.replace(/.*\/api\//, '').replace(/\/route\.(ts|tsx)$/, '').replace(/\\/g, '/');
|
|
3743
|
+
testFilePath = `tests/api/${routePath}.test.ts`;
|
|
3744
|
+
}
|
|
3745
|
+
else if (isComponent) {
|
|
3746
|
+
// Components: alongside the file
|
|
3747
|
+
testFilePath = file.replace(/\.(tsx)$/, '.test.tsx');
|
|
3748
|
+
}
|
|
3749
|
+
else {
|
|
3750
|
+
// Services/utils: tests/services/
|
|
3751
|
+
testFilePath = `tests/${fileName.replace(/\.(ts|tsx)$/, '.test.ts')}`;
|
|
3752
|
+
}
|
|
3753
|
+
response += `**Test File:** \`${testFilePath}\`\n\n`;
|
|
3754
|
+
response += `---\n\n`;
|
|
3755
|
+
// Generate test stub based on file type
|
|
3756
|
+
if (isApiRoute && httpMethods.length > 0) {
|
|
3757
|
+
response += `## API Route Test Stub\n\n`;
|
|
3758
|
+
response += '```typescript\n';
|
|
3759
|
+
response += `import { describe, it, expect, beforeEach } from '${testFramework}';\n\n`;
|
|
3760
|
+
response += `describe('${file.replace(/.*\/api\//, '/api/').replace(/\/route\.(ts|tsx)$/, '')}', () => {\n`;
|
|
3761
|
+
for (const method of httpMethods) {
|
|
3762
|
+
response += ` describe('${method}', () => {\n`;
|
|
3763
|
+
response += ` it('should handle successful request', async () => {\n`;
|
|
3764
|
+
response += ` // Arrange: Set up test data\n`;
|
|
3765
|
+
response += ` const request = new Request('http://localhost/api/...', {\n`;
|
|
3766
|
+
response += ` method: '${method}',\n`;
|
|
3767
|
+
if (method !== 'GET' && method !== 'DELETE') {
|
|
3768
|
+
response += ` body: JSON.stringify({ /* test data */ }),\n`;
|
|
3769
|
+
response += ` headers: { 'Content-Type': 'application/json' },\n`;
|
|
3770
|
+
}
|
|
3771
|
+
response += ` });\n\n`;
|
|
3772
|
+
response += ` // Act: Call the handler\n`;
|
|
3773
|
+
response += ` // const response = await ${method}(request);\n`;
|
|
3774
|
+
response += ` // const data = await response.json();\n\n`;
|
|
3775
|
+
response += ` // Assert: Check response\n`;
|
|
3776
|
+
response += ` // expect(response.status).toBe(200);\n`;
|
|
3777
|
+
response += ` // expect(data).toMatchObject({ /* expected */ });\n`;
|
|
3778
|
+
response += ` });\n\n`;
|
|
3779
|
+
response += ` it('should handle validation errors', async () => {\n`;
|
|
3780
|
+
response += ` // Test with invalid input\n`;
|
|
3781
|
+
response += ` // expect(response.status).toBe(400);\n`;
|
|
3782
|
+
response += ` });\n\n`;
|
|
3783
|
+
response += ` it('should handle unauthorized access', async () => {\n`;
|
|
3784
|
+
response += ` // Test without auth\n`;
|
|
3785
|
+
response += ` // expect(response.status).toBe(401);\n`;
|
|
3786
|
+
response += ` });\n`;
|
|
3787
|
+
response += ` });\n\n`;
|
|
3788
|
+
}
|
|
3789
|
+
response += `});\n`;
|
|
3790
|
+
response += '```\n\n';
|
|
3791
|
+
}
|
|
3792
|
+
else if (isComponent) {
|
|
3793
|
+
const componentName = exportedItems[0] || fileName.replace(/\.(tsx)$/, '');
|
|
3794
|
+
response += `## Component Test Stub\n\n`;
|
|
3795
|
+
response += '```typescript\n';
|
|
3796
|
+
response += `import { render, screen, fireEvent } from '@testing-library/react';\n`;
|
|
3797
|
+
response += `import { describe, it, expect } from '${testFramework}';\n`;
|
|
3798
|
+
response += `import { ${componentName} } from './${fileName.replace(/\.tsx$/, '')}';\n\n`;
|
|
3799
|
+
response += `describe('${componentName}', () => {\n`;
|
|
3800
|
+
response += ` it('renders correctly', () => {\n`;
|
|
3801
|
+
response += ` render(<${componentName} />);\n`;
|
|
3802
|
+
response += ` // expect(screen.getByRole('...')).toBeInTheDocument();\n`;
|
|
3803
|
+
response += ` });\n\n`;
|
|
3804
|
+
response += ` it('handles user interaction', async () => {\n`;
|
|
3805
|
+
response += ` render(<${componentName} />);\n`;
|
|
3806
|
+
response += ` // const button = screen.getByRole('button', { name: /.../ });\n`;
|
|
3807
|
+
response += ` // await fireEvent.click(button);\n`;
|
|
3808
|
+
response += ` // expect(screen.getByText('...')).toBeInTheDocument();\n`;
|
|
3809
|
+
response += ` });\n\n`;
|
|
3810
|
+
response += ` it('displays loading state', () => {\n`;
|
|
3811
|
+
response += ` // Test loading state\n`;
|
|
3812
|
+
response += ` });\n\n`;
|
|
3813
|
+
response += ` it('handles errors gracefully', () => {\n`;
|
|
3814
|
+
response += ` // Test error state\n`;
|
|
3815
|
+
response += ` });\n`;
|
|
3816
|
+
response += `});\n`;
|
|
3817
|
+
response += '```\n\n';
|
|
3818
|
+
}
|
|
3819
|
+
else {
|
|
3820
|
+
// Generic function/service tests
|
|
3821
|
+
response += `## Unit Test Stub\n\n`;
|
|
3822
|
+
response += '```typescript\n';
|
|
3823
|
+
response += `import { describe, it, expect, vi } from '${testFramework}';\n`;
|
|
3824
|
+
if (exportedItems.length > 0) {
|
|
3825
|
+
response += `import { ${exportedItems.join(', ')} } from '${file.replace(/\.(ts|tsx)$/, '')}';\n`;
|
|
3826
|
+
}
|
|
3827
|
+
response += `\n`;
|
|
3828
|
+
response += `describe('${fileName.replace(/\.(ts|tsx)$/, '')}', () => {\n`;
|
|
3829
|
+
for (const item of exportedItems.slice(0, 5)) { // Limit to first 5
|
|
3830
|
+
response += ` describe('${item}', () => {\n`;
|
|
3831
|
+
response += ` it('should work correctly with valid input', async () => {\n`;
|
|
3832
|
+
response += ` // Arrange\n`;
|
|
3833
|
+
response += ` const input = { /* test data */ };\n\n`;
|
|
3834
|
+
response += ` // Act\n`;
|
|
3835
|
+
response += ` // const result = await ${item}(input);\n\n`;
|
|
3836
|
+
response += ` // Assert\n`;
|
|
3837
|
+
response += ` // expect(result).toBe(/* expected */);\n`;
|
|
3838
|
+
response += ` });\n\n`;
|
|
3839
|
+
response += ` it('should handle edge cases', async () => {\n`;
|
|
3840
|
+
response += ` // Test with empty/null/undefined inputs\n`;
|
|
3841
|
+
response += ` });\n\n`;
|
|
3842
|
+
response += ` it('should throw on invalid input', async () => {\n`;
|
|
3843
|
+
response += ` // expect(() => ${item}(invalid)).toThrow();\n`;
|
|
3844
|
+
response += ` });\n`;
|
|
3845
|
+
response += ` });\n\n`;
|
|
3846
|
+
}
|
|
3847
|
+
response += `});\n`;
|
|
3848
|
+
response += '```\n\n';
|
|
3849
|
+
}
|
|
3850
|
+
}
|
|
3851
|
+
else if (feature) {
|
|
3852
|
+
// Generate feature-based test structure
|
|
3853
|
+
response += `**Feature:** ${feature}\n`;
|
|
3854
|
+
response += `**Test Framework:** ${testFramework}\n\n`;
|
|
3855
|
+
response += `---\n\n`;
|
|
3856
|
+
response += `## Feature Test Structure\n\n`;
|
|
3857
|
+
response += `For feature "${feature}", create the following test files:\n\n`;
|
|
3858
|
+
response += `### 1. Unit Tests (\`tests/unit/${feature.toLowerCase().replace(/\s+/g, '-')}.test.ts\`)\n\n`;
|
|
3859
|
+
response += '```typescript\n';
|
|
3860
|
+
response += `import { describe, it, expect } from '${testFramework}';\n\n`;
|
|
3861
|
+
response += `describe('${feature} - Unit Tests', () => {\n`;
|
|
3862
|
+
response += ` describe('Core Logic', () => {\n`;
|
|
3863
|
+
response += ` it('should handle happy path', () => {\n`;
|
|
3864
|
+
response += ` // Test the main success scenario\n`;
|
|
3865
|
+
response += ` });\n\n`;
|
|
3866
|
+
response += ` it('should validate input', () => {\n`;
|
|
3867
|
+
response += ` // Test input validation\n`;
|
|
3868
|
+
response += ` });\n\n`;
|
|
3869
|
+
response += ` it('should handle errors', () => {\n`;
|
|
3870
|
+
response += ` // Test error handling\n`;
|
|
3871
|
+
response += ` });\n`;
|
|
3872
|
+
response += ` });\n`;
|
|
3873
|
+
response += `});\n`;
|
|
3874
|
+
response += '```\n\n';
|
|
3875
|
+
response += `### 2. Integration Tests (\`tests/integration/${feature.toLowerCase().replace(/\s+/g, '-')}.test.ts\`)\n\n`;
|
|
3876
|
+
response += '```typescript\n';
|
|
3877
|
+
response += `import { describe, it, expect, beforeAll, afterAll } from '${testFramework}';\n\n`;
|
|
3878
|
+
response += `describe('${feature} - Integration Tests', () => {\n`;
|
|
3879
|
+
response += ` beforeAll(async () => {\n`;
|
|
3880
|
+
response += ` // Set up test database, mock services, etc.\n`;
|
|
3881
|
+
response += ` });\n\n`;
|
|
3882
|
+
response += ` afterAll(async () => {\n`;
|
|
3883
|
+
response += ` // Clean up\n`;
|
|
3884
|
+
response += ` });\n\n`;
|
|
3885
|
+
response += ` it('should complete the full flow', async () => {\n`;
|
|
3886
|
+
response += ` // Test the complete feature flow\n`;
|
|
3887
|
+
response += ` });\n`;
|
|
3888
|
+
response += `});\n`;
|
|
3889
|
+
response += '```\n\n';
|
|
3890
|
+
if (testType === 'e2e' || testFramework === 'playwright') {
|
|
3891
|
+
response += `### 3. E2E Tests (\`e2e/${feature.toLowerCase().replace(/\s+/g, '-')}.spec.ts\`)\n\n`;
|
|
3892
|
+
response += '```typescript\n';
|
|
3893
|
+
response += `import { test, expect } from '@playwright/test';\n\n`;
|
|
3894
|
+
response += `test.describe('${feature}', () => {\n`;
|
|
3895
|
+
response += ` test('user can complete the flow', async ({ page }) => {\n`;
|
|
3896
|
+
response += ` // Navigate to the feature\n`;
|
|
3897
|
+
response += ` await page.goto('/...');\n\n`;
|
|
3898
|
+
response += ` // Interact with the UI\n`;
|
|
3899
|
+
response += ` // await page.click('button');\n`;
|
|
3900
|
+
response += ` // await page.fill('input', 'value');\n\n`;
|
|
3901
|
+
response += ` // Verify the result\n`;
|
|
3902
|
+
response += ` // await expect(page.getByText('...')).toBeVisible();\n`;
|
|
3903
|
+
response += ` });\n`;
|
|
3904
|
+
response += `});\n`;
|
|
3905
|
+
response += '```\n\n';
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
response += `---\n\n`;
|
|
3909
|
+
response += `**Next Steps:**\n`;
|
|
3910
|
+
response += `1. Create the test file at the suggested path\n`;
|
|
3911
|
+
response += `2. Uncomment and fill in the test implementations\n`;
|
|
3912
|
+
response += `3. Run tests: \`npm test\`\n`;
|
|
3913
|
+
return {
|
|
3914
|
+
content: [{
|
|
3915
|
+
type: 'text',
|
|
3916
|
+
text: response,
|
|
3917
|
+
}],
|
|
3918
|
+
};
|
|
3919
|
+
}
|
|
3632
3920
|
/**
|
|
3633
3921
|
* MANDATORY: Validate that a feature is complete before AI can say "done" (v6.0 Server-Side)
|
|
3634
3922
|
* Runs local checks (tests, TypeScript), then validates with server
|
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:
|
|
@@ -1659,6 +1682,9 @@ class CodeBakersServer {
|
|
|
1659
1682
|
case 'run_tests':
|
|
1660
1683
|
return this.handleRunTests(args as { filter?: string; watch?: boolean });
|
|
1661
1684
|
|
|
1685
|
+
case 'generate_tests':
|
|
1686
|
+
return this.handleGenerateTests(args as { file?: string; feature?: string; testType?: 'unit' | 'integration' | 'e2e' });
|
|
1687
|
+
|
|
1662
1688
|
case 'validate_complete':
|
|
1663
1689
|
return this.handleValidateComplete(args as { feature: string; files?: string[] });
|
|
1664
1690
|
|
|
@@ -4079,6 +4105,281 @@ Just describe what you want to build! I'll automatically:
|
|
|
4079
4105
|
};
|
|
4080
4106
|
}
|
|
4081
4107
|
|
|
4108
|
+
/**
|
|
4109
|
+
* Generate test stubs for a file or feature
|
|
4110
|
+
* Analyzes source code and creates appropriate test templates
|
|
4111
|
+
*/
|
|
4112
|
+
private handleGenerateTests(args: { file?: string; feature?: string; testType?: 'unit' | 'integration' | 'e2e' }) {
|
|
4113
|
+
const { file, feature, testType } = args;
|
|
4114
|
+
const cwd = process.cwd();
|
|
4115
|
+
|
|
4116
|
+
if (!file && !feature) {
|
|
4117
|
+
return {
|
|
4118
|
+
content: [{
|
|
4119
|
+
type: 'text' as const,
|
|
4120
|
+
text: 'โ Please provide either a file path or feature name to generate tests for.',
|
|
4121
|
+
}],
|
|
4122
|
+
isError: true,
|
|
4123
|
+
};
|
|
4124
|
+
}
|
|
4125
|
+
|
|
4126
|
+
// Detect test framework
|
|
4127
|
+
let testFramework = 'vitest';
|
|
4128
|
+
try {
|
|
4129
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
4130
|
+
if (fs.existsSync(pkgPath)) {
|
|
4131
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
4132
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
4133
|
+
if (deps['jest']) testFramework = 'jest';
|
|
4134
|
+
else if (deps['@playwright/test']) testFramework = 'playwright';
|
|
4135
|
+
else if (deps['vitest']) testFramework = 'vitest';
|
|
4136
|
+
}
|
|
4137
|
+
} catch {
|
|
4138
|
+
// Use default
|
|
4139
|
+
}
|
|
4140
|
+
|
|
4141
|
+
let response = `# ๐งช Test Stub Generator\n\n`;
|
|
4142
|
+
|
|
4143
|
+
if (file) {
|
|
4144
|
+
// Generate tests for a specific file
|
|
4145
|
+
const filePath = path.isAbsolute(file) ? file : path.join(cwd, file);
|
|
4146
|
+
|
|
4147
|
+
if (!fs.existsSync(filePath)) {
|
|
4148
|
+
return {
|
|
4149
|
+
content: [{
|
|
4150
|
+
type: 'text' as const,
|
|
4151
|
+
text: `โ File not found: ${file}`,
|
|
4152
|
+
}],
|
|
4153
|
+
isError: true,
|
|
4154
|
+
};
|
|
4155
|
+
}
|
|
4156
|
+
|
|
4157
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
4158
|
+
const fileName = path.basename(file);
|
|
4159
|
+
const fileExt = path.extname(file);
|
|
4160
|
+
const isApiRoute = file.includes('/api/') || file.includes('\\api\\');
|
|
4161
|
+
const isComponent = fileExt === '.tsx' && !isApiRoute;
|
|
4162
|
+
const isService = file.includes('/services/') || file.includes('\\services\\') || file.includes('/lib/') || file.includes('\\lib\\');
|
|
4163
|
+
|
|
4164
|
+
// Detect exported functions/components
|
|
4165
|
+
const exportedItems: string[] = [];
|
|
4166
|
+
const exportDefaultMatch = content.match(/export\s+default\s+(function\s+)?(\w+)/);
|
|
4167
|
+
const exportMatches = content.matchAll(/export\s+(async\s+)?(function|const)\s+(\w+)/g);
|
|
4168
|
+
|
|
4169
|
+
if (exportDefaultMatch && exportDefaultMatch[2]) {
|
|
4170
|
+
exportedItems.push(exportDefaultMatch[2]);
|
|
4171
|
+
}
|
|
4172
|
+
for (const match of exportMatches) {
|
|
4173
|
+
if (match[3]) exportedItems.push(match[3]);
|
|
4174
|
+
}
|
|
4175
|
+
|
|
4176
|
+
// HTTP methods for API routes
|
|
4177
|
+
const httpMethods: string[] = [];
|
|
4178
|
+
if (isApiRoute) {
|
|
4179
|
+
if (content.includes('export async function GET') || content.includes('export function GET')) httpMethods.push('GET');
|
|
4180
|
+
if (content.includes('export async function POST') || content.includes('export function POST')) httpMethods.push('POST');
|
|
4181
|
+
if (content.includes('export async function PUT') || content.includes('export function PUT')) httpMethods.push('PUT');
|
|
4182
|
+
if (content.includes('export async function PATCH') || content.includes('export function PATCH')) httpMethods.push('PATCH');
|
|
4183
|
+
if (content.includes('export async function DELETE') || content.includes('export function DELETE')) httpMethods.push('DELETE');
|
|
4184
|
+
}
|
|
4185
|
+
|
|
4186
|
+
response += `**Source:** \`${file}\`\n`;
|
|
4187
|
+
response += `**Type:** ${isApiRoute ? 'API Route' : isComponent ? 'React Component' : isService ? 'Service/Utility' : 'Module'}\n`;
|
|
4188
|
+
response += `**Test Framework:** ${testFramework}\n\n`;
|
|
4189
|
+
|
|
4190
|
+
// Determine test file path
|
|
4191
|
+
const testFileName = fileName.replace(/\.(ts|tsx)$/, '.test$1');
|
|
4192
|
+
let testFilePath: string;
|
|
4193
|
+
|
|
4194
|
+
if (isApiRoute) {
|
|
4195
|
+
// API routes: tests/api/[route].test.ts
|
|
4196
|
+
const routePath = file.replace(/.*\/api\//, '').replace(/\/route\.(ts|tsx)$/, '').replace(/\\/g, '/');
|
|
4197
|
+
testFilePath = `tests/api/${routePath}.test.ts`;
|
|
4198
|
+
} else if (isComponent) {
|
|
4199
|
+
// Components: alongside the file
|
|
4200
|
+
testFilePath = file.replace(/\.(tsx)$/, '.test.tsx');
|
|
4201
|
+
} else {
|
|
4202
|
+
// Services/utils: tests/services/
|
|
4203
|
+
testFilePath = `tests/${fileName.replace(/\.(ts|tsx)$/, '.test.ts')}`;
|
|
4204
|
+
}
|
|
4205
|
+
|
|
4206
|
+
response += `**Test File:** \`${testFilePath}\`\n\n`;
|
|
4207
|
+
response += `---\n\n`;
|
|
4208
|
+
|
|
4209
|
+
// Generate test stub based on file type
|
|
4210
|
+
if (isApiRoute && httpMethods.length > 0) {
|
|
4211
|
+
response += `## API Route Test Stub\n\n`;
|
|
4212
|
+
response += '```typescript\n';
|
|
4213
|
+
response += `import { describe, it, expect, beforeEach } from '${testFramework}';\n\n`;
|
|
4214
|
+
response += `describe('${file.replace(/.*\/api\//, '/api/').replace(/\/route\.(ts|tsx)$/, '')}', () => {\n`;
|
|
4215
|
+
|
|
4216
|
+
for (const method of httpMethods) {
|
|
4217
|
+
response += ` describe('${method}', () => {\n`;
|
|
4218
|
+
response += ` it('should handle successful request', async () => {\n`;
|
|
4219
|
+
response += ` // Arrange: Set up test data\n`;
|
|
4220
|
+
response += ` const request = new Request('http://localhost/api/...', {\n`;
|
|
4221
|
+
response += ` method: '${method}',\n`;
|
|
4222
|
+
if (method !== 'GET' && method !== 'DELETE') {
|
|
4223
|
+
response += ` body: JSON.stringify({ /* test data */ }),\n`;
|
|
4224
|
+
response += ` headers: { 'Content-Type': 'application/json' },\n`;
|
|
4225
|
+
}
|
|
4226
|
+
response += ` });\n\n`;
|
|
4227
|
+
response += ` // Act: Call the handler\n`;
|
|
4228
|
+
response += ` // const response = await ${method}(request);\n`;
|
|
4229
|
+
response += ` // const data = await response.json();\n\n`;
|
|
4230
|
+
response += ` // Assert: Check response\n`;
|
|
4231
|
+
response += ` // expect(response.status).toBe(200);\n`;
|
|
4232
|
+
response += ` // expect(data).toMatchObject({ /* expected */ });\n`;
|
|
4233
|
+
response += ` });\n\n`;
|
|
4234
|
+
response += ` it('should handle validation errors', async () => {\n`;
|
|
4235
|
+
response += ` // Test with invalid input\n`;
|
|
4236
|
+
response += ` // expect(response.status).toBe(400);\n`;
|
|
4237
|
+
response += ` });\n\n`;
|
|
4238
|
+
response += ` it('should handle unauthorized access', async () => {\n`;
|
|
4239
|
+
response += ` // Test without auth\n`;
|
|
4240
|
+
response += ` // expect(response.status).toBe(401);\n`;
|
|
4241
|
+
response += ` });\n`;
|
|
4242
|
+
response += ` });\n\n`;
|
|
4243
|
+
}
|
|
4244
|
+
response += `});\n`;
|
|
4245
|
+
response += '```\n\n';
|
|
4246
|
+
|
|
4247
|
+
} else if (isComponent) {
|
|
4248
|
+
const componentName = exportedItems[0] || fileName.replace(/\.(tsx)$/, '');
|
|
4249
|
+
response += `## Component Test Stub\n\n`;
|
|
4250
|
+
response += '```typescript\n';
|
|
4251
|
+
response += `import { render, screen, fireEvent } from '@testing-library/react';\n`;
|
|
4252
|
+
response += `import { describe, it, expect } from '${testFramework}';\n`;
|
|
4253
|
+
response += `import { ${componentName} } from './${fileName.replace(/\.tsx$/, '')}';\n\n`;
|
|
4254
|
+
response += `describe('${componentName}', () => {\n`;
|
|
4255
|
+
response += ` it('renders correctly', () => {\n`;
|
|
4256
|
+
response += ` render(<${componentName} />);\n`;
|
|
4257
|
+
response += ` // expect(screen.getByRole('...')).toBeInTheDocument();\n`;
|
|
4258
|
+
response += ` });\n\n`;
|
|
4259
|
+
response += ` it('handles user interaction', async () => {\n`;
|
|
4260
|
+
response += ` render(<${componentName} />);\n`;
|
|
4261
|
+
response += ` // const button = screen.getByRole('button', { name: /.../ });\n`;
|
|
4262
|
+
response += ` // await fireEvent.click(button);\n`;
|
|
4263
|
+
response += ` // expect(screen.getByText('...')).toBeInTheDocument();\n`;
|
|
4264
|
+
response += ` });\n\n`;
|
|
4265
|
+
response += ` it('displays loading state', () => {\n`;
|
|
4266
|
+
response += ` // Test loading state\n`;
|
|
4267
|
+
response += ` });\n\n`;
|
|
4268
|
+
response += ` it('handles errors gracefully', () => {\n`;
|
|
4269
|
+
response += ` // Test error state\n`;
|
|
4270
|
+
response += ` });\n`;
|
|
4271
|
+
response += `});\n`;
|
|
4272
|
+
response += '```\n\n';
|
|
4273
|
+
|
|
4274
|
+
} else {
|
|
4275
|
+
// Generic function/service tests
|
|
4276
|
+
response += `## Unit Test Stub\n\n`;
|
|
4277
|
+
response += '```typescript\n';
|
|
4278
|
+
response += `import { describe, it, expect, vi } from '${testFramework}';\n`;
|
|
4279
|
+
if (exportedItems.length > 0) {
|
|
4280
|
+
response += `import { ${exportedItems.join(', ')} } from '${file.replace(/\.(ts|tsx)$/, '')}';\n`;
|
|
4281
|
+
}
|
|
4282
|
+
response += `\n`;
|
|
4283
|
+
response += `describe('${fileName.replace(/\.(ts|tsx)$/, '')}', () => {\n`;
|
|
4284
|
+
|
|
4285
|
+
for (const item of exportedItems.slice(0, 5)) { // Limit to first 5
|
|
4286
|
+
response += ` describe('${item}', () => {\n`;
|
|
4287
|
+
response += ` it('should work correctly with valid input', async () => {\n`;
|
|
4288
|
+
response += ` // Arrange\n`;
|
|
4289
|
+
response += ` const input = { /* test data */ };\n\n`;
|
|
4290
|
+
response += ` // Act\n`;
|
|
4291
|
+
response += ` // const result = await ${item}(input);\n\n`;
|
|
4292
|
+
response += ` // Assert\n`;
|
|
4293
|
+
response += ` // expect(result).toBe(/* expected */);\n`;
|
|
4294
|
+
response += ` });\n\n`;
|
|
4295
|
+
response += ` it('should handle edge cases', async () => {\n`;
|
|
4296
|
+
response += ` // Test with empty/null/undefined inputs\n`;
|
|
4297
|
+
response += ` });\n\n`;
|
|
4298
|
+
response += ` it('should throw on invalid input', async () => {\n`;
|
|
4299
|
+
response += ` // expect(() => ${item}(invalid)).toThrow();\n`;
|
|
4300
|
+
response += ` });\n`;
|
|
4301
|
+
response += ` });\n\n`;
|
|
4302
|
+
}
|
|
4303
|
+
response += `});\n`;
|
|
4304
|
+
response += '```\n\n';
|
|
4305
|
+
}
|
|
4306
|
+
|
|
4307
|
+
} else if (feature) {
|
|
4308
|
+
// Generate feature-based test structure
|
|
4309
|
+
response += `**Feature:** ${feature}\n`;
|
|
4310
|
+
response += `**Test Framework:** ${testFramework}\n\n`;
|
|
4311
|
+
response += `---\n\n`;
|
|
4312
|
+
|
|
4313
|
+
response += `## Feature Test Structure\n\n`;
|
|
4314
|
+
response += `For feature "${feature}", create the following test files:\n\n`;
|
|
4315
|
+
|
|
4316
|
+
response += `### 1. Unit Tests (\`tests/unit/${feature.toLowerCase().replace(/\s+/g, '-')}.test.ts\`)\n\n`;
|
|
4317
|
+
response += '```typescript\n';
|
|
4318
|
+
response += `import { describe, it, expect } from '${testFramework}';\n\n`;
|
|
4319
|
+
response += `describe('${feature} - Unit Tests', () => {\n`;
|
|
4320
|
+
response += ` describe('Core Logic', () => {\n`;
|
|
4321
|
+
response += ` it('should handle happy path', () => {\n`;
|
|
4322
|
+
response += ` // Test the main success scenario\n`;
|
|
4323
|
+
response += ` });\n\n`;
|
|
4324
|
+
response += ` it('should validate input', () => {\n`;
|
|
4325
|
+
response += ` // Test input validation\n`;
|
|
4326
|
+
response += ` });\n\n`;
|
|
4327
|
+
response += ` it('should handle errors', () => {\n`;
|
|
4328
|
+
response += ` // Test error handling\n`;
|
|
4329
|
+
response += ` });\n`;
|
|
4330
|
+
response += ` });\n`;
|
|
4331
|
+
response += `});\n`;
|
|
4332
|
+
response += '```\n\n';
|
|
4333
|
+
|
|
4334
|
+
response += `### 2. Integration Tests (\`tests/integration/${feature.toLowerCase().replace(/\s+/g, '-')}.test.ts\`)\n\n`;
|
|
4335
|
+
response += '```typescript\n';
|
|
4336
|
+
response += `import { describe, it, expect, beforeAll, afterAll } from '${testFramework}';\n\n`;
|
|
4337
|
+
response += `describe('${feature} - Integration Tests', () => {\n`;
|
|
4338
|
+
response += ` beforeAll(async () => {\n`;
|
|
4339
|
+
response += ` // Set up test database, mock services, etc.\n`;
|
|
4340
|
+
response += ` });\n\n`;
|
|
4341
|
+
response += ` afterAll(async () => {\n`;
|
|
4342
|
+
response += ` // Clean up\n`;
|
|
4343
|
+
response += ` });\n\n`;
|
|
4344
|
+
response += ` it('should complete the full flow', async () => {\n`;
|
|
4345
|
+
response += ` // Test the complete feature flow\n`;
|
|
4346
|
+
response += ` });\n`;
|
|
4347
|
+
response += `});\n`;
|
|
4348
|
+
response += '```\n\n';
|
|
4349
|
+
|
|
4350
|
+
if (testType === 'e2e' || testFramework === 'playwright') {
|
|
4351
|
+
response += `### 3. E2E Tests (\`e2e/${feature.toLowerCase().replace(/\s+/g, '-')}.spec.ts\`)\n\n`;
|
|
4352
|
+
response += '```typescript\n';
|
|
4353
|
+
response += `import { test, expect } from '@playwright/test';\n\n`;
|
|
4354
|
+
response += `test.describe('${feature}', () => {\n`;
|
|
4355
|
+
response += ` test('user can complete the flow', async ({ page }) => {\n`;
|
|
4356
|
+
response += ` // Navigate to the feature\n`;
|
|
4357
|
+
response += ` await page.goto('/...');\n\n`;
|
|
4358
|
+
response += ` // Interact with the UI\n`;
|
|
4359
|
+
response += ` // await page.click('button');\n`;
|
|
4360
|
+
response += ` // await page.fill('input', 'value');\n\n`;
|
|
4361
|
+
response += ` // Verify the result\n`;
|
|
4362
|
+
response += ` // await expect(page.getByText('...')).toBeVisible();\n`;
|
|
4363
|
+
response += ` });\n`;
|
|
4364
|
+
response += `});\n`;
|
|
4365
|
+
response += '```\n\n';
|
|
4366
|
+
}
|
|
4367
|
+
}
|
|
4368
|
+
|
|
4369
|
+
response += `---\n\n`;
|
|
4370
|
+
response += `**Next Steps:**\n`;
|
|
4371
|
+
response += `1. Create the test file at the suggested path\n`;
|
|
4372
|
+
response += `2. Uncomment and fill in the test implementations\n`;
|
|
4373
|
+
response += `3. Run tests: \`npm test\`\n`;
|
|
4374
|
+
|
|
4375
|
+
return {
|
|
4376
|
+
content: [{
|
|
4377
|
+
type: 'text' as const,
|
|
4378
|
+
text: response,
|
|
4379
|
+
}],
|
|
4380
|
+
};
|
|
4381
|
+
}
|
|
4382
|
+
|
|
4082
4383
|
/**
|
|
4083
4384
|
* MANDATORY: Validate that a feature is complete before AI can say "done" (v6.0 Server-Side)
|
|
4084
4385
|
* Runs local checks (tests, TypeScript), then validates with server
|