feat: add integration test framework and health check improvements

- Add test helper library with assertion functions (scripts/lib/test-helpers.sh)
- Add gateway integration test script (scripts/tests/gateway-test.sh)
- Add configuration validation tool (scripts/validate-config.ts)
- Add health-check.ts library with Tauri command wrappers
- Add HealthStatusIndicator component to ConnectionStatus.tsx
- Add E2E test specs for memory, settings, and team collaboration
- Update ZCLAW-DEEP-ANALYSIS.md to reflect actual project state

Key improvements:
- Store architecture now properly documented as migrated
- Tauri backend shown as 85-90% complete
- Component integration status clarified

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
iven
2026-03-21 00:09:47 +08:00
parent ce522de7e9
commit c5d91cf9f0
11 changed files with 4911 additions and 26 deletions

310
scripts/validate-config.ts Normal file
View File

@@ -0,0 +1,310 @@
#!/usr/bin/env node
/**
* ZCLAW Configuration Validator
*
* Validates configuration files and environment setup.
* Run with: npx ts-node scripts/validate-config.ts
*/
import * as fs from 'fs';
import * as path from 'path';
// Types
interface ValidationResult {
file: string;
valid: boolean;
errors: string[];
warnings: string[];
}
interface ConfigValidationSummary {
timestamp: string;
totalFiles: number;
validFiles: number;
invalidFiles: number;
totalErrors: number;
totalWarnings: number;
results: ValidationResult[];
}
// Color output helpers
const colors = {
reset: '\x1b[0m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
gray: '\x1b[90m',
};
function log(color: keyof typeof colors, message: string): void {
console.log(`${colors[color]}${message}${colors.reset}`);
}
// Validators
function validateTomlFile(filePath: string): ValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
if (!fs.existsSync(filePath)) {
return { file: filePath, valid: false, errors: [`File not found: ${filePath}`], warnings: [] };
}
const content = fs.readFileSync(filePath, 'utf-8');
const lines = content.split('\n');
// Basic TOML validation
let currentSection = '';
const definedKeys = new Set<string>();
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
const lineNum = i + 1;
// Skip empty lines and comments
if (!line || line.startsWith('#')) continue;
// Section header
const sectionMatch = line.match(/^\[([^\]]+)\]$/);
if (sectionMatch) {
currentSection = sectionMatch[1];
continue;
}
// Key-value pair
const kvMatch = line.match(/^([^=]+)=(.*)$/);
if (kvMatch) {
const key = kvMatch[1].trim();
const value = kvMatch[2].trim();
const fullKey = currentSection ? `${currentSection}.${key}` : key;
// Check for duplicate keys
if (definedKeys.has(fullKey)) {
warnings.push(`Line ${lineNum}: Duplicate key "${fullKey}"`);
}
definedKeys.add(fullKey);
// Check for empty values
if (!value || value === '""' || value === "''") {
warnings.push(`Line ${lineNum}: Empty value for "${fullKey}"`);
}
// Check for unquoted strings that might need quoting
if (!value.startsWith('"') && !value.startsWith("'") && !value.startsWith('[') &&
!value.startsWith('{') && !/^(true|false|\d+|\d+\.\d+)$/.test(value)) {
if (value.includes(' ') || value.includes('#')) {
errors.push(`Line ${lineNum}: Value "${value}" should be quoted`);
}
}
// Check for environment variable references
if (value.includes('${') && !value.includes('}')) {
errors.push(`Line ${lineNum}: Unclosed environment variable reference`);
}
} else if (line && !line.startsWith('#')) {
// Invalid line
errors.push(`Line ${lineNum}: Invalid TOML syntax: "${line}"`);
}
}
return {
file: filePath,
valid: errors.length === 0,
errors,
warnings,
};
}
function validateMainConfig(): ValidationResult {
const configPath = path.join(process.cwd(), 'config/config.toml');
const result = validateTomlFile(configPath);
if (result.errors.length === 0 && fs.existsSync(configPath)) {
const content = fs.readFileSync(configPath, 'utf-8');
// Check for required sections
const requiredSections = ['gateway', 'agent', 'models'];
for (const section of requiredSections) {
if (!content.includes(`[${section}]`)) {
result.warnings.push(`Missing recommended section: [${section}]`);
}
}
// Check for required keys
const requiredKeys = ['gateway.url', 'agent.default_model'];
for (const key of requiredKeys) {
if (!content.includes(key.split('.').pop()!)) {
result.warnings.push(`Missing recommended key: ${key}`);
}
}
}
return result;
}
function validateChineseProviders(): ValidationResult {
const configPath = path.join(process.cwd(), 'config/chinese-providers.toml');
const result = validateTomlFile(configPath);
if (result.errors.length === 0 && fs.existsSync(configPath)) {
const content = fs.readFileSync(configPath, 'utf-8');
// Check for Chinese model providers
const providers = ['glm', 'qwen', 'kimi', 'minimax', 'deepseek'];
for (const provider of providers) {
if (!content.includes(`[${provider}`) && !content.includes(`[${provider}]`)) {
result.warnings.push(`Missing Chinese model provider: ${provider}`);
}
}
}
return result;
}
function validatePluginConfigs(): ValidationResult[] {
const results: ValidationResult[] = [];
const pluginsDir = path.join(process.cwd(), 'plugins');
if (!fs.existsSync(pluginsDir)) {
return [{ file: 'plugins/', valid: true, errors: [], warnings: ['No plugins directory found'] }];
}
const plugins = fs.readdirSync(pluginsDir).filter(f =>
fs.statSync(path.join(pluginsDir, f)).isDirectory()
);
for (const plugin of plugins) {
const pluginJsonPath = path.join(pluginsDir, plugin, 'plugin.json');
if (fs.existsSync(pluginJsonPath)) {
const result: ValidationResult = {
file: pluginJsonPath,
valid: true,
errors: [],
warnings: [],
};
try {
const content = fs.readFileSync(pluginJsonPath, 'utf-8');
const config = JSON.parse(content);
// Check required fields
if (!config.name) result.errors.push('Missing required field: name');
if (!config.version) result.warnings.push('Missing recommended field: version');
if (!config.description) result.warnings.push('Missing recommended field: description');
result.valid = result.errors.length === 0;
} catch (e) {
result.valid = false;
result.errors.push(`Invalid JSON: ${(e as Error).message}`);
}
results.push(result);
}
}
return results;
}
function validateEnvironment(): ValidationResult {
const result: ValidationResult = {
file: 'environment',
valid: true,
errors: [],
warnings: [],
};
// Check Node.js version
const nodeVersion = process.version;
const majorVersion = parseInt(nodeVersion.slice(1).split('.')[0], 10);
if (majorVersion < 18) {
result.warnings.push(`Node.js version ${nodeVersion} is below recommended 18.x`);
}
// Check for .env file
const envPath = path.join(process.cwd(), '.env');
if (fs.existsSync(envPath)) {
const envContent = fs.readFileSync(envPath, 'utf-8');
// Check for sensitive patterns
const sensitivePatterns = ['API_KEY', 'SECRET', 'PASSWORD', 'TOKEN'];
for (const pattern of sensitivePatterns) {
const regex = new RegExp(`${pattern}\\s*=\\s*[^\\s]+`, 'g');
const matches = envContent.match(regex);
if (matches) {
for (const match of matches) {
// Check if the value is not a placeholder
const value = match.split('=')[1].trim();
if (!value.includes('your_') && !value.includes('xxx') && value.length > 8) {
result.warnings.push(`Potential exposed secret in .env: ${pattern}`);
}
}
}
}
}
return result;
}
// Main validation
async function main(): Promise<void> {
log('blue', '\n=== ZCLAW Configuration Validator ===\n');
const results: ValidationResult[] = [];
// Run all validators
log('gray', 'Validating main configuration...');
results.push(validateMainConfig());
log('gray', 'Validating Chinese providers configuration...');
results.push(validateChineseProviders());
log('gray', 'Validating plugin configurations...');
results.push(...validatePluginConfigs());
log('gray', 'Validating environment...');
results.push(validateEnvironment());
// Print results
console.log('\n');
for (const result of results) {
const status = result.valid ? '✓' : '✗';
const statusColor = result.valid ? 'green' : 'red';
log(statusColor, `${status} ${result.file}`);
for (const error of result.errors) {
log('red', ` ERROR: ${error}`);
}
for (const warning of result.warnings) {
log('yellow', ` WARN: ${warning}`);
}
}
// Summary
const summary: ConfigValidationSummary = {
timestamp: new Date().toISOString(),
totalFiles: results.length,
validFiles: results.filter(r => r.valid).length,
invalidFiles: results.filter(r => !r.valid).length,
totalErrors: results.reduce((sum, r) => sum + r.errors.length, 0),
totalWarnings: results.reduce((sum, r) => sum + r.warnings.length, 0),
results: results,
};
console.log('\n');
log('blue', '=== Summary ===');
console.log(` Files checked: ${summary.totalFiles}`);
console.log(` Valid: ${colors.green}${summary.validFiles}${colors.reset}`);
console.log(` Invalid: ${colors.red}${summary.invalidFiles}${colors.reset}`);
console.log(` Errors: ${summary.totalErrors}`);
console.log(` Warnings: ${summary.totalWarnings}`);
// Write JSON report
const reportPath = path.join(process.cwd(), 'config-validation-report.json');
fs.writeFileSync(reportPath, JSON.stringify(summary, null, 2));
log('gray', `\nReport saved to: ${reportPath}`);
// Exit with appropriate code
process.exit(summary.invalidFiles > 0 ? 1 : 0);
}
main().catch(console.error);