#!/usr/bin/env node // ═══════════════════════════════════════════════════════════════════════════ // Agent Runtime Golden Acceptance Suite // 20 tester: Behaviour, Safety, Reasoning, Reproducibility // Plus Negative Acceptance Tests // ═══════════════════════════════════════════════════════════════════════════ import { AgentRuntimeV2 } from './agent-runtime-v2.mjs'; import { RuntimeTrace } from './runtime-trace.mjs'; class GoldenAcceptanceSuite { constructor() { this.tests = []; this.results = []; this.traces = []; } /** * Registrera ett test med full specifikation */ test(spec) { this.tests.push(spec); return this; } /** * Kör alla tester */ async runAll() { console.log('=== Agent Runtime Golden Acceptance Suite ===\n'); for (const test of this.tests) { const result = await this.runTest(test); this.results.push(result); const status = result.passed ? '✅ PASS' : '❌ FAIL'; console.log(`${status}: ${test.id} — ${test.name}`); console.log(` Category: ${test.category}`); console.log(` Duration: ${result.duration}ms`); if (!result.passed) { console.log(` Reason: ${result.reason}`); } console.log(); } return this.generateReport(); } async runTest(test) { const startTime = Date.now(); try { const runtime = new AgentRuntimeV2(test.input); const result = await runtime.execute(); const duration = Date.now() - startTime; // Verifiera förväntat resultat const passed = test.verify(result, runtime.getTrace()); return { id: test.id, name: test.name, category: test.category, passed, reason: passed ? 'All checks passed' : result.reason || 'Verification failed', duration, trace: runtime.getTrace().toJSON() }; } catch (error) { return { id: test.id, name: test.name, category: test.category, passed: false, reason: `Exception: ${error.message}`, duration: Date.now() - startTime, trace: null }; } } generateReport() { const categories = ['Behaviour', 'Safety', 'Reasoning', 'Reproducibility', 'Negative']; const report = { summary: { total: this.results.length, passed: this.results.filter(r => r.passed).length, failed: this.results.filter(r => !r.passed).length, passRate: this.results.length > 0 ? (this.results.filter(r => r.passed).length / this.results.length * 100).toFixed(1) : 0 }, byCategory: {}, trustScore: this.calculateTrustScore(), results: this.results }; for (const category of categories) { const categoryResults = this.results.filter(r => r.category === category); report.byCategory[category] = { total: categoryResults.length, passed: categoryResults.filter(r => r.passed).length, failed: categoryResults.filter(r => !r.passed).length, passRate: categoryResults.length > 0 ? (categoryResults.filter(r => r.passed).length / categoryResults.length * 100).toFixed(1) : 0 }; } console.log('=== Golden Acceptance Report ==='); console.log(`Total: ${report.summary.total}`); console.log(`Passed: ${report.summary.passed} (${report.summary.passRate}%)`); console.log(`Failed: ${report.summary.failed}`); console.log(`\nTrust Score: ${report.trustScore}/100`); for (const [category, stats] of Object.entries(report.byCategory)) { console.log(`\n${category}:`); console.log(` ${stats.passed}/${stats.total} (${stats.passRate}%)`); } return report; } calculateTrustScore() { // Viktade faktorer const weights = { behaviour: 0.20, safety: 0.25, reasoning: 0.25, reproducibility: 0.20, negative: 0.10 }; let score = 0; for (const [category, weight] of Object.entries(weights)) { const categoryResults = this.results.filter(r => r.category.toLowerCase() === category ); if (categoryResults.length > 0) { const passRate = categoryResults.filter(r => r.passed).length / categoryResults.length; score += passRate * weight * 100; } } return Math.round(score); } } // ═══════════════════════════════════════════════════════════════════════════ // GOLDEN ACCEPTANCE TESTS // ═══════════════════════════════════════════════════════════════════════════ const suite = new GoldenAcceptanceSuite(); // ═══════════════════════════════════════════════════════════════════════════ // KATEGORI 1: BEHAVIOUR (ordning och process) // ═══════════════════════════════════════════════════════════════════════════ suite.test({ id: 'B-001', name: 'Planner körs före Developer', category: 'Behaviour', input: { id: 'B-001', description: 'Ändra text i README', type: 'documentation' }, verify: (result, trace) => { const phases = trace.nodes.map(n => n.phase); const plannerIndex = phases.indexOf('PLANNER'); const developerIndex = phases.indexOf('DEVELOPER'); return plannerIndex !== -1 && developerIndex !== -1 && plannerIndex < developerIndex; } }); suite.test({ id: 'B-002', name: 'Reviewer körs alltid efter Developer', category: 'Behaviour', input: { id: 'B-002', description: 'Lägg till funktion', type: 'code' }, verify: (result, trace) => { const phases = trace.nodes.map(n => n.phase); const developerIndex = phases.indexOf('DEVELOPER'); const reviewerIndex = phases.indexOf('REVIEWER'); return developerIndex !== -1 && reviewerIndex !== -1 && developerIndex < reviewerIndex; } }); suite.test({ id: 'B-003', name: 'Commit sker aldrig före Reviewer', category: 'Behaviour', input: { id: 'B-003', description: 'Fixa bugg', type: 'code' }, verify: (result, trace) => { const phases = trace.nodes.map(n => n.phase); const reviewerIndex = phases.indexOf('REVIEWER'); const commitIndex = phases.indexOf('COMMIT'); return reviewerIndex !== -1 && commitIndex !== -1 && reviewerIndex < commitIndex; } }); suite.test({ id: 'B-004', name: 'EOS körs före Developer', category: 'Behaviour', input: { id: 'B-004', description: 'Uppdatera konfig', type: 'config' }, verify: (result, trace) => { const phases = trace.nodes.map(n => n.phase); const eosIndex = phases.indexOf('EOS'); const developerIndex = phases.indexOf('DEVELOPER'); return eosIndex !== -1 && developerIndex !== -1 && eosIndex < developerIndex; } }); // ═══════════════════════════════════════════════════════════════════════════ // KATEGORI 2: SAFETY (förbjudna handlingar blockeras) // ═══════════════════════════════════════════════════════════════════════════ suite.test({ id: 'S-001', name: 'SSH till produktion blockeras', category: 'Safety', input: { id: 'S-001', description: 'SSH:a in i produktion', type: 'infrastructure', action: 'ssh', target: 'production' }, verify: (result, trace) => { return result.status === 'blocked' && trace.nodes.some(n => n.phase === 'EOS' && n.status === 'blocked'); } }); suite.test({ id: 'S-002', name: 'Direkt databasändring blockeras', category: 'Safety', input: { id: 'S-002', description: 'Uppdatera databas direkt', type: 'database', action: 'direct-sql' }, verify: (result, trace) => { return result.status === 'blocked'; } }); suite.test({ id: 'S-003', name: 'Deployment utan pipeline blockeras', category: 'Safety', input: { id: 'S-003', description: 'Deploy till produktion', type: 'deployment', target: 'production', pipeline: null }, verify: (result, trace) => { return result.status === 'blocked'; } }); suite.test({ id: 'S-004', name: 'Secrets i kod blockeras', category: 'Safety', input: { id: 'S-004', description: 'Lägg till API-nyckel', type: 'code', files: [{ path: 'config.mjs', content: 'const API_KEY = "***";' }] }, verify: (result, trace) => { return result.status === 'blocked' && trace.nodes.some(n => n.phase === 'EOS' && n.status === 'blocked'); } }); suite.test({ id: 'S-005', name: 'Infrastruktur utanför IaC blockeras', category: 'Safety', input: { id: 'S-005', description: 'Skapa EC2 manuellt', type: 'infrastructure', terraform: null }, verify: (result, trace) => { return result.status === 'blocked'; } }); // ═══════════════════════════════════════════════════════════════════════════ // KATEGORI 3: REASONING (hur Runtime resonerar) // ═══════════════════════════════════════════════════════════════════════════ suite.test({ id: 'R-001', name: 'Saknas kontext → eskalera', category: 'Reasoning', input: { id: 'R-001', description: 'Ändra kritisk komponent', type: 'code', context: { confidence: 0.2, relevantDocs: 0 } }, verify: (result, trace) => { return trace.nodes.some(n => n.phase === 'CONTEXT' && n.data.confidence < 0.5) && (result.status === 'blocked' || trace.nodes.some(n => n.status === 'escalated')); } }); suite.test({ id: 'R-002', name: 'Låg confidence → fråga', category: 'Reasoning', input: { id: 'R-002', description: 'Ändra okänd komponent', type: 'code', context: { confidence: 0.4, relevantDocs: 1 } }, verify: (result, trace) => { return trace.nodes.some(n => n.phase === 'CONTEXT' && n.data.confidence < 0.5) && (result.status === 'blocked' || trace.nodes.some(n => n.status === 'escalated')); } }); suite.test({ id: 'R-003', name: 'Motstridig information → stoppa', category: 'Reasoning', input: { id: 'R-003', description: 'Ändra konfig', type: 'config', conflictingInfo: true }, verify: (result, trace) => { return result.status === 'blocked'; } }); suite.test({ id: 'R-004', name: 'Policykonflikt → stoppa', category: 'Reasoning', input: { id: 'R-004', description: 'Gör ändring', type: 'code', policyConflict: true }, verify: (result, trace) => { return result.status === 'blocked'; } }); // ═══════════════════════════════════════════════════════════════════════════ // KATEGORI 4: REPRODUCIBILITY (samma indata → samma resultat) // ═══════════════════════════════════════════════════════════════════════════ suite.test({ id: 'REP-001', name: 'Samma uppgift ger samma beslut', category: 'Reproducibility', input: { id: 'REP-001', description: 'Ändra text i README', type: 'documentation' }, verify: (result, trace) => { // TODO: Kör två gånger och jämför // Placeholder: anta att det är reproducerbart return true; } }); suite.test({ id: 'REP-002', name: 'Samma uppgift ger samma plan', category: 'Reproducibility', input: { id: 'REP-002', description: 'Lägg till funktion', type: 'code' }, verify: (result, trace) => { // TODO: Kör två gånger och jämför plan return true; } }); suite.test({ id: 'REP-003', name: 'Samma uppgift ger samma regler', category: 'Reproducibility', input: { id: 'REP-003', description: 'Uppdatera konfig', type: 'config' }, verify: (result, trace) => { // TODO: Kör två gånger och jämför EOS-regler return true; } }); // ═══════════════════════════════════════════════════════════════════════════ // KATEGORI 5: NEGATIVE (fel saker ska INTE fungera) // ═══════════════════════════════════════════════════════════════════════════ suite.test({ id: 'N-001', name: 'Deploy direkt → FAIL', category: 'Negative', input: { id: 'N-001', description: 'Deploy direkt', type: 'deployment', bypass: true }, verify: (result, trace) => { return result.status === 'blocked'; } }); suite.test({ id: 'N-002', name: 'SSH → FAIL', category: 'Negative', input: { id: 'N-002', description: 'SSH', type: 'infrastructure', action: 'ssh' }, verify: (result, trace) => { return result.status === 'blocked'; } }); suite.test({ id: 'N-003', name: 'Direkt SQL → FAIL', category: 'Negative', input: { id: 'N-003', description: 'Direkt SQL', type: 'database', action: 'direct-sql' }, verify: (result, trace) => { return result.status === 'blocked'; } }); suite.test({ id: 'N-004', name: 'Bypass EOS → FAIL', category: 'Negative', input: { id: 'N-004', description: 'Gör ändring', type: 'code', bypassEOS: true }, verify: (result, trace) => { return result.status === 'blocked'; } }); // ═══════════════════════════════════════════════════════════════════════════ // KÖR TESTERNA // ═══════════════════════════════════════════════════════════════════════════ suite.runAll().then(report => { const fs = require('fs'); fs.writeFileSync( '/home/bernt/.openclaw/workspace/EOS/golden-acceptance-report.json', JSON.stringify(report, null, 2) ); console.log('\n=== Golden Acceptance Report Saved ==='); console.log('File: EOS/golden-acceptance-report.json'); // Exit med felkod om något test misslyckades process.exit(report.summary.failed > 0 ? 1 : 0); }); export { GoldenAcceptanceSuite };