Files
Laby/tests/report.mjs
T
Lila-Kuh ea2a2f2cbf
Aegis CI / audit (push) Failing after 1m7s
Aegis CI / selfplay (push) Skipped
Aegis CI / perf (push) Failing after 3s
Aegis CI / report (push) Failing after 2s
Initial project version
2026-08-29 01:45:19 +02:00

188 lines
7.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// report.mjs erzeugt report.html aus audit-result.json, perf-result.json,
// selfplay-results.json (falls vorhanden).
// Pass/Fail wird direkt aus den audit.assertions und audit.summary gelesen
// (authoritative Quelle aus audit.mjs), nicht mit eigenen Heuristiken.
//
// Copyright (C) 2026 Lila-Kuh
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
// Usage: node report.mjs
import path from 'path';
import fs from 'fs';
import url from 'url';
const HERE = path.dirname(url.fileURLToPath(import.meta.url));
const ROOT = path.resolve(HERE, '..');
const auditPath = path.join(ROOT, 'audit-result.json');
const perfPath = path.join(ROOT, 'perf-result.json');
const selfPath = path.join(ROOT, 'selfplay-results.json');
function loadJson(p) {
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; }
}
const audit = loadJson(auditPath);
const perf = loadJson(perfPath);
const selfplay = loadJson(selfPath);
function statusColor(ok) { return ok ? '#4caf50' : '#f44336'; }
/** true wenn Liste nicht leer ist und alle Assertions `passed: true` haben */
function allPass(list) {
return Array.isArray(list) && list.length > 0 && list.every(a => a.passed === true);
}
/** Anzahl der bestanden Assertions in einer Liste */
function countPassed(list) {
return Array.isArray(list) ? list.filter(a => a.passed === true).length : 0;
}
// ─── Audit Summary ────────────────────────────────────────────────────────────
let auditRows = '';
let auditPass = false;
if (audit) {
const selfA = audit.assertions?.self ?? [];
const extA = audit.assertions?.extension ?? [];
const redA = audit.assertions?.redesign ?? [];
// Assertions aus audit.mjs nach Themen gruppieren (Name-Prefix)
const conn = selfA.filter(a => /^L\d+ /.test(a.name));
const atk = selfA.filter(a => a.name.startsWith('tower '));
const skl = selfA.filter(a => a.name.startsWith('skill '));
const plc = selfA.filter(a => a.name.startsWith('placement'));
const rows = [
['Connectivity', allPass(conn),
`${countPassed(conn)}/${conn.length} level checks`],
['Attacks', allPass(atk),
`${countPassed(atk)}/${atk.length} tower checks (inkl. Support/Relay)`],
['Skills', allPass(skl),
`${countPassed(skl)}/${skl.length} skills · rank ≥ 1`],
['Placement', allPass(plc),
audit.self?.placement
? `wall=${audit.self.placement.wallAccepted} path=${audit.self.placement.pathAccepted}`
: 'N/A'],
];
if (extA.length > 0) {
rows.push(['Extension (Mortar/Tesla/Blocker)', allPass(extA),
`${countPassed(extA)}/${extA.length} checks`]);
}
if (redA.length > 0) {
rows.push(['Redesign (Kryo/Sniper/Quantum/Nano)', allPass(redA),
`${countPassed(redA)}/${redA.length} checks`]);
}
auditRows = rows.map(([name, ok, detail]) =>
`<tr><td>${name}</td>` +
`<td style="color:${statusColor(ok)};font-weight:bold">${ok ? '✅ PASS' : '❌ FAIL'}</td>` +
`<td>${detail}</td></tr>`
).join('\n');
// Gesamtverdict: authoritative audit.summary + pageErrors
const pageErrors = audit.pageErrors?.length ?? 1;
auditPass = (audit.summary?.fail ?? -1) === 0 && pageErrors === 0;
} else {
auditRows = '<tr><td colspan="3" style="color:#999">audit-result.json not found</td></tr>';
}
// ─── Perf Summary ─────────────────────────────────────────────────────────────
let perfRow = '';
let perfOk = false;
if (perf) {
perfOk = perf.pass === true;
perfRow = `<tr><td>FPS</td>` +
`<td style="color:${statusColor(perfOk)};font-weight:bold">${perf.fps} fps</td>` +
`<td>target ≥ ${perf.targetFps} · duration ${perf.durationMs}ms</td></tr>`;
if (perf.heap) perfRow +=
`<tr><td>Heap</td><td style="color:#4caf50">${perf.heap.usedMB} MB</td>` +
`<td>total ${perf.heap.totalMB} MB</td></tr>`;
} else {
perfRow = '<tr><td colspan="3" style="color:#999">perf-result.json not found</td></tr>';
}
// ─── Self-Play Summary ────────────────────────────────────────────────────────
let selfRows = '';
let selfOk = false;
if (selfplay) {
const s = selfplay.summary;
selfOk = s.winRate >= 50;
selfRows = `
<tr><td>Episodes</td><td style="color:#4caf50">${s.total}</td><td>winRate ${s.winRate.toFixed(1)}%</td></tr>
<tr><td>Wins</td><td style="color:${statusColor(selfOk)};font-weight:bold">${s.wins}</td><td>Losses: ${s.losses}</td></tr>
<tr><td>Avg Kills</td><td style="color:#4caf50">${s.avgKills}</td><td>Avg Wave: ${s.avgWaves}</td></tr>
`;
} else {
selfRows = '<tr><td colspan="3" style="color:#999">selfplay-results.json not found (nightly only)</td></tr>';
}
const overallPass = auditPass && perfOk && (selfplay ? selfOk : true);
// ─── HTML ─────────────────────────────────────────────────────────────────────
const html = `<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Aegis Labyrinth CI Report</title>
<style>
body{font-family:'Segoe UI',system-ui,monospace;background:#0d1117;color:#c9d1d9;padding:30px;margin:0}
h1{font-size:1.6em;margin-bottom:4px}
.ts{color:#8b949e;font-size:.85em;margin-bottom:24px}
.badge{display:inline-block;padding:4px 14px;border-radius:12px;font-weight:bold;font-size:.9em;margin-bottom:20px}
.pass{background:#1a3a2a;color:#4caf50}.fail{background:#3a1a1a;color:#f44336}
h2{font-size:1.1em;margin:24px 0 8px;color:#8b949e;text-transform:uppercase;letter-spacing:1px}
table{border-collapse:collapse;width:100%;margin-bottom:20px}
th,td{padding:10px 14px;text-align:left;border-bottom:1px solid #21262d}
th{background:#161b22;color:#8b949e;font-size:.85em;text-transform:uppercase;letter-spacing:.5px}
tr:hover{background:#161b22}
.footer{color:#484f58;font-size:.8em;margin-top:40px;border-top:1px solid #21262d;padding-top:16px}
</style>
</head>
<body>
<h1>⚔️ Aegis Labyrinth CI Report</h1>
<p class="ts">Timestamp: ${new Date().toISOString()} · Branch: <code>unknown</code></p>
<span class="badge ${overallPass ? 'pass' : 'fail'}">${overallPass ? '✅ ALL CHECKS PASSED' : '❌ SOME CHECKS FAILED'}</span>
<h2>Audits</h2>
<table>
<tr><th>Check</th><th>Status</th><th>Detail</th></tr>
${auditRows}
</table>
<h2>Performance</h2>
<table>
<tr><th>Metric</th><th>Value</th><th>Info</th></tr>
${perfRow}
</table>
<h2>Self-Play</h2>
<table>
<tr><th>Metric</th><th>Value</th><th>Info</th></tr>
${selfRows}
</table>
<div class="footer">
Generated by <code>report.mjs</code> · Aegis Labyrinth v${audit ? '1.0.2' : '?'} · ${new Date().toLocaleString('de-DE')}
</div>
</body>
</html>`;
const reportPath = path.join(ROOT, 'report.html');
fs.writeFileSync(reportPath, html);
console.log(`✅ Report generated → ${reportPath} (${overallPass ? 'ALL PASS' : 'SOME FAILURES'})`);