// Uji otomatis satu perintah (RENCANA 5.4): node tests/all.mjs // 1. sintaks node --check untuk semua berkas JS proyek (game, menu, konten, manifest karakter, uji); cepat, tanpa peramban // 2. smoke node tests/harness.mjs smoke 7 karakter x easy/hard melawan CPU, gagal bila ada galat halaman // 3. pasangan node tests/harness.mjs tests/pairs.mjs 49 pasangan arah dimainkan singkat oleh bot, galat konsol, jurus terpakai, data TIERS, Koleksi // Keluar dengan kode 0 bila semuanya lolos, 1 bila ada yang gagal. Tidak mengubah berkas apa pun. // Env: ALL_SKIP=sintaks,smoke,pasangan (lewati langkah) PAIRS_SECONDS (detik permainan per pasangan, bawaan 24) PW_CORE, CHROME_BIN, HEADED seperti tests/harness.mjs import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { spawnSync } from 'node:child_process'; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const skip = new Set((process.env.ALL_SKIP || '').split(',').map(x => x.trim()).filter(Boolean)); const run = (args, opt = {}) => spawnSync(process.execPath, args, { cwd: root, encoding: 'utf8', maxBuffer: 64 << 20, ...opt }); const tail = (text, n) => String(text || '').trim().split('\n').slice(-n).join('\n'); // berkas JS yang diperiksa sintaksnya: tingkat atas, content/, manifest karakter, dan uji function jsFiles() { const out = []; for (const f of fs.readdirSync(root)) if (/\.(js|mjs)$/.test(f)) out.push(f); for (const dir of ['content', 'tests', 'tools']) if (fs.existsSync(path.join(root, dir))) for (const f of fs.readdirSync(path.join(root, dir))) if (/\.(js|mjs)$/.test(f)) out.push(`${dir}/${f}`); for (const d of fs.readdirSync(path.join(root, 'assets'))) { const m = `assets/${d}/manifest.js`; if (fs.existsSync(path.join(root, m))) out.push(m); } return out.sort(); } const steps = [ ['sintaks', () => { const bad = [], files = jsFiles(); for (const f of files) { const r = run(['--check', f]); if (r.status !== 0) bad.push(`${f}: ${tail(r.stderr, 3)}`); } return { ok: !bad.length, text: bad.length ? bad.join('\n') : `${files.length} berkas JS lolos node --check` }; }], ['smoke', () => { const r = run(['tests/harness.mjs', 'smoke']); return { ok: r.status === 0, text: tail(r.stdout + r.stderr, r.status === 0 ? 2 : 25) }; }], ['pasangan', () => { const r = run(['tests/harness.mjs', 'tests/pairs.mjs']); return { ok: r.status === 0, text: tail(r.stdout + r.stderr, r.status === 0 ? 8 : 30) }; }] ]; const rows = []; for (const [name, fn] of steps) { if (skip.has(name)) { rows.push([name, 'DILEWATI', 0, '']); continue; } const t = Date.now(); let res; try { res = fn(); } catch (e) { res = { ok: false, text: String(e.stack || e) }; } const dt = (Date.now() - t) / 1000; rows.push([name, res.ok ? 'LOLOS' : 'GAGAL', dt, res.text]); console.log(`\n== ${name}: ${res.ok ? 'LOLOS' : 'GAGAL'} (${dt.toFixed(1)} dtk)\n${res.text}`); } const failed = rows.filter(r => r[1] === 'GAGAL'); console.log('\nRingkasan: ' + rows.map(r => `${r[0]} ${r[1]}${r[1] === 'DILEWATI' ? '' : ` (${r[2].toFixed(1)} dtk)`}`).join(' | ')); console.log(failed.length ? `ADA YANG GAGAL: ${failed.map(r => r[0]).join(', ')}` : 'SEMUA UJI LOLOS'); process.exit(failed.length ? 1 : 0);