UNPKG

prprompts-flutter-generator

Version:

AI-powered Flutter development with full automation + official extension support - Generate 32 security-audited guides & auto-implement in 2-3 hours. NEW v5.1: Official Claude Code plugin with hooks, Gemini TOML commands, Qwen MCP settings. Features: Comp

418 lines (356 loc) 12.3 kB
#!/usr/bin/env node /** * PRPROMPTS Skill Testing Script * * Tests individual skills with mock inputs and validates outputs * * Usage: * node scripts/test-skill.js <skill-id> [options] * node scripts/test-skill.js prd-analyzer --input tests/fixtures/sample-prd.md * node scripts/test-skill.js prprompts-generator --expect-files "PRPROMPTS/*.md" --expect-count 32 * node scripts/test-skill.js flutter-bootstrapper --mock * * Options: * --input <path> Input file path * --expect-output <json> Expected output JSON * --expect-files <glob> Expected output file pattern * --expect-count <n> Expected file count * --mock Use mock inputs from skill.json * --verbose Show detailed output */ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); // ANSI color codes const colors = { reset: '\x1b[0m', green: '\x1b[32m', red: '\x1b[31m', yellow: '\x1b[33m', blue: '\x1b[34m', cyan: '\x1b[36m' }; class SkillTester { constructor(skillId, options = {}) { this.skillId = skillId; this.options = options; this.errors = []; this.warnings = []; this.passed = 0; this.failed = 0; } log(message, color = 'reset') { if (this.options.verbose || color !== 'reset') { console.log(`${colors[color]}${message}${colors.reset}`); } } error(message) { this.errors.push(message); this.failed++; this.log(`❌ ${message}`, 'red'); } warning(message) { this.warnings.push(message); this.log(`⚠️ ${message}`, 'yellow'); } success(message) { this.passed++; this.log(`✅ ${message}`, 'green'); } info(message) { this.log(`ℹ️ ${message}`, 'cyan'); } /** * Find skill.json file */ findSkillFile() { // Try to find skill in any category const categories = ['prprompts-core', 'automation', 'validation', 'utilities', 'repository-meta', 'development-workflow']; for (const category of categories) { const skillPath = path.join('.claude', 'skills', category, this.skillId, 'skill.json'); if (fs.existsSync(skillPath)) { return skillPath; } } // Try direct path (category/skill-name format) if (this.skillId.includes('/')) { const skillPath = path.join('.claude', 'skills', this.skillId, 'skill.json'); if (fs.existsSync(skillPath)) { return skillPath; } } return null; } /** * Load and validate skill definition */ loadSkill() { this.info(`Loading skill: ${this.skillId}`); const skillPath = this.findSkillFile(); if (!skillPath) { this.error(`Skill not found: ${this.skillId}`); return null; } this.skillPath = skillPath; this.skillDir = path.dirname(skillPath); this.info(`Found at: ${skillPath}`); try { const skillContent = fs.readFileSync(skillPath, 'utf8'); const skill = JSON.parse(skillContent); this.success('Skill JSON is valid'); return skill; } catch (err) { this.error(`Failed to parse skill.json: ${err.message}`); return null; } } /** * Validate skill structure */ validateStructure(skill) { this.info('Validating skill structure...'); // Check required fields const requiredFields = ['id', 'name', 'version', 'category', 'description']; for (const field of requiredFields) { if (!skill[field]) { this.error(`Missing required field: ${field}`); } else { this.success(`Has ${field}: ${skill[field]}`); } } // Check version format if (skill.version && !/^\d+\.\d+\.\d+$/.test(skill.version)) { this.error(`Invalid version format: ${skill.version} (should be semver)`); } // Check category const validCategories = ['prprompts-core', 'automation', 'validation', 'utilities', 'repository-meta', 'development-workflow']; if (skill.category && !validCategories.includes(skill.category)) { this.error(`Invalid category: ${skill.category}`); } // Check for skill.md const skillMdPath = path.join(this.skillDir, 'skill.md'); if (fs.existsSync(skillMdPath)) { this.success('skill.md exists'); } else { this.error('skill.md is missing'); } // Check for README.md (junior docs) const readmePath = path.join(this.skillDir, 'README.md'); if (fs.existsSync(readmePath)) { this.success('README.md exists (junior documentation)'); } else { this.warning('README.md is missing (recommended for junior developers)'); } // Check for examples.md const examplesPath = path.join(this.skillDir, 'examples.md'); if (fs.existsSync(examplesPath)) { this.success('examples.md exists'); } else { this.warning('examples.md is missing (recommended)'); } } /** * Test skill with mock inputs */ testWithMockInputs(skill) { this.info('Testing with mock inputs...'); if (!skill.cicd || !skill.cicd.testable) { this.warning('Skill is not configured for CI/CD testing'); return; } const mockInputs = skill.cicd.mockInputs || {}; this.info(`Mock inputs: ${JSON.stringify(mockInputs, null, 2)}`); // Validate inputs exist for (const [key, value] of Object.entries(mockInputs)) { if (typeof value === 'string' && value.endsWith('.md')) { // Check if file exists if (fs.existsSync(value)) { this.success(`Mock input file exists: ${value}`); } else { this.error(`Mock input file missing: ${value}`); } } } // Check expected outputs const expectedOutputs = skill.cicd.expectedOutputs || {}; if (Object.keys(expectedOutputs).length > 0) { this.success(`Expected outputs defined: ${Object.keys(expectedOutputs).join(', ')}`); } else { this.warning('No expected outputs defined for validation'); } } /** * Check multi-AI parity */ checkMultiAIParity() { this.info('Checking multi-AI parity...'); const qwenPath = this.skillPath.replace('.claude', '.qwen'); const geminiPath = this.skillPath.replace('.claude', '.gemini'); if (fs.existsSync(qwenPath)) { this.success('Skill exists in .qwen/'); } else { this.error('Skill missing in .qwen/'); } if (fs.existsSync(geminiPath)) { this.success('Skill exists in .gemini/'); } else { this.error('Skill missing in .gemini/'); } // Check if files are identical try { const claudeContent = fs.readFileSync(this.skillPath, 'utf8'); const qwenContent = fs.existsSync(qwenPath) ? fs.readFileSync(qwenPath, 'utf8') : ''; const geminiContent = fs.existsSync(geminiPath) ? fs.readFileSync(geminiPath, 'utf8') : ''; if (claudeContent === qwenContent && claudeContent === geminiContent) { this.success('All AI skill files are identical'); } else { this.warning('AI skill files differ (may need sync)'); } } catch (err) { this.warning(`Could not compare files: ${err.message}`); } } /** * Validate against JSON schema */ validateAgainstSchema(skill) { this.info('Validating against JSON schema...'); const schemaPath = path.join('schemas', 'skill-schema.json'); if (!fs.existsSync(schemaPath)) { this.warning('JSON schema not found, skipping validation'); return; } try { // Try to use ajv if available const ajv = require('ajv'); const Ajv = ajv.default || ajv; const ajvInstance = new Ajv(); const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8')); const validate = ajvInstance.compile(schema); const valid = validate(skill); if (valid) { this.success('Skill passes JSON schema validation'); } else { this.error(`Schema validation failed: ${JSON.stringify(validate.errors, null, 2)}`); } } catch (err) { this.warning(`Could not validate schema (ajv not installed): ${err.message}`); } } /** * Run all tests */ async run() { console.log(''); console.log('='.repeat(60)); console.log(`🧪 PRPROMPTS Skill Test: ${this.skillId}`); console.log('='.repeat(60)); console.log(''); const skill = this.loadSkill(); if (!skill) { return this.reportResults(); } this.validateStructure(skill); this.validateAgainstSchema(skill); this.testWithMockInputs(skill); this.checkMultiAIParity(); return this.reportResults(); } /** * Report test results */ reportResults() { console.log(''); console.log('='.repeat(60)); console.log('📊 Test Results'); console.log('='.repeat(60)); console.log(''); console.log(`${colors.green}✅ Passed: ${this.passed}${colors.reset}`); console.log(`${colors.red}❌ Failed: ${this.failed}${colors.reset}`); console.log(`${colors.yellow}⚠️ Warnings: ${this.warnings.length}${colors.reset}`); console.log(''); if (this.errors.length > 0) { console.log(`${colors.red}Errors:${colors.reset}`); this.errors.forEach((err, i) => console.log(` ${i + 1}. ${err}`)); console.log(''); } if (this.warnings.length > 0 && this.options.verbose) { console.log(`${colors.yellow}Warnings:${colors.reset}`); this.warnings.forEach((warn, i) => console.log(` ${i + 1}. ${warn}`)); console.log(''); } const success = this.failed === 0; if (success) { console.log(`${colors.green}🎉 All tests passed!${colors.reset}`); } else { console.log(`${colors.red}💥 Some tests failed${colors.reset}`); } console.log(''); return success ? 0 : 1; } } // Parse command line arguments function parseArgs() { const args = process.argv.slice(2); if (args.length === 0 || args[0] === '--help' || args[0] === '-h') { console.log(` Usage: node scripts/test-skill.js <skill-id> [options] Arguments: <skill-id> Skill identifier (e.g., prd-analyzer) Options: --input <path> Input file path --expect-output <json> Expected output JSON --expect-files <glob> Expected output file pattern --expect-count <n> Expected file count --mock Use mock inputs from skill.json --verbose Show detailed output --help, -h Show this help message Examples: node scripts/test-skill.js prd-analyzer node scripts/test-skill.js prd-analyzer --input tests/fixtures/sample-prd.md node scripts/test-skill.js prprompts-generator --expect-count 32 node scripts/test-skill.js flutter-bootstrapper --mock --verbose `); process.exit(0); } const skillId = args[0]; const options = { verbose: args.includes('--verbose'), mock: args.includes('--mock') }; // Parse other options for (let i = 1; i < args.length; i++) { const arg = args[i]; const nextArg = args[i + 1]; if (arg === '--input' && nextArg) { options.input = nextArg; i++; } else if (arg === '--expect-output' && nextArg) { options.expectOutput = JSON.parse(nextArg); i++; } else if (arg === '--expect-files' && nextArg) { options.expectFiles = nextArg; i++; } else if (arg === '--expect-count' && nextArg) { options.expectCount = parseInt(nextArg, 10); i++; } } return { skillId, options }; } // Main execution async function main() { const { skillId, options } = parseArgs(); const tester = new SkillTester(skillId, options); const exitCode = await tester.run(); process.exit(exitCode); } // Run if called directly if (require.main === module) { main().catch(err => { console.error(`${colors.red}Fatal error: ${err.message}${colors.reset}`); console.error(err.stack); process.exit(1); }); } module.exports = { SkillTester };