@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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codebakers/cli",
3
- "version": "3.9.15",
3
+ "version": "3.9.17",
4
4
  "description": "CodeBakers CLI - Production patterns for AI-assisted development",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
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