feat: add server and frontend initial - wood chopping

This commit is contained in:
2026-09-05 16:37:45 +05:00
parent c79681dc59
commit f6c338e1b0
28 changed files with 1857 additions and 195 deletions
+49 -19
View File
@@ -1,38 +1,68 @@
/* Дымовой тест M0: сервер должен быть запущен. `pnpm --filter @idle/server smoke` */
/* Дымовой тест M1: сервер должен быть запущен с DEV_HTTP=1, SQLITE_PATH=:memory:.
`pnpm --filter @idle/server smoke` */
import WebSocket from 'ws';
const seen = [];
const BASE = 'http://localhost:3000';
const WS = 'ws://localhost:3000/ws';
function connect(onMsg) {
const ws = new WebSocket(WS);
ws.on('open', () => ws.send(JSON.stringify({ t: 'hello', v: 2 })));
ws.on('message', (d) => onMsg(JSON.parse(String(d))));
return ws;
}
// 1. welcome + полный снапшот
const first = [];
await new Promise((resolve, reject) => {
const ws = new WebSocket('ws://localhost:3000/ws');
ws.on('open', () => ws.send(JSON.stringify({ t: 'hello', v: 1 })));
ws.on('message', (d) => {
seen.push(JSON.parse(String(d)));
if (seen.length >= 2) {
const ws = connect((m) => {
first.push(m);
if (first.length >= 2) {
ws.close();
resolve();
}
});
ws.on('error', reject);
setTimeout(() => reject(new Error('timeout waiting for welcome+snapshot')), 5000);
setTimeout(() => reject(new Error('timeout waiting welcome+snapshot')), 5000);
});
const [welcome, snap] = first;
console.log('welcome:', JSON.stringify({ t: welcome.t, v: welcome.v, tickMs: welcome.tickMs }));
if (welcome.t !== 'welcome' || welcome.v !== 2) throw new Error(`bad welcome: ${JSON.stringify(welcome)}`);
if (snap.t !== 'snapshot' || snap.nodes.length !== 7) throw new Error(`bad snapshot: ${snap.t}, nodes=${snap.nodes?.length}`);
console.log(`snapshot: деревьев=${snap.nodes.length}, зрителей=${snap.avatars.length}`);
const [welcome, snap] = seen;
console.log('welcome:', JSON.stringify(welcome));
console.log(
'avatars:',
snap.avatars.map((a) => `${a.name}@${a.x},${a.y} ${a.face}`).join(' | '),
);
if (welcome.t !== 'welcome' || welcome.v !== 1) throw new Error('bad welcome');
if (snap.t !== 'snapshot' || snap.avatars.length !== 3) throw new Error('bad snapshot');
// 2. команда через dev-эндпоинт
const res = await fetch(`${BASE}/dev/command`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ name: 'Тестер', text: '!рубить' }),
});
if (!res.ok) throw new Error(`dev command failed: ${res.status} ${await res.text()}`);
console.log('dev command: accepted');
// неверная версия протокола — сервер обязан закрыть сокет с кодом 4001
// 3. ждём дельту: аватар «Тестер» должен заспавниться и пойти к дереву
const delta = await new Promise((resolve, reject) => {
const ws = connect((m) => {
if (m.t === 'delta' && m.avatars?.some((a) => a.name === 'Тестер')) {
ws.close();
resolve(m);
}
});
ws.on('error', reject);
setTimeout(() => reject(new Error('no delta with Тестер')), 8000);
});
const tester = delta.avatars.find((a) => a.name === 'Тестер');
console.log(`delta: ${tester.name} moving=${tester.moving} action=${tester.action} target=(${tester.tx},${tester.ty})`);
console.log(`events: ${(delta.events ?? []).map((e) => e.k).join(',') || '—'}`);
if (!tester.moving || tester.action !== 'idle') throw new Error('Тестер должен идти к дереву (idle + moving)');
// 4. неверная версия протокола — close 4001
const closeCode = await new Promise((resolve) => {
const ws = new WebSocket('ws://localhost:3000/ws');
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);
});
console.log('close code for wrong version:', closeCode);
if (closeCode !== 4001) throw new Error(`expected close 4001, got ${closeCode}`);
console.log('WS smoke: OK');