embedia
Version:
Zero-configuration AI chatbot integration CLI - direct file copy with embedded API keys
711 lines (598 loc) • 21.4 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
const { execSync } = require('child_process');
const logger = require('../utils/logger');
/**
* ValidationService - Tests integration success and provides feedback
* Ensures the chatbot actually works after CLI integration
*/
class ValidationService {
constructor() {
this.validationResults = {
passed: [],
failed: [],
warnings: [],
score: 0,
maxScore: 0
};
}
/**
* Run comprehensive validation of the integration
* @param {string} projectDirectory - Project directory
* @param {Object} projectInfo - Project information
* @param {Object} integrationResults - Results from auto-integrator
* @returns {Promise<Object>} Validation results
*/
async validateIntegration(projectDirectory, projectInfo, integrationResults) {
try {
logger.info('🔍 Validating chatbot integration...');
this.projectDirectory = projectDirectory;
this.projectInfo = projectInfo;
// Run validation tests
await this.validateFileStructure();
await this.validateImports();
await this.validateApiEndpoint();
await this.validateEnvironment();
await this.validateDependencies();
await this.validateConfiguration();
await this.validateBuildProcess();
// Calculate final score
this.calculateScore();
// Generate recommendations
const recommendations = this.generateRecommendations();
logger.success(`✅ Validation completed - Score: ${this.validationResults.score}/${this.validationResults.maxScore}`);
return {
...this.validationResults,
recommendations,
success: this.validationResults.score >= this.validationResults.maxScore * 0.8
};
} catch (error) {
this.validationResults.failed.push({
test: 'validation_process',
error: error.message
});
throw error;
}
}
/**
* Validate file structure
*/
async validateFileStructure() {
const tests = [
{
name: 'Components directory exists',
check: () => fs.pathExists(path.join(this.projectDirectory, this.projectInfo.paths.components))
},
{
name: 'Chatbot component exists',
check: () => fs.pathExists(path.join(this.projectDirectory, this.projectInfo.paths.components, 'AutoChat/Chatbot.tsx'))
},
{
name: 'Chat hook exists',
check: () => fs.pathExists(path.join(this.projectDirectory, this.projectInfo.paths.components, 'AutoChat/useChat.ts'))
},
{
name: 'Types file exists',
check: () => fs.pathExists(path.join(this.projectDirectory, this.projectInfo.paths.components, 'AutoChat/types.ts'))
},
{
name: 'API endpoint exists',
check: () => this.checkApiEndpoint()
}
];
await this.runTests('File Structure', tests);
}
/**
* Check if API endpoint exists in correct location
*/
async checkApiEndpoint() {
try {
const { routerType } = this.projectInfo;
const apiPath = this.projectInfo.paths?.api || 'api';
if (routerType === 'app' || routerType === 'hybrid') {
return fs.pathExists(path.join(this.projectDirectory, apiPath, 'chat/route.ts'));
} else if (routerType === 'pages') {
return fs.pathExists(path.join(this.projectDirectory, apiPath, 'chat.ts'));
} else {
// For other project types, check for general API endpoint
return fs.pathExists(path.join(this.projectDirectory, apiPath, 'chat.ts'));
}
} catch (error) {
logger.warn(`API endpoint check failed: ${error.message}`);
return false;
}
}
/**
* Validate imports and syntax
*/
async validateImports() {
const tests = [
{
name: 'Chatbot component syntax valid',
check: () => this.validateFileContent(
path.join(this.projectInfo.paths.components, 'AutoChat/Chatbot.tsx'),
['import React', 'export default']
)
},
{
name: 'Hook imports are correct',
check: () => this.validateHookImports()
},
{
name: 'Layout integration exists',
check: () => this.validateLayoutIntegration()
}
];
await this.runTests('Import Validation', tests);
}
/**
* Validate hook imports with flexible import pattern matching
*/
async validateHookImports() {
const hookPath = path.join(this.projectDirectory, this.projectInfo.paths.components, 'AutoChat/useChat.ts');
if (!(await fs.pathExists(hookPath))) {
return false;
}
const content = await fs.readFile(hookPath, 'utf8');
// More flexible React import validation
const hasReactImports =
content.includes("import") && content.includes("react") ||
content.includes("useState") || content.includes("useEffect") ||
content.includes("useCallback") || content.includes("useRef");
// Check for type imports (supports both relative and absolute imports)
const hasTypeImports =
content.includes("import") && content.includes("types") ||
content.includes("Message") || content.includes("interface") ||
content.includes("type Message");
// Check for proper export (either named or default)
const hasValidExport =
content.includes('export const useChat') ||
content.includes('export default') ||
content.includes('export function useChat') ||
content.includes('export { useChat }');
// Check that essential hook functionality exists
const hasHookLogic =
content.includes('useState') &&
content.includes('fetch') &&
(content.includes('/api/chat') || content.includes('api/chat'));
return hasReactImports && hasValidExport && hasHookLogic;
}
/**
* Validate layout integration
*/
async validateLayoutIntegration() {
if (!this.projectInfo.paths.layout) {
return false;
}
const layoutPath = path.join(this.projectDirectory, this.projectInfo.paths.layout);
if (!(await fs.pathExists(layoutPath))) {
return false;
}
const content = await fs.readFile(layoutPath, 'utf8');
// Check for chatbot import and component
return content.includes('AutoChat/Chatbot') && content.includes('<Chatbot');
}
/**
* Validate API endpoint functionality
*/
async validateApiEndpoint() {
const tests = [
{
name: 'API route file is valid JavaScript/TypeScript',
check: () => this.validateApiSyntax()
},
{
name: 'API route handles POST requests',
check: () => this.validateApiContent()
},
{
name: 'API route uses correct imports',
check: () => this.validateApiImports()
}
];
await this.runTests('API Validation', tests);
}
/**
* Validate API syntax
*/
async validateApiSyntax() {
const apiPath = this.getApiPath();
if (!(await fs.pathExists(path.join(this.projectDirectory, apiPath)))) {
return false;
}
const content = await fs.readFile(path.join(this.projectDirectory, apiPath), 'utf8');
// Basic syntax checks
return content.includes('export') &&
(content.includes('POST') || content.includes('req.method')) &&
!this.hasSyntaxErrors(content);
}
/**
* Get API path based on router type
*/
getApiPath() {
const { routerType, type } = this.projectInfo;
let apiPath = this.projectInfo.paths?.api || 'api';
// For scaffolded App Router projects, ensure correct API path
if (type === 'empty' && routerType === 'app') {
apiPath = 'src/app/api';
}
if (routerType === 'app' || routerType === 'hybrid') {
return path.join(apiPath, 'chat/route.ts');
} else {
return path.join(apiPath, 'chat.ts');
}
}
/**
* Basic syntax error detection
*/
hasSyntaxErrors(content) {
const commonErrors = [
'SyntaxError',
'Unexpected token',
'Unexpected end of input',
'Missing closing',
'Unclosed'
];
return commonErrors.some(error => content.includes(error));
}
/**
* Validate API content
*/
async validateApiContent() {
const apiPath = path.join(this.projectDirectory, this.getApiPath());
if (!(await fs.pathExists(apiPath))) {
return false;
}
const content = await fs.readFile(apiPath, 'utf8');
// Check for different router types
const { routerType } = this.projectInfo;
if (routerType === 'pages') {
// Pages Router validation - more flexible
const hasHandler = content.includes('handler') || content.includes('export default');
const hasPostMethod = content.includes('req.method') && content.includes('POST');
const hasAiIntegration = content.includes('@google/generative-ai') ||
content.includes('GoogleGenerativeAI') ||
content.includes('genai');
const hasResponse = content.includes('res.status') || content.includes('res.json');
return hasHandler && hasPostMethod && hasAiIntegration && hasResponse;
} else {
// App Router validation - more flexible for async patterns
const hasPostExport = content.includes('export async function POST') ||
content.includes('export function POST');
const hasRequestParam = content.includes('request') || content.includes('Request');
const hasAiIntegration = content.includes('@google/generative-ai') ||
content.includes('GoogleGenerativeAI') ||
content.includes('genai');
const hasResponseJson = content.includes('Response.json') ||
content.includes('new Response') ||
content.includes('json()');
const hasAsyncAwait = content.includes('async') && content.includes('await');
return hasPostExport && hasRequestParam && hasAiIntegration &&
(hasResponseJson || hasAsyncAwait);
}
}
/**
* Validate API imports
*/
async validateApiImports() {
const apiPath = path.join(this.projectDirectory, this.getApiPath());
if (!(await fs.pathExists(apiPath))) {
return false;
}
const content = await fs.readFile(apiPath, 'utf8');
// Check for correct Google AI package import
return content.includes('@google/generative-ai') ||
content.includes('GoogleGenerativeAI') ||
content.includes('genai');
}
/**
* Validate environment setup
*/
async validateEnvironment() {
const tests = [
{
name: '.env.local exists',
check: () => fs.pathExists(path.join(this.projectDirectory, '.env.local'))
},
{
name: 'API key is configured',
check: () => this.checkApiKeyExists()
},
{
name: 'Public variables are set',
check: () => this.checkPublicVars()
},
{
name: '.gitignore excludes .env.local',
check: () => this.checkGitignore()
}
];
await this.runTests('Environment', tests);
}
/**
* Check if API key exists in .env.local
*/
async checkApiKeyExists() {
return await this.validateApiKeyConfiguration();
}
/**
* Enhanced API key validation - checks multiple env files and providers
*/
async validateApiKeyConfiguration() {
const envPaths = ['.env.local', '.env', '.env.example'];
for (const envFile of envPaths) {
const envPath = path.join(this.projectDirectory, envFile);
if (await fs.pathExists(envPath)) {
const content = await fs.readFile(envPath, 'utf8');
// Check for key existence (not just placeholder)
const hasGeminiKey = content.includes('GEMINI_API_KEY=') &&
!content.includes('your_gemini_api_key_here');
const hasOpenaiKey = content.includes('OPENAI_API_KEY=') &&
!content.includes('your_openai_api_key_here');
if (hasGeminiKey || hasOpenaiKey) {
return true;
}
}
}
return false; // Only fail if no valid keys found
}
/**
* Check public environment variables
*/
async checkPublicVars() {
const envPath = path.join(this.projectDirectory, '.env.local');
if (!(await fs.pathExists(envPath))) {
return false;
}
const content = await fs.readFile(envPath, 'utf8');
return content.includes('NEXT_PUBLIC_CHATBOT_ENABLED');
}
/**
* Check if .gitignore excludes .env.local
*/
async checkGitignore() {
const gitignorePath = path.join(this.projectDirectory, '.gitignore');
if (!(await fs.pathExists(gitignorePath))) {
return true; // Not a git project, that's ok
}
const content = await fs.readFile(gitignorePath, 'utf8');
return content.includes('.env.local');
}
/**
* Validate dependencies
*/
async validateDependencies() {
const tests = [
{
name: 'package.json exists',
check: () => fs.pathExists(path.join(this.projectDirectory, 'package.json'))
},
{
name: 'Required dependencies installed',
check: () => this.checkRequiredDependencies()
},
{
name: 'node_modules exists',
check: () => fs.pathExists(path.join(this.projectDirectory, 'node_modules'))
}
];
await this.runTests('Dependencies', tests);
}
/**
* Check required dependencies
*/
async checkRequiredDependencies() {
const packageJsonPath = path.join(this.projectDirectory, 'package.json');
if (!(await fs.pathExists(packageJsonPath))) {
return false;
}
const packageJson = await fs.readJson(packageJsonPath);
const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies };
// Check for AI dependencies
const requiredAiDeps = ['@google/generative-ai', '@google/genai', 'openai'];
const hasAiDep = requiredAiDeps.some(dep => allDeps[dep]);
if (!hasAiDep) {
return false;
}
// Verify node_modules existence (indicates successful install)
const nodeModulesPath = path.join(this.projectDirectory, 'node_modules');
const nodeModulesExists = await fs.pathExists(nodeModulesPath);
return nodeModulesExists;
}
/**
* Validate configuration files
*/
async validateConfiguration() {
const tests = [
{
name: 'TypeScript configuration',
check: () => this.validateTypeScriptConfig()
},
{
name: 'Tailwind configuration',
check: () => this.validateTailwindConfig()
}
];
await this.runTests('Configuration', tests);
}
/**
* Validate TypeScript configuration
*/
async validateTypeScriptConfig() {
if (!this.projectInfo.config.typescript) {
return true; // Not a TypeScript project
}
const tsConfigPath = path.join(this.projectDirectory, 'tsconfig.json');
if (!(await fs.pathExists(tsConfigPath))) {
return false;
}
const tsConfig = await fs.readJson(tsConfigPath);
const includes = tsConfig.include || [];
return includes.some(include => include.includes(this.projectInfo.paths.components));
}
/**
* Validate Tailwind configuration
*/
async validateTailwindConfig() {
if (!this.projectInfo.config.tailwind) {
return true; // Not using Tailwind
}
const configFiles = ['tailwind.config.js', 'tailwind.config.ts', 'tailwind.config.mjs'];
for (const configFile of configFiles) {
const configPath = path.join(this.projectDirectory, configFile);
if (await fs.pathExists(configPath)) {
const content = await fs.readFile(configPath, 'utf8');
return content.includes(this.projectInfo.paths.components);
}
}
return false;
}
/**
* Validate build process
*/
async validateBuildProcess() {
const tests = [
{
name: 'Project can be built',
check: () => this.testBuild()
}
];
await this.runTests('Build Process', tests, false); // Non-critical
}
/**
* Test if project can build (optional, may be slow)
*/
async testBuild() {
try {
// This is optional and may be slow, so we'll skip it for now
// execSync('npm run build', { cwd: this.projectDirectory, stdio: 'pipe' });
return true;
} catch (error) {
return false;
}
}
/**
* Validate file content
*/
async validateFileContent(relativePath, requiredStrings) {
const fullPath = path.join(this.projectDirectory, relativePath);
if (!(await fs.pathExists(fullPath))) {
return false;
}
const content = await fs.readFile(fullPath, 'utf8');
return requiredStrings.every(str => content.includes(str));
}
/**
* Run a set of tests
*/
async runTests(category, tests, critical = true) {
logger.info(`🔍 Testing: ${category}`);
for (const test of tests) {
try {
const result = await test.check();
if (result) {
this.validationResults.passed.push({
category,
test: test.name,
critical
});
logger.debug(`✅ ${test.name}`);
} else {
const failure = {
category,
test: test.name,
critical
};
if (critical) {
this.validationResults.failed.push(failure);
logger.warn(`❌ ${test.name}`);
} else {
this.validationResults.warnings.push(failure);
logger.debug(`⚠️ ${test.name}`);
}
}
this.validationResults.maxScore++;
if (result) {
this.validationResults.score++;
}
} catch (error) {
this.validationResults.failed.push({
category,
test: test.name,
error: error.message,
critical
});
this.validationResults.maxScore++;
logger.error(`❌ ${test.name}: ${error.message}`);
}
}
}
/**
* Calculate final validation score
*/
calculateScore() {
const { passed, failed, warnings } = this.validationResults;
// Weight critical tests more heavily
const criticalPassed = passed.filter(p => p.critical !== false).length;
const criticalFailed = failed.filter(f => f.critical !== false).length;
const criticalTotal = criticalPassed + criticalFailed;
if (criticalTotal === 0) {
this.validationResults.score = this.validationResults.maxScore;
return;
}
// Critical tests are 80% of score, non-critical are 20%
const criticalScore = (criticalPassed / criticalTotal) * 0.8;
const nonCriticalScore = ((passed.length - criticalPassed) / Math.max(1, this.validationResults.maxScore - criticalTotal)) * 0.2;
this.validationResults.score = Math.round((criticalScore + nonCriticalScore) * this.validationResults.maxScore);
}
/**
* Generate recommendations based on validation results
*/
generateRecommendations() {
const recommendations = [];
const { failed, warnings } = this.validationResults;
// Critical failures
failed.forEach(failure => {
if (failure.category === 'File Structure') {
recommendations.push(`📁 ${failure.test} - Check file placement and naming`);
} else if (failure.category === 'Environment') {
recommendations.push(`🔑 ${failure.test} - Review environment variable setup`);
} else if (failure.category === 'Dependencies') {
recommendations.push(`📦 ${failure.test} - Run 'npm install' to install missing packages`);
} else if (failure.category === 'API Validation') {
recommendations.push(`🚀 ${failure.test} - Check API endpoint implementation`);
} else {
recommendations.push(`⚠️ ${failure.test} - Requires manual attention`);
}
});
// Add general recommendations
if (this.validationResults.score < this.validationResults.maxScore) {
recommendations.push('🔍 Run integration again if issues persist');
recommendations.push('📖 Check the integration guide for manual steps');
}
if (this.validationResults.score >= this.validationResults.maxScore * 0.8) {
recommendations.push('🎉 Integration looks good! Try running: npm run dev');
}
return recommendations;
}
/**
* Get validation summary
*/
getSummary() {
const { passed, failed, warnings, score, maxScore } = this.validationResults;
return {
success: score >= maxScore * 0.8,
score: `${score}/${maxScore}`,
percentage: Math.round((score / maxScore) * 100),
counts: {
passed: passed.length,
failed: failed.length,
warnings: warnings.length
},
status: score >= maxScore * 0.9 ? 'excellent' :
score >= maxScore * 0.8 ? 'good' :
score >= maxScore * 0.6 ? 'needs work' : 'poor'
};
}
}
module.exports = ValidationService;