Files
idle-xboct/packages/server/smoke.mjs
T

135 lines
5.3 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.
/* Дымовой тест M4 (кооп-костёр), две фазы:
node smoke.mjs phase1 — вклад, уровень, ledger (сервер с DEV_HTTP=1 + файловая база)
node smoke.mjs phase2 — после рестарта сервера: персистентность + декей + guard версии */
import { readFileSync, writeFileSync, unlinkSync } from 'node:fs';
import WebSocket from 'ws';
const BASE = 'http://localhost:3000';
const WS = 'ws://localhost:3000/ws';
const PROGRESS_FILE = new URL('./smoke-progress.tmp', import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1');
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
for (let i = 0; i < 30; i++) {
try {
const r = await fetch(`${BASE}/healthz`);
if (r.ok) break;
} catch {}
await sleep(500);
}
const COOLDOWN = 1400;
function connect(onMsg) {
const ws = new WebSocket(WS);
ws.on('open', () => ws.send(JSON.stringify({ t: 'hello', v: 5 })));
ws.on('message', (d) => onMsg(JSON.parse(String(d))));
return ws;
}
async function dev(path, body) {
const res = await fetch(BASE + path, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`POST ${path} -> ${res.status} ${await res.text()}`);
}
async function state() {
const res = await fetch(`${BASE}/dev/state`);
return res.json();
}
function waitForDelta(pred, timeoutMs = 9000) {
return new Promise((resolve, reject) => {
const ws = connect((m) => {
if (m.t === 'delta' && pred(m)) {
ws.close();
resolve(m);
}
});
ws.on('error', reject);
setTimeout(() => {
ws.terminate();
reject(new Error('timeout waiting delta'));
}, timeoutMs);
});
}
function firstSnapshot() {
return new Promise((resolve, reject) => {
const seen = [];
const ws = connect((m) => {
seen.push(m);
if (seen.length >= 2) {
ws.close();
resolve(seen[1]);
}
});
ws.on('error', reject);
setTimeout(() => reject(new Error('timeout')), 5000);
});
}
const phase = process.argv[2] ?? 'phase1';
if (phase === 'phase1') {
const snap = await firstSnapshot();
console.log('welcome v5 | campfire:', JSON.stringify(snap.campfire));
if (!snap.campfire || snap.campfire.level < 1) throw new Error('no campfire state');
// спавн, брёвна, !костёр
await dev('/dev/command', { name: 'Тестер', text: '!рубить' });
await waitForDelta((m) => m.avatars?.some((a) => a.name === 'Тестер' && a.moving));
await dev('/dev/give', { name: 'Тестер', item: 'log', qty: 2 });
await dev('/dev/tp', { name: 'Тестер', x: 1900, y: 505 });
await sleep(COOLDOWN);
await dev('/dev/command', { name: 'Тестер', text: '!костёр' });
const d1 = await waitForDelta((m) => m.avatars?.some((a) => a.name === 'Тестер' && a.action === 'feed'));
const feeder = d1.avatars.find((a) => a.name === 'Тестер');
console.log('feed: action=' + feeder.action, 'tool=' + feeder.tool);
if (feeder.action !== 'feed' || feeder.tool !== 'log') throw new Error('bad feed state');
const d2 = await waitForDelta((m) => m.events?.some((e) => e.k === 'fire' && e.name === 'Тестер'), 15000);
const fireEv = d2.events.find((e) => e.k === 'fire');
console.log(`fire: прогресс=${fireEv.progress}, личный вклад=${fireEv.contributed}`);
if (fireEv.contributed < 10 || fireEv.progress < 10) throw new Error('campfire did not grow');
await dev('/dev/fire', { points: 95 });
const d3 = await waitForDelta((m) => m.events?.some((e) => e.k === 'firelevel'), 5000);
const lv = d3.events.find((e) => e.k === 'firelevel').level;
const st1 = await state();
console.log(
`firelevel: ${lv}, campfire.level=${st1.campfire.level}, топ: ${st1.campfire.top
.map((t) => t.name + '=' + t.contributed)
.join(', ')}`,
);
if (lv < 2 || st1.campfire.level < 2) throw new Error('campfire level did not rise');
if (!st1.campfire.top.some((t) => t.name === 'Тестер')) throw new Error('ledger missing');
writeFileSync(PROGRESS_FILE, String(st1.campfire.progress), 'utf8');
console.log(`phase1 OK (progress=${st1.campfire.progress}). Перезапусти сервер и запусти: node smoke.mjs phase2`);
} else if (phase === 'phase2') {
const before = Number(readFileSync(PROGRESS_FILE, 'utf8'));
const snap = await firstSnapshot();
console.log(`after restart: прогресс ${snap.campfire.progress} (был ${before}), уровень ${snap.campfire.level}`);
if (snap.campfire.progress >= before) throw new Error('decay did not happen after restart');
if (snap.campfire.level < 2) throw new Error('campfire level lost after restart');
if (!snap.campfire.top.some((t) => t.name === 'Тестер')) throw new Error('ledger lost after restart');
try {
unlinkSync(PROGRESS_FILE);
} catch {}
const closeCode = await new Promise((resolve) => {
const ws = new WebSocket(WS);
ws.on('open', () => ws.send(JSON.stringify({ t: 'hello', v: 999 })));
ws.on('close', (code) => resolve(code));
setTimeout(() => resolve('no-close'), 3000);
});
if (closeCode !== 4001) throw new Error(`expected close 4001, got ${closeCode}`);
console.log('WS smoke: OK');
} else {
throw new Error(`unknown phase: ${phase}`);
}