337 lines
15 KiB
JavaScript
337 lines
15 KiB
JavaScript
#!/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(); |