Files
Laby/tests/audit.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

337 lines
15 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.
#!/usr/bin/env node
/**
* Aegis Labyrinth Automatische Audit-Suite
* Führt die 3 eingebauten Audits aus: Self, Extension, Redesign
* Ausgabe: audit-result.json + Konsolentabelle
*
* 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/>.
*/
import { chromium } from 'playwright';
import path from 'node:path';
import fs from 'node:fs';
import crypto from 'node:crypto';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');
const HTML_PATH = path.resolve(ROOT, 'Aegis-Labyrinth.html');
// ─── Helpers ───────────────────────────────────────────────────────
function timestamp() {
return new Date().toISOString();
}
function sha256(buf) {
return crypto.createHash('sha256').update(buf).digest('hex').slice(0, 16);
}
// ─── Assertion Helpers ─────────────────────────────────────────────
function check(name, actual, expected) {
const passed = actual === expected;
return { name, actual, expected, passed };
}
function checkTrue(name, value) {
return { name, actual: value, expected: true, passed: value === true };
}
function checkFalse(name, value) {
return { name, actual: value, expected: false, passed: value === false };
}
function checkGt(name, value, min = 1) {
const passed = value > min;
return { name, actual: value, expected: `> ${min}`, passed };
}
function checkGte(name, value, min = 0) {
const passed = value >= min;
return { name, actual: value, expected: `>= ${min}`, passed };
}
function checkNotInf(name, value) {
const passed = value !== Infinity && value !== undefined && value !== null;
return { name, actual: value, expected: 'finite', passed };
}
function info(name, value) {
return { name, actual: value, expected: 'info', passed: true };
}
// ─── Self-Audit Assertions ─────────────────────────────────────────
function assertSelfAudit(report) {
const results = [];
// Connectivity: every level must have start(s), an exit, and reachability
for (const c of report.connectivity) {
results.push(checkTrue(`L${c.level} has start`, c.starts > 0));
results.push(checkTrue(`L${c.level} has exit`, c.exit));
results.push(checkTrue(`L${c.level} reachable`, c.reachable));
}
// Attacks: diagnostic probe collect data, don't hard-assert on all towers
// (Support/relay/utility towers intentionally don't fire projectiles)
for (const a of report.attacks) {
results.push(info(`tower ${a.id}: projectiles=${a.projectiles} holos=${a.holos} hurt=${a.hurt}`, true));
}
// Skills: after 4 buys, rank must be at max (at least 1)
for (const s of report.skills) {
results.push(checkGte(`skill ${s.id} rank>=1`, s.rank, 1));
}
// Placement: wall cell should accept, path cell should reject
results.push(checkTrue('placement: wallAccepted (tower on free cell)', report.placement.wallAccepted));
results.push(checkFalse('placement: pathAccepted (tower on path rejected)', report.placement.pathAccepted));
results.push(checkGte('placement: tower count>=1', report.placement.count, 1));
// AutoWave: informational just verify it's a valid state
results.push(info('autoWave: state', report.autoWave.state));
results.push(info('autoWave: active', report.autoWave.active));
// Endless: verify state is valid, endlessWave is a number
results.push(info('endless: state', report.endless.state));
results.push(checkGte('endless: endlessWave>=0', report.endless.endlessWave, 0));
return results;
}
// ─── Extension-Audit Assertions ────────────────────────────────────
function assertExtensionAudit(report) {
const results = [];
// Mortar: diagnostic projectile may be resolved immediately by updateProjectiles
results.push(info('mortar: projectile spawned (may resolve instantly)', report.mortar.projectile));
results.push(checkTrue('mortar: both targets hurt (splash)', report.mortar.hurt));
// Tesla: must damage at least one enemy and apply freeze
results.push(checkGt('tesla: damaged enemies count', report.tesla.damaged));
results.push(checkGt('tesla: frozen count', report.tesla.frozen));
// Reward: dedup should work (second victory doesn't double-grant)
results.push(checkTrue('reward: pointsAfterDuplicate>=1', report.reward.pointsAfterDuplicate >= 1));
results.push(checkTrue('reward: storedRank matches rankAfterBuy', report.reward.storedRank === report.reward.rankAfterBuy));
// Blocker alternate: placement succeeds, route still finite
if (report.blockerAlternate.placed) {
results.push(checkTrue('blockerAlternate: placed', report.blockerAlternate.placed));
results.push(checkTrue('blockerAlternate: route still finite', report.blockerAlternate.finite));
results.push(checkTrue('blockerAlternate: path restored after sell', report.blockerAlternate.soldPathRestored));
} else {
results.push({ name: 'blockerAlternate: no candidate found (acceptable)', passed: true, info: true });
}
// Blocker closed: blocker destroyed by enemy, path restored
if (report.blockerClosed.placed) {
results.push(checkTrue('blockerClosed: placed', report.blockerClosed.placed));
results.push(checkTrue('blockerClosed: destroyed by enemy', report.blockerClosed.destroyed));
results.push(checkTrue('blockerClosed: path restored', report.blockerClosed.pathRestored));
} else {
results.push({ name: 'blockerClosed: no candidate found (acceptable)', passed: true, info: true });
}
return results;
}
// ─── Redesign-Audit Assertions ─────────────────────────────────────
function assertRedesignAudit(report) {
const results = [];
// Kryo: hits at least one target, applies slow
results.push(checkTrue('kryo: target hit', report.kryo.randomLastTarget || report.kryo.visibleSlow));
results.push(checkTrue('kryo: slow applied', report.kryo.visibleSlow));
// Sniper: pierces multiple enemies, no DoT
results.push(checkGt('sniper: pierced count', report.sniper.pierced));
results.push(checkTrue('sniper: no DoT (burn/poison)', report.sniper.noDot));
if (report.sniper.highestAbsoluteHp !== undefined) {
results.push(checkTrue('sniper: highest absolute HP targeting', report.sniper.highestAbsoluteHp));
}
// Mortar (redesign): fixed impact point
results.push(checkTrue('mortar: fixed impact (witness hit, fleeer untouched)', report.mortar.fixedImpact));
// Quantum: line hits, off-line untouched, beam created
results.push(checkGt('quantum: line hits', report.quantum.lineHits));
results.push(checkTrue('quantum: off-line untouched', report.quantum.offLineUntouched));
results.push(checkTrue('quantum: beam created', report.quantum.beam));
// Nano: spawned, damaged target, cleaned up
results.push(checkGt('nano: spawned count', report.nano.spawned));
results.push(checkTrue('nano: damaged target', report.nano.damaged));
results.push(checkTrue('nano: cleaned up', report.nano.cleaned));
return results;
}
// ─── Result Printer ────────────────────────────────────────────────
function printResults(name, results) {
const pass = results.filter(r => r.passed).length;
const fail = results.length - pass;
const status = fail === 0 ? '✅' : '❌';
console.log(` ${status} ${name}: ${pass}/${results.length} passed${fail > 0 ? ` (${fail} FAILED)` : ''}`);
if (fail > 0) {
results.filter(r => !r.passed).forEach(r => {
console.log(` ❌ ${r.name}: actual=${JSON.stringify(r.actual)} expected=${r.expected}`);
});
}
}
// ─── Main ──────────────────────────────────────────────────────────
async function main() {
console.log('═══════════════════════════════════════════════════');
console.log(' AEGIS LABYRINTH AUTOMATED AUDIT SUITE');
console.log('═══════════════════════════════════════════════════');
console.log(` Datei: ${HTML_PATH}`);
console.log(` Zeit: ${timestamp()}\n`);
if (!fs.existsSync(HTML_PATH)) {
console.error('❌ Aegis-Labyrinth.html nicht gefunden!');
process.exit(1);
}
const result = {
timestamp: timestamp(),
file: HTML_PATH,
fileHash: sha256(fs.readFileSync(HTML_PATH)),
self: null,
extension: null,
redesign: null,
assertions: { self: [], extension: [], redesign: [] },
pageErrors: [],
summary: { pass: 0, fail: 0, total: 0 }
};
let browser;
try {
browser = await chromium.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const context = await browser.newContext({ viewport: { width: 1920, height: 1080 } });
const page = await context.newPage();
const pageErrors = [];
page.on('pageerror', (err) => pageErrors.push(err.message));
const url = 'file:///' + HTML_PATH.replace(/\\/g, '/');
await page.goto(url, { waitUntil: 'load', timeout: 30000 });
await page.waitForFunction(() => typeof window.__AEGIS_DEBUG__ !== 'undefined', { timeout: 10000 });
console.log(' ✅ Seite geladen, __AEGIS_DEBUG__ verfügbar\n');
// ─── 1. Self-Audit ────────────────────────────────────────────────────────────
console.log('📡 SELF-AUDIT (runSelfAudit)');
result.self = await page.evaluate(() => {
try {
return window.__AEGIS_DEBUG__.runSelfAudit();
} catch (e) {
return { error: e.message };
}
});
if (result.self.error) {
console.log(` ❌ Error: ${result.self.error}`);
} else {
result.assertions.self = assertSelfAudit(result.self);
printResults('self', result.assertions.self);
}
console.log('');
// ─── 2. Extension-Audit ──────────────────────────────────────────────────────
console.log('🧪 EXTENSION-AUDIT (runExtensionAudit)');
result.extension = await page.evaluate(() => {
try {
return window.__AEGIS_DEBUG__.runExtensionAudit();
} catch (e) {
return { error: e.message };
}
});
if (result.extension.error) {
console.log(` ❌ Error: ${result.extension.error}`);
} else {
result.assertions.extension = assertExtensionAudit(result.extension);
printResults('extension', result.assertions.extension);
}
console.log('');
// ─── 3. Redesign-Audit ───────────────────────────────────────────────────────
console.log('🔬 REDESIGN-AUDIT (runRedesignAudit)');
result.redesign = await page.evaluate(() => {
try {
return window.__AEGIS_DEBUG__.runRedesignAudit();
} catch (e) {
return { error: e.message };
}
});
if (result.redesign.error) {
console.log(` ❌ Error: ${result.redesign.error}`);
} else {
result.assertions.redesign = assertRedesignAudit(result.redesign);
printResults('redesign', result.assertions.redesign);
}
console.log('');
// ─── 4. Page Errors ──────────────────────────────────────────────────────────
console.log('🐛 PAGE ERROR CHECK');
result.pageErrors = pageErrors;
if (pageErrors.length === 0) {
console.log(' ✅ Keine JS-Fehler\n');
} else {
console.log(` ❌ ${pageErrors.length} JS-Fehler:`);
pageErrors.slice(0, 5).forEach(e => console.log(` - ${e}`));
console.log('');
}
// ─── Screenshot ──────────────────────────────────────────────────────────────
const shotPath = path.join(ROOT, 'audit-final.png');
await page.screenshot({ path: shotPath, fullPage: false }).catch(() => {});
if (fs.existsSync(shotPath)) {
console.log(` 📸 Screenshot: ${shotPath}`);
}
// ─── Summary ─────────────────────────────────────────────────────────────────
const allAssertions = [
...result.assertions.self,
...result.assertions.extension,
...result.assertions.redesign
];
const pass = allAssertions.filter(a => a.passed).length;
const fail = allAssertions.length - pass;
const total = allAssertions.length + pageErrors.length;
result.summary = { pass, fail, total };
console.log('═══════════════════════════════════════════════════');
console.log(` ERGEBNIS: ✅ ${pass} PASS ❌ ${fail} FAIL (${allAssertions.length} Checks, ${pageErrors.length} PageErrors)`);
console.log('═══════════════════════════════════════════════════');
const outPath = path.join(ROOT, 'audit-result.json');
fs.writeFileSync(outPath, JSON.stringify(result, null, 2), 'utf-8');
console.log(`\n 📄 Ergebnis gespeichert: ${outPath}`);
await browser.close();
process.exit(fail > 0 || pageErrors.length > 0 ? 1 : 0);
} catch (err) {
console.error('\n❌ Audit fehlgeschlagen:', err.message);
if (browser) await browser.close().catch(() => {});
const outPath = path.join(ROOT, 'audit-result.json');
fs.writeFileSync(outPath, JSON.stringify(result, null, 2), 'utf-8');
process.exit(2);
}
}
main();