383 lines
14 KiB
JavaScript
383 lines
14 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* upgrade-test.mjs — OpenClaw Upgrade Compatibility Test Suite
|
|
*
|
|
* Checks:
|
|
* 1. AGENTS compatibility — all sections present
|
|
* 2. GOVERNANCE compatibility — all sections present, state machine valid
|
|
* 3. PORTFOLIO compatibility — all sections present, project states valid
|
|
* 4. Audit compatibility — all 4 audit scripts run without crash
|
|
* 5. Template compatibility — all 7 templates are valid markdown
|
|
* 6. Cross-ref integrity — no broken references between docs
|
|
* 7. Version Contract compliance — required interfaces exist
|
|
*
|
|
* Usage:
|
|
* node scripts/upgrade-test.mjs
|
|
*
|
|
* Returns exit code 0 if all PASS, 1 if any FAIL.
|
|
*/
|
|
|
|
import { fileURLToPath } from 'url';
|
|
import path from 'path';
|
|
import fs from 'fs';
|
|
import { spawnSync } from 'child_process';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
const WORKSPACE = path.resolve(__dirname, '..');
|
|
|
|
// ── Test Framework ──
|
|
let testsPassed = 0;
|
|
let testsFailed = 0;
|
|
let testsTotal = 0;
|
|
|
|
function test(name, fn) {
|
|
testsTotal++;
|
|
try {
|
|
const result = fn();
|
|
if (result === true || result === undefined) {
|
|
console.log(` ✅ PASS: ${name}`);
|
|
testsPassed++;
|
|
} else {
|
|
console.log(` ❌ FAIL: ${name}`);
|
|
console.log(` ${result}`);
|
|
testsFailed++;
|
|
}
|
|
} catch (e) {
|
|
console.log(` ❌ FAIL: ${name}`);
|
|
console.log(` ${e.message}`);
|
|
testsFailed++;
|
|
}
|
|
}
|
|
|
|
function read(rel) {
|
|
const p = path.join(WORKSPACE, rel);
|
|
if (!fs.existsSync(p)) throw new Error(`File not found: ${rel}`);
|
|
return fs.readFileSync(p, 'utf8');
|
|
}
|
|
|
|
function hasSection(content, sectionPattern) {
|
|
return sectionPattern.test(content);
|
|
}
|
|
|
|
// ── Run Audit Scripts (capture output, don't fail on WARN) ──
|
|
function runScript(rel) {
|
|
const result = spawnSync('node', [path.join(WORKSPACE, rel)], {
|
|
encoding: 'utf8',
|
|
timeout: 30000,
|
|
});
|
|
if (result.error) throw new Error(`Script execution error: ${result.error.message}`);
|
|
return { stdout: result.stdout, stderr: result.stderr, status: result.status };
|
|
}
|
|
|
|
console.log(`\nOpenClaw Upgrade Compatibility Test Suite`);
|
|
console.log(`Workspace: ${WORKSPACE}`);
|
|
console.log(`${'='.repeat(60)}\n`);
|
|
|
|
// ── 1. AGENTS Compatibility ──
|
|
console.log(`\n--- 1. AGENTS Compatibility ---\n`);
|
|
|
|
test('AGENTS.md exists', () => {
|
|
const content = read('AGENTS.md');
|
|
return content.length > 100 ? true : 'File too short';
|
|
});
|
|
|
|
test('AGENTS.md §0 Complexity Classification', () => {
|
|
const content = read('AGENTS.md');
|
|
return hasSection(content, /###\s*0\.\s*Complexity Classification/) ? true : 'Missing §0';
|
|
});
|
|
|
|
test('AGENTS.md §1 Task Tree Construction', () => {
|
|
const content = read('AGENTS.md');
|
|
return hasSection(content, /###\s*1\.\s*Task Tree/) ? true : 'Missing §1';
|
|
});
|
|
|
|
test('AGENTS.md §2 Planning Protocol', () => {
|
|
const content = read('AGENTS.md');
|
|
return hasSection(content, /###\s*2\.\s*Planning Protocol/) ? true : 'Missing §2';
|
|
});
|
|
|
|
test('AGENTS.md §3 Pre-flight Check', () => {
|
|
const content = read('AGENTS.md');
|
|
return hasSection(content, /###\s*3\.\s*Pre-flight/) ? true : 'Missing §3';
|
|
});
|
|
|
|
test('AGENTS.md §4 Failure Replanning', () => {
|
|
const content = read('AGENTS.md');
|
|
return hasSection(content, /###\s*4\.\s*Failure Replanning/) ? true : 'Missing §4';
|
|
});
|
|
|
|
test('AGENTS.md §5 Verification Protocol', () => {
|
|
const content = read('AGENTS.md');
|
|
return hasSection(content, /###\s*5\.\s*Verification Protocol/) ? true : 'Missing §5';
|
|
});
|
|
|
|
test('AGENTS.md §6 Governance Compliance', () => {
|
|
const content = read('AGENTS.md');
|
|
return hasSection(content, /###\s*6\.\s*Governance Compliance/) ? true : 'Missing §6';
|
|
});
|
|
|
|
test('AGENTS.md §7 Auto-Capture Trigger', () => {
|
|
const content = read('AGENTS.md');
|
|
return hasSection(content, /###\s*7\.\s*Auto-Capture/) ? true : 'Missing §7';
|
|
});
|
|
|
|
test('AGENTS.md Learning Loop', () => {
|
|
const content = read('AGENTS.md');
|
|
return hasSection(content, /## Learning Loop/) ? true : 'Missing Learning Loop';
|
|
});
|
|
|
|
test('AGENTS.md Context Budget', () => {
|
|
const content = read('AGENTS.md');
|
|
return hasSection(content, /## Agent Package Context Budget/) ? true : 'Missing Context Budget';
|
|
});
|
|
|
|
// ── 2. GOVERNANCE Compatibility ──
|
|
console.log(`\n--- 2. GOVERNANCE Compatibility ---\n`);
|
|
|
|
test('GOVERNANCE.md exists', () => {
|
|
const content = read('GOVERNANCE.md');
|
|
return content.length > 100 ? true : 'File too short';
|
|
});
|
|
|
|
test('GOVERNANCE.md §1 State Machine (8 states)', () => {
|
|
const content = read('GOVERNANCE.md');
|
|
const states = ['NOT_STARTED', 'IN_PROGRESS', 'BLOCKED', 'WAITING_DEPENDENCY', 'READY_FOR_REVIEW', 'VERIFIED', 'FAILED', 'ARCHIVED'];
|
|
const missing = states.filter(s => !content.includes(s));
|
|
return missing.length === 0 ? true : `Missing states: ${missing.join(', ')}`;
|
|
});
|
|
|
|
test('GOVERNANCE.md §1 Transition Rules', () => {
|
|
const content = read('GOVERNANCE.md');
|
|
return hasSection(content, /Transition Table/) || hasSection(content, /§1\.3/) ? true : 'No transition rules found';
|
|
});
|
|
|
|
test('GOVERNANCE.md §2 Heartbeat Rule', () => {
|
|
const content = read('GOVERNANCE.md');
|
|
return hasSection(content, /§2 Heartbeat/) ? true : 'Missing §2';
|
|
});
|
|
|
|
test('GOVERNANCE.md §3 Blocker Management', () => {
|
|
const content = read('GOVERNANCE.md');
|
|
return hasSection(content, /§3 Blocker/) ? true : 'Missing §3';
|
|
});
|
|
|
|
test('GOVERNANCE.md §4 Replanning Trigger', () => {
|
|
const content = read('GOVERNANCE.md');
|
|
return hasSection(content, /§4 Replanning/) ? true : 'Missing §4';
|
|
});
|
|
|
|
test('GOVERNANCE.md §5 Scope Explosion', () => {
|
|
const content = read('GOVERNANCE.md');
|
|
return hasSection(content, /§5 Scope/) ? true : 'Missing §5';
|
|
});
|
|
|
|
test('GOVERNANCE.md §6 Execution Audit', () => {
|
|
const content = read('GOVERNANCE.md');
|
|
return hasSection(content, /§6 Execution/) ? true : 'Missing §6';
|
|
});
|
|
|
|
test('GOVERNANCE.md §7 Health Score (5 dimensions)', () => {
|
|
const content = read('GOVERNANCE.md');
|
|
const dims = ['Delivery', 'Quality', 'Parallelism', 'Dependency', 'Learning'];
|
|
const missing = dims.filter(d => !content.includes(d + ' Score'));
|
|
return missing.length === 0 ? true : `Missing dimensions: ${missing.join(', ')}`;
|
|
});
|
|
|
|
test('GOVERNANCE.md §8 Auto-Stop Rule', () => {
|
|
const content = read('GOVERNANCE.md');
|
|
return hasSection(content, /§8 Auto-Stop/) ? true : 'Missing §8';
|
|
});
|
|
|
|
test('GOVERNANCE.md §9 Quality Gate', () => {
|
|
const content = read('GOVERNANCE.md');
|
|
return hasSection(content, /§9 Governance Quality/) ? true : 'Missing §9';
|
|
});
|
|
|
|
// ── 3. PORTFOLIO Compatibility ──
|
|
console.log(`\n--- 3. PORTFOLIO Compatibility ---\n`);
|
|
|
|
test('PORTFOLIO.md exists', () => {
|
|
const content = read('PORTFOLIO.md');
|
|
return content.length > 100 ? true : 'File too short';
|
|
});
|
|
|
|
test('PORTFOLIO.md §1 Portfolio Registry', () => {
|
|
const content = read('PORTFOLIO.md');
|
|
return hasSection(content, /§1 Portfolio Registry/) ? true : 'Missing §1';
|
|
});
|
|
|
|
test('PORTFOLIO.md §2 Project States (7 states)', () => {
|
|
const content = read('PORTFOLIO.md');
|
|
const states = ['PLANNING', 'ACTIVE', 'BLOCKED', 'RECOVERY', 'PAUSED', 'COMPLETED', 'ARCHIVED'];
|
|
const missing = states.filter(s => !content.includes(s));
|
|
return missing.length === 0 ? true : `Missing: ${missing.join(', ')}`;
|
|
});
|
|
|
|
test('PORTFOLIO.md §3 Priority System (P0-P3)', () => {
|
|
const content = read('PORTFOLIO.md');
|
|
return (content.includes('P0') && content.includes('P1') && content.includes('P2') && content.includes('P3'))
|
|
? true : 'Missing one or more priority levels';
|
|
});
|
|
|
|
test('PORTFOLIO.md §4 Resource Allocation', () => {
|
|
const content = read('PORTFOLIO.md');
|
|
return hasSection(content, /§4 Resource/) ? true : 'Missing §4';
|
|
});
|
|
|
|
test('PORTFOLIO.md §5 Health Aggregation', () => {
|
|
const content = read('PORTFOLIO.md');
|
|
return hasSection(content, /§5 Project Health/) ? true : 'Missing §5';
|
|
});
|
|
|
|
test('PORTFOLIO.md §6 Portfolio Health Score', () => {
|
|
const content = read('PORTFOLIO.md');
|
|
return hasSection(content, /§6 Portfolio Health/) ? true : 'Missing §6';
|
|
});
|
|
|
|
test('PORTFOLIO.md §7 Learning Reuse', () => {
|
|
const content = read('PORTFOLIO.md');
|
|
return hasSection(content, /§7 Learning Reuse/) ? true : 'Missing §7';
|
|
});
|
|
|
|
test('PORTFOLIO.md §8 Portfolio Audit', () => {
|
|
const content = read('PORTFOLIO.md');
|
|
return hasSection(content, /§8 Portfolio Audit/) ? true : 'Missing §8';
|
|
});
|
|
|
|
test('PORTFOLIO.md §9 Kill Rule / §10 Pause Rule / §11 Promotion Rule', () => {
|
|
const content = read('PORTFOLIO.md');
|
|
return (hasSection(content, /§9 Project Kill/) &&
|
|
hasSection(content, /§10 Project Pause/) &&
|
|
hasSection(content, /§11 Project Promotion/))
|
|
? true : 'Missing one of §9/§10/§11';
|
|
});
|
|
|
|
test('PORTFOLIO.md §12 Executive Dashboard', () => {
|
|
const content = read('PORTFOLIO.md');
|
|
return hasSection(content, /§12 Executive Dashboard/) ? true : 'Missing §12';
|
|
});
|
|
|
|
test('PORTFOLIO.md §13 Quality Gate / §14 Integration', () => {
|
|
const content = read('PORTFOLIO.md');
|
|
return (hasSection(content, /§13 Portfolio Quality/) &&
|
|
hasSection(content, /§14 Integration/))
|
|
? true : 'Missing §13 or §14';
|
|
});
|
|
|
|
// ── 4. Audit Compatibility ──
|
|
console.log(`\n--- 4. Audit Script Compatibility ---\n`);
|
|
|
|
for (const script of ['audit-agents.mjs', 'audit-governance.mjs', 'audit-portfolio.mjs', 'audit-all.mjs']) {
|
|
const rel = path.join('scripts', script);
|
|
test(`${script} exists`, () => {
|
|
return fs.existsSync(path.join(WORKSPACE, rel)) ? true : `Missing: ${rel}`;
|
|
});
|
|
|
|
test(`${script} runs without crash`, () => {
|
|
const result = runScript(rel);
|
|
if (result.status === 1 && result.stderr && !result.stderr.includes('FAIL')) {
|
|
// Check if it's a WARN-only exit (audit scripts exit 0 even on WARN)
|
|
// Actually, audit-all exits with code 0 always — let's just check for crash
|
|
}
|
|
// Crashing would throw — if we're here, it ran
|
|
return result.status !== null ? true : 'Script did not produce exit code';
|
|
});
|
|
}
|
|
|
|
// ── 5. Template Compatibility ──
|
|
console.log(`\n--- 5. Template Compatibility ---\n`);
|
|
|
|
const templates = [
|
|
'progress.log.template', 'blocker.md.template', 'execution-audit.md.template',
|
|
'recovery-report.md.template', 'portfolio.md.template', 'portfolio-audit.md.template',
|
|
'dashboard.md.template'
|
|
];
|
|
|
|
for (const tpl of templates) {
|
|
test(`${tpl} exists`, () => {
|
|
return fs.existsSync(path.join(WORKSPACE, 'scripts/templates', tpl)) ? true : `Missing: scripts/templates/${tpl}`;
|
|
});
|
|
|
|
test(`${tpl} is valid markdown`, () => {
|
|
const content = read(`scripts/templates/${tpl}`);
|
|
if (content.length < 10) return `Template too short: ${content.length} bytes`;
|
|
// Check for unbalanced code fences
|
|
const fences = (content.match(/```/g) || []).length;
|
|
return (fences % 2 === 0) ? true : `Unbalanced code fences (${fences})`;
|
|
});
|
|
}
|
|
|
|
// ── 6. Cross-Ref Integrity ──
|
|
console.log(`\n--- 6. Cross-Reference Integrity ---\n`);
|
|
|
|
test('AGENTS.md → GOVERNANCE.md references resolve', () => {
|
|
const content = read('AGENTS.md');
|
|
const refs = content.match(/GOVERNANCE\.md/g) || [];
|
|
// All should exist since GOVERNANCE.md exists
|
|
return fs.existsSync(path.join(WORKSPACE, 'GOVERNANCE.md'))
|
|
? true
|
|
: 'GOVERNANCE.md not found (referenced from AGENTS.md)';
|
|
});
|
|
|
|
test('GOVERNANCE.md → PORTFOLIO.md references resolve', () => {
|
|
const content = read('GOVERNANCE.md');
|
|
const refs = content.match(/PORTFOLIO\.md/g) || [];
|
|
return fs.existsSync(path.join(WORKSPACE, 'PORTFOLIO.md'))
|
|
? true
|
|
: 'PORTFOLIO.md not found (referenced from GOVERNANCE.md)';
|
|
});
|
|
|
|
test('PORTFOLIO.md → AGENTS.md references resolve', () => {
|
|
const content = read('PORTFOLIO.md');
|
|
const refs = content.match(/AGENTS\.md/g) || [];
|
|
return fs.existsSync(path.join(WORKSPACE, 'AGENTS.md'))
|
|
? true
|
|
: 'AGENTS.md not found (referenced from PORTFOLIO.md)';
|
|
});
|
|
|
|
test('No orphan cross-references', () => {
|
|
const allContent = read('AGENTS.md') + read('GOVERNANCE.md') + read('PORTFOLIO.md');
|
|
// Check that all .md references point to existing files
|
|
const refs = allContent.match(/[A-Z][A-Za-z-]*\.md/g) || [];
|
|
// Filter out date-like patterns (YYYY-MM-DD.md, DD.md)
|
|
const knownNonFiles = ['YYYY-MM-DD.md', 'DD.md', 'MM.md', 'YYYY.md'];
|
|
const fileRefs = refs.filter(r => !knownNonFiles.includes(r));
|
|
const existing = new Set(['AGENTS.md', 'GOVERNANCE.md', 'PORTFOLIO.md', 'MEMORY.md',
|
|
'HEARTBEAT.md', 'SOUL.md', 'IDENTITY.md', 'USER.md', 'TOOLS.md', 'README.md',
|
|
'PROJECT-COMPLETION-REPORT.md', 'COMPATIBILITY-STRATEGY.md', 'SYSTEM-AUDIT.md',
|
|
'version-contract.md', 'upgrade-playbook.md', 'AGENTS-PROTOCOL-FULL.md']);
|
|
const missing = fileRefs.filter(r => !existing.has(r));
|
|
return missing.length === 0 ? true : `Possible unresolved refs: ${[...new Set(missing)].join(', ')}`;
|
|
});
|
|
|
|
// ── 7. Version Contract Compliance ──
|
|
console.log(`\n--- 7. Version Contract Compliance ---\n`);
|
|
|
|
test('version-contract.md exists', () => {
|
|
return fs.existsSync(path.join(WORKSPACE, 'version-contract.md')) ? true : 'Missing version-contract.md';
|
|
});
|
|
|
|
test('Required interfaces are present', () => {
|
|
const agents = read('AGENTS.md');
|
|
const hasCtxSearch = agents.includes('ctx_search');
|
|
const hasMemSearch = agents.includes('memory_search') || agents.includes('memory_get');
|
|
const hasCtxIndex = agents.includes('ctx_index');
|
|
// All are OPTIONAL to have — the test is: if they're referenced, they should be expected
|
|
// This is a soft check
|
|
return true; // Soft pass — version-contract.md defines the expectation
|
|
});
|
|
|
|
// ── Summary ──
|
|
console.log(`\n${'='.repeat(60)}`);
|
|
console.log(`Upgrade Test Suite Complete`);
|
|
console.log(`${'='.repeat(60)}`);
|
|
console.log(` Total: ${testsTotal}`);
|
|
console.log(` PASS: ${testsPassed}`);
|
|
console.log(` FAIL: ${testsFailed}`);
|
|
console.log(` Verdict: ${testsFailed > 0 ? '❌ FAIL — upgrade may break compatibility' : '✅ PASS — all compatible'}`);
|
|
console.log();
|
|
|
|
process.exit(testsFailed > 0 ? 1 : 0);
|