DUNGEONS-33: clean shutdown locks and add optional Telegram 2048

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
2026-09-06 09:14:08 +05:00
parent eb135ac6ef
commit 5e7b24cbaf
9 changed files with 240 additions and 22 deletions

View File

@@ -5,6 +5,7 @@ import { once } from 'node:events';
import { DiscordBot } from '../src/bots/discord.js';
import { TelegramBot } from '../src/bots/telegram.js';
import { createGateway } from '../src/server.js';
import { AccessService } from '../src/bots/service.js';
import { fixture } from './helpers.js';
test('Telegram /start offers Get access; private callback creates request and admin callback delivers number', async () => {
@@ -25,10 +26,61 @@ test('Telegram /start offers Get access; private callback creates request and ad
assert.equal(f.store.getRequest(id).status, 'approved');
assert.ok(f.messages.some(m => m.userId === '5' && m.text.includes('Your number:')));
assert.equal(calls.filter(c => c.method === 'answerCallbackQuery').length, 2);
assert.equal(calls.filter(c => c.method === 'sendGame').length, 0);
await bot.handle({ message: { from: user, chat: { type: 'group' }, text: '/start' } }, f.service);
assert.equal(calls.filter(c => c.method === 'sendMessage').length, 3);
});
test('Telegram sends native 2048 to pending users and handles Play without changing access', async () => {
const f = fixture();
f.config.telegram.gameShortName = 'dungeons_2048';
const calls = [];
const bot = new TelegramBot(f.config, f.store, { fetchImpl: async (url, options) => {
calls.push({ method: url.split('/').at(-1), body: JSON.parse(options.body) });
return Response.json({ ok: true, result: {} });
} });
const service = new AccessService(f.config, f.store, { telegram: bot });
const callback = { id: 'request1', from: { id: 5 }, message: { chat: { type: 'private' } }, data: 'request' };
await bot.handle({ callback_query: callback }, service);
assert.deepEqual(calls.at(-1), { method: 'sendGame', body: { chat_id: '5', game_short_name: 'dungeons_2048' } });
assert.ok(calls.some(c => c.body.chat_id === '100' && c.body.reply_markup));
assert.equal(f.store.state.requests[0].status, 'pending');
const before = structuredClone(f.store.state);
await bot.handle({ callback_query: { id: 'play1', from: { id: 5 }, game_short_name: 'dungeons_2048' } }, service);
assert.deepEqual(calls.at(-1), { method: 'answerCallbackQuery', body: { callback_query_id: 'play1', url: 'https://play2048.co/' } });
assert.deepEqual(f.store.state, before);
for (const name of ['unknown_game', 'dungeons_2048']) {
if (name === 'dungeons_2048') f.config.telegram.gameShortName = '';
await bot.handle({ callback_query: { id: 'unknown', from: { id: 5 }, game_short_name: name } }, service);
assert.equal(calls.at(-1).body.show_alert, true);
assert.equal(calls.at(-1).body.url, undefined);
}
f.config.telegram.gameShortName = 'dungeons_2048';
await service.decide('telegram', '100', f.store.state.requests[0].id, 'approve');
await bot.handle({ callback_query: callback }, service);
assert.equal(calls.filter(c => c.method === 'sendGame').length, 1, 'approved users do not get another waiting game');
});
test('a Telegram game failure does not lose the request or prevent approval', async () => {
const f = fixture();
f.config.telegram.gameShortName = 'dungeons_2048';
const errors = [];
const bot = new TelegramBot(f.config, f.store, {
log: { error: text => errors.push(text) },
fetchImpl: async url => url.endsWith('/sendGame')
? Response.json({ ok: false }, { status: 400 })
: Response.json({ ok: true, result: {} }),
});
const service = new AccessService(f.config, f.store, { telegram: bot });
await bot.handle({ callback_query: { id: 'request1', from: { id: 5 }, message: { chat: { type: 'private' } }, data: 'request' } }, service);
assert.equal(errors.length, 1);
const request = f.store.state.requests[0];
assert.equal(request.status, 'pending');
assert.deepEqual(request.notifiedAdmins, ['100', '101']);
await service.decide('telegram', '100', request.id, 'approve');
assert.equal(f.store.getRequest(request.id).status, 'approved');
});
test('Discord HTTP endpoint validates the exact signed body, rejects stale/forged signatures, and defers approvals', async t => {
const f = fixture();
const { privateKey, publicKey } = generateKeyPairSync('ed25519');