Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
669 lines
20 KiB
JavaScript
669 lines
20 KiB
JavaScript
// ============================================================
|
||
// Tests: TelegramAdapter
|
||
// ============================================================
|
||
// Tests the Telegram adapter WITHOUT actual Telegram API calls.
|
||
// Mocks the API layer, tests routing, keyword parsing,
|
||
// sticker management, lifecycle hooks, and feedback loop.
|
||
// ============================================================
|
||
|
||
import { TelegramAdapter, KEYWORDS, DEFAULT_STICKER_EMOTION_MAP, thinkingDelay } from './telegram.js';
|
||
import { PesPlatformAdapter, RENDER_MODE, MEDIA_TYPE, PRESENCE } from './base.js';
|
||
import { Pes } from '../core/pes.js';
|
||
import { tmpdir } from 'node:os';
|
||
import { join } from 'node:path';
|
||
import { mkdirSync, existsSync, rmSync } from 'node:fs';
|
||
|
||
let testNum = 0;
|
||
let passed = 0;
|
||
let failed = 0;
|
||
|
||
function assert(condition, msg) {
|
||
testNum++;
|
||
if (condition) {
|
||
passed++;
|
||
console.log(` ✅ ${testNum}. ${msg}`);
|
||
} else {
|
||
failed++;
|
||
console.log(` ❌ ${testNum}. ${msg}`);
|
||
}
|
||
}
|
||
|
||
function section(name) {
|
||
console.log(`\n── ${name} ──`);
|
||
}
|
||
|
||
// ── Test setup ────────────────────────────────────────────
|
||
|
||
const TEST_DIR = join(tmpdir(), `pes-telegram-test-${Date.now()}`);
|
||
mkdirSync(TEST_DIR, { recursive: true });
|
||
|
||
function makePes(name = 'TestDog') {
|
||
const dbPath = join(TEST_DIR, `${name}-${Date.now()}.db`);
|
||
const pes = new Pes({
|
||
name,
|
||
ownerId: 'nikitron',
|
||
dbPath,
|
||
autoTick: false,
|
||
seed: 0.42,
|
||
});
|
||
pes.birth();
|
||
return pes;
|
||
}
|
||
|
||
function makeAdapter(pes, overrides = {}) {
|
||
const adapter = new TelegramAdapter(pes, {
|
||
botToken: 'FAKE_TOKEN_123',
|
||
chatId: '12345',
|
||
autoListen: false,
|
||
useThinkingDelay: false,
|
||
...overrides,
|
||
});
|
||
return adapter;
|
||
}
|
||
|
||
|
||
// ════════════════════════════════════════════════════════════
|
||
// TEST SUITE
|
||
// ════════════════════════════════════════════════════════════
|
||
|
||
console.log('🐾 TelegramAdapter Tests\n');
|
||
|
||
|
||
// ── 1. Constructor ──
|
||
|
||
section('Constructor');
|
||
|
||
{
|
||
const pes = makePes('Thor');
|
||
const adapter = makeAdapter(pes);
|
||
|
||
assert(adapter instanceof PesPlatformAdapter, 'extends PesPlatformAdapter');
|
||
assert(adapter.platformName === 'telegram', 'platformName = telegram');
|
||
assert(adapter.renderMode === RENDER_MODE.RICH, 'default renderMode = RICH');
|
||
assert(adapter.botToken === 'FAKE_TOKEN_123', 'botToken stored');
|
||
assert(adapter.chatId === '12345', 'chatId stored as string');
|
||
assert(adapter.useThinkingDelay === false, 'useThinkingDelay from config');
|
||
assert(adapter._polling === false, 'polling off initially');
|
||
assert(adapter._currentStickerPack === null, 'no sticker pack initially');
|
||
|
||
adapter.destroy();
|
||
pes.destroy();
|
||
}
|
||
|
||
// Constructor validation
|
||
{
|
||
const pes = makePes('Thor2');
|
||
let threw = false;
|
||
try { new TelegramAdapter(pes, { chatId: '123' }); }
|
||
catch { threw = true; }
|
||
assert(threw, 'throws without botToken');
|
||
|
||
threw = false;
|
||
try { new TelegramAdapter(pes, { botToken: 'tok' }); }
|
||
catch { threw = true; }
|
||
assert(threw, 'throws without chatId');
|
||
|
||
pes.destroy();
|
||
}
|
||
|
||
|
||
// ── 2. Keyword Matching ──
|
||
|
||
section('Keyword Matching');
|
||
|
||
{
|
||
const pes = makePes('KeyDog');
|
||
const adapter = makeAdapter(pes);
|
||
|
||
// Track events
|
||
const events = [];
|
||
pes.on('expression', e => events.push(e));
|
||
|
||
// Test greeting
|
||
const greetMatches = ['привет', 'Привет!', 'здравствуй', 'хай', 'hi', 'Hello'];
|
||
for (const g of greetMatches) {
|
||
assert(KEYWORDS.greetings.patterns.some(p => p.test(g)), `greeting: "${g}" matches`);
|
||
}
|
||
|
||
// Test praise
|
||
const praiseMatches = ['молодец', 'хорошо', 'КЛАСС', 'супер', 'good boy'];
|
||
for (const p of praiseMatches) {
|
||
assert(KEYWORDS.praise.patterns.some(pat => pat.test(p)), `praise: "${p}" matches`);
|
||
}
|
||
|
||
// Test scold (note: "нет" and "стоп" use ^ anchor + exact-end patterns)
|
||
const scoldMatches = ['нет', 'плохо', 'стоп', 'фу'];
|
||
for (const s of scoldMatches) {
|
||
const matched = KEYWORDS.scold.patterns.some(p => p.test(s));
|
||
assert(matched, `scold: "${s}" matches`);
|
||
}
|
||
|
||
// Test fetch
|
||
const fetchMatches = ['принеси', 'найди config.js', 'fetch'];
|
||
for (const f of fetchMatches) {
|
||
assert(KEYWORDS.fetch.patterns.some(p => p.test(f)), `fetch: "${f}" matches`);
|
||
}
|
||
|
||
// Test status
|
||
assert(KEYWORDS.status.patterns.some(p => p.test('как дела')), 'status: "как дела" matches');
|
||
assert(KEYWORDS.status.patterns.some(p => p.test('статус')), 'status: "статус" matches');
|
||
|
||
// Non-match
|
||
assert(!KEYWORDS.greetings.patterns.some(p => p.test('code review')), 'non-match: "code review" not a greeting');
|
||
|
||
adapter.destroy();
|
||
pes.destroy();
|
||
}
|
||
|
||
|
||
// ── 3. Sticker Emotion Map ──
|
||
|
||
section('Sticker Emotion Map');
|
||
|
||
{
|
||
assert(DEFAULT_STICKER_EMOTION_MAP.butt_wiggle_turbo === 'happy', 'butt_wiggle_turbo → happy');
|
||
assert(DEFAULT_STICKER_EMOTION_MAP.ears_forward === 'alert', 'ears_forward → alert');
|
||
assert(DEFAULT_STICKER_EMOTION_MAP.sploot === 'relaxed', 'sploot → relaxed');
|
||
assert(DEFAULT_STICKER_EMOTION_MAP.zoomies === 'zoomies', 'zoomies → zoomies');
|
||
assert(DEFAULT_STICKER_EMOTION_MAP.hide_behind === 'scared', 'hide_behind → scared');
|
||
}
|
||
|
||
|
||
// ── 4. Thinking Delay ──
|
||
|
||
section('Thinking Delay');
|
||
|
||
{
|
||
const puppy = thinkingDelay(5);
|
||
const young = thinkingDelay(30);
|
||
const adult = thinkingDelay(50);
|
||
const experienced = thinkingDelay(70);
|
||
const cyber = thinkingDelay(90);
|
||
|
||
assert(puppy >= 2000, `puppy delay >= 2000ms (got ${Math.round(puppy)})`);
|
||
assert(young >= 1000 && young < 2100, `young delay 1000-2000ms (got ${Math.round(young)})`);
|
||
assert(adult >= 500 && adult < 1100, `adult delay 500-1000ms (got ${Math.round(adult)})`);
|
||
assert(experienced >= 200 && experienced < 600, `experienced delay 200-500ms (got ${Math.round(experienced)})`);
|
||
assert(cyber < 210, `cyber delay < 210ms (got ${Math.round(cyber)})`);
|
||
}
|
||
|
||
|
||
// ── 5. Render ──
|
||
|
||
section('Render (formatBody / formatGlyphs)');
|
||
|
||
{
|
||
const pes = makePes('RenderDog');
|
||
const adapter = makeAdapter(pes);
|
||
|
||
// No stickers registered → null
|
||
assert(adapter.formatBody('butt_wiggle_turbo', {}) === null, 'no sticker → null');
|
||
|
||
// Register a sticker
|
||
adapter.registerMedia(MEDIA_TYPE.STICKER, 'happy', 'CAACAgI_happy_file_id');
|
||
assert(adapter.formatBody('butt_wiggle_turbo', {}) === 'CAACAgI_happy_file_id',
|
||
'butt_wiggle_turbo → happy sticker via DEFAULT_STICKER_EMOTION_MAP');
|
||
|
||
// Direct body key lookup
|
||
adapter.registerMedia(MEDIA_TYPE.STICKER, 'butt_wiggle_turbo', 'CAACAgI_direct_file');
|
||
assert(adapter.formatBody('butt_wiggle_turbo', {}) === 'CAACAgI_direct_file',
|
||
'direct bodyKey lookup takes priority');
|
||
|
||
// Default sticker fallback
|
||
adapter.registerMedia(MEDIA_TYPE.STICKER, 'default', 'CAACAgI_default');
|
||
assert(adapter.formatBody('unknown_body_key', {}) === 'CAACAgI_default',
|
||
'unknown bodyKey → default sticker');
|
||
|
||
// Voice returns null (text mode for Telegram)
|
||
assert(adapter.formatVoice(null, {}) === null, 'null voiceKey → null');
|
||
|
||
// Glyphs
|
||
assert(adapter.formatGlyphs(null, {}) === null, 'null glyphs → null');
|
||
assert(adapter.formatGlyphs([], {}) === null, 'empty glyphs → null');
|
||
|
||
adapter.destroy();
|
||
pes.destroy();
|
||
}
|
||
|
||
|
||
// ── 6. Media Registry ──
|
||
|
||
section('Media Registry');
|
||
|
||
{
|
||
const pes = makePes('MediaDog');
|
||
const adapter = makeAdapter(pes);
|
||
|
||
// Load media pack
|
||
adapter.loadMediaPack({
|
||
[MEDIA_TYPE.STICKER]: {
|
||
happy: 'sticker_happy',
|
||
sad: 'sticker_sad',
|
||
sleeping: 'sticker_sleep',
|
||
},
|
||
[MEDIA_TYPE.REACTION]: {
|
||
happy: '❤️',
|
||
alert: '⚡',
|
||
},
|
||
});
|
||
|
||
assert(adapter.getMedia(MEDIA_TYPE.STICKER, 'happy') === 'sticker_happy', 'sticker: happy loaded');
|
||
assert(adapter.getMedia(MEDIA_TYPE.STICKER, 'sad') === 'sticker_sad', 'sticker: sad loaded');
|
||
assert(adapter.getMedia(MEDIA_TYPE.REACTION, 'happy') === '❤️', 'reaction: happy loaded');
|
||
assert(adapter.hasMedia(MEDIA_TYPE.STICKER, 'sleeping'), 'hasMedia: sleeping sticker exists');
|
||
assert(!adapter.hasMedia(MEDIA_TYPE.STICKER, 'nonexistent'), 'hasMedia: nonexistent = false');
|
||
|
||
const keys = adapter.getMediaKeys(MEDIA_TYPE.STICKER);
|
||
assert(keys.length === 3, `sticker keys count = 3 (got ${keys.length})`);
|
||
|
||
adapter.destroy();
|
||
pes.destroy();
|
||
}
|
||
|
||
|
||
// ── 7. Message Tracking (feedback bridge) ──
|
||
|
||
section('Message Tracking & Feedback');
|
||
|
||
{
|
||
const pes = makePes('FeedbackDog');
|
||
const adapter = makeAdapter(pes);
|
||
|
||
// Track messages
|
||
adapter.trackMessage('expr_001', 'tg_msg_100');
|
||
adapter.trackMessage('expr_002', 'tg_msg_101');
|
||
adapter.trackMessage('expr_003', 'tg_msg_102');
|
||
|
||
assert(adapter.trackedMessages === 3, 'tracked 3 messages');
|
||
assert(adapter._lookupExpression('tg_msg_100') === 'expr_001', 'lookup: tg_msg_100 → expr_001');
|
||
assert(adapter._lookupExpression('tg_msg_102') === 'expr_003', 'lookup: tg_msg_102 → expr_003');
|
||
assert(adapter._lookupExpression('tg_msg_999') === null, 'lookup: unknown → null');
|
||
|
||
// Reaction classification
|
||
assert(adapter._classifyReaction('👍') === 'emoji_positive', '👍 = positive');
|
||
assert(adapter._classifyReaction('👎') === 'emoji_negative', '👎 = negative');
|
||
assert(adapter._classifyReaction('🤷') === 'emoji_neutral', '🤷 = neutral');
|
||
assert(adapter._classifyReaction('hello') === 'text', '"hello" = text');
|
||
assert(adapter._classifyReaction(null) === 'silence', 'null = silence');
|
||
|
||
adapter.destroy();
|
||
pes.destroy();
|
||
}
|
||
|
||
|
||
// ── 8. Auto-emoji map ──
|
||
|
||
section('Emoji → Emotion Key Map');
|
||
|
||
{
|
||
const pes = makePes('EmojiDog');
|
||
const adapter = makeAdapter(pes);
|
||
|
||
assert(adapter._emojiToEmotionKey('😊') === 'happy', '😊 → happy');
|
||
assert(adapter._emojiToEmotionKey('😢') === 'sad', '😢 → sad');
|
||
assert(adapter._emojiToEmotionKey('😡') === 'rage', '😡 → rage');
|
||
assert(adapter._emojiToEmotionKey('😴') === 'sleeping', '😴 → sleeping');
|
||
assert(adapter._emojiToEmotionKey('🤔') === 'confused', '🤔 → confused');
|
||
assert(adapter._emojiToEmotionKey('🔥') === 'excited', '🔥 → excited');
|
||
assert(adapter._emojiToEmotionKey('🐾') === 'default', '🐾 → default');
|
||
assert(adapter._emojiToEmotionKey('🌵') === null, '🌵 → null (unknown)');
|
||
|
||
adapter.destroy();
|
||
pes.destroy();
|
||
}
|
||
|
||
|
||
// ── 9. Delivery Pipeline (mock sendMessage) ──
|
||
|
||
section('Delivery Pipeline');
|
||
|
||
{
|
||
const pes = makePes('DeliveryDog');
|
||
const adapter = makeAdapter(pes);
|
||
|
||
// Override sendMessage to capture output
|
||
const sent = [];
|
||
adapter.sendMessage = async (output) => {
|
||
sent.push(output);
|
||
return 'msg_' + sent.length;
|
||
};
|
||
|
||
adapter._connected = true;
|
||
|
||
// Create a fake expression result
|
||
const fakeResult = {
|
||
id: 'test_expr_1',
|
||
expression: {
|
||
bodyKey: 'butt_wiggle_turbo',
|
||
voiceKey: 'roo_roo',
|
||
glyphKeys: ['paw', 'heart'],
|
||
meaning: 'happy',
|
||
renderBody: () => '💫🍑💫',
|
||
renderVoice: () => 'руу-руу-руу!',
|
||
renderGlyphs: () => '🐾❤️',
|
||
render: () => '💫🍑💫 руу-руу-руу! 🐾❤️',
|
||
toJSON: () => ({ bodyKey: 'butt_wiggle_turbo' }),
|
||
},
|
||
priority: 50,
|
||
suppressed: false,
|
||
shouldNotify: false,
|
||
emotionSnapshot: { state: 'happy', intensity: 0.8 },
|
||
platformHints: {},
|
||
};
|
||
|
||
// Deliver
|
||
let success;
|
||
(async () => {
|
||
success = await adapter.deliver(fakeResult);
|
||
})().then(() => {
|
||
assert(success === true, 'delivery returned true');
|
||
assert(sent.length === 1, 'sendMessage called once');
|
||
assert(adapter.stats.sent === 1, 'stats.sent = 1');
|
||
assert(adapter.trackedMessages === 1, 'message tracked for feedback');
|
||
|
||
// Suppressed expressions should not deliver
|
||
const suppressedResult = { ...fakeResult, suppressed: true };
|
||
adapter.deliver(suppressedResult).then(s => {
|
||
assert(s === false, 'suppressed delivery returns false');
|
||
});
|
||
});
|
||
}
|
||
|
||
|
||
// ── 10. Lifecycle Hooks ──
|
||
|
||
section('Lifecycle Hooks');
|
||
|
||
{
|
||
const pes = makePes('LifeDog');
|
||
const adapter = makeAdapter(pes);
|
||
|
||
const lifecycleEvents = [];
|
||
adapter.on('lifecycle', e => lifecycleEvents.push(e));
|
||
|
||
// Override sendMessage (don't actually call API)
|
||
adapter.sendMessage = async () => null;
|
||
adapter._sendText = async () => ({ message_id: 1 });
|
||
adapter._sendSticker = async () => ({ message_id: 2 });
|
||
|
||
// Test onBorn
|
||
adapter.onBorn({ name: 'LifeDog', breed: 'corgi' }).then(() => {
|
||
assert(lifecycleEvents.some(e => e.event === 'born'), 'onBorn emits lifecycle:born');
|
||
});
|
||
|
||
// Test onWake
|
||
adapter.onWake({ name: 'LifeDog', minutesAway: 120 }).then(() => {
|
||
assert(lifecycleEvents.some(e => e.event === 'wake'), 'onWake emits lifecycle:wake');
|
||
});
|
||
|
||
// Test onSleep
|
||
adapter.onSleep({ name: 'LifeDog' }).then(() => {
|
||
assert(lifecycleEvents.some(e => e.event === 'sleep'), 'onSleep emits lifecycle:sleep');
|
||
});
|
||
|
||
// Test onLevelUp
|
||
adapter.onLevelUp({ name: 'LifeDog', level: 20, xp: 100 }).then(() => {
|
||
assert(lifecycleEvents.some(e => e.event === 'levelUp'), 'onLevelUp emits lifecycle:levelUp');
|
||
});
|
||
|
||
// Test onRunaway
|
||
adapter.onRunaway({
|
||
name: 'LifeDog',
|
||
letter: {
|
||
finalWords: '🐾...💤',
|
||
reason: 'dual_rage',
|
||
snapshot: { total_interactions: 42, commands_learned: 5 },
|
||
}
|
||
}).then(() => {
|
||
assert(lifecycleEvents.some(e => e.event === 'runaway'), 'onRunaway emits lifecycle:runaway');
|
||
});
|
||
|
||
// Cleanup after async settles
|
||
setTimeout(() => {
|
||
adapter.destroy();
|
||
pes.destroy();
|
||
}, 100);
|
||
}
|
||
|
||
|
||
// ── 11. Status Formatting ──
|
||
|
||
section('Status Formatting');
|
||
|
||
{
|
||
const pes = makePes('StatusDog');
|
||
const adapter = makeAdapter(pes);
|
||
|
||
// Test bar helper
|
||
assert(adapter._bar(1.0, 10) === '██████████', 'bar(1.0) = all filled');
|
||
assert(adapter._bar(0.0, 10) === '░░░░░░░░░░', 'bar(0.0) = all empty');
|
||
assert(adapter._bar(0.5, 10) === '█████░░░░░', 'bar(0.5) = half');
|
||
|
||
adapter.destroy();
|
||
pes.destroy();
|
||
}
|
||
|
||
|
||
// ── 12. Update Processing (mocked) ──
|
||
|
||
section('Update Processing');
|
||
|
||
(async () => {
|
||
const pes = makePes('UpdateDog');
|
||
const adapter = makeAdapter(pes);
|
||
|
||
// Mock API methods
|
||
adapter._sendText = async () => ({ message_id: 1 });
|
||
adapter._sendSticker = async () => ({ message_id: 2 });
|
||
adapter._setReaction = async () => true;
|
||
|
||
const pesEvents = [];
|
||
pes.on('expression', e => pesEvents.push(e));
|
||
|
||
// Test text message processing
|
||
await adapter._processUpdate({
|
||
message: {
|
||
chat: { id: 12345 },
|
||
message_id: 100,
|
||
text: 'привет',
|
||
}
|
||
});
|
||
assert(adapter._lastOwnerMessageId === 100, 'lastOwnerMessageId updated');
|
||
assert(pesEvents.length > 0, 'PES received event from greeting');
|
||
|
||
// Test wrong chat ignored (should not trigger anything)
|
||
await adapter._processUpdate({
|
||
message: {
|
||
chat: { id: 99999 },
|
||
message_id: 200,
|
||
text: 'привет',
|
||
}
|
||
});
|
||
|
||
// Test bot command
|
||
adapter._handleBotCommand = async (msg) => {
|
||
assert(msg.text === '/status', 'bot command routed correctly');
|
||
};
|
||
|
||
await adapter._processUpdate({
|
||
message: {
|
||
chat: { id: 12345 },
|
||
message_id: 101,
|
||
text: '/status',
|
||
}
|
||
});
|
||
|
||
// Test sticker message
|
||
await adapter._processUpdate({
|
||
message: {
|
||
chat: { id: 12345 },
|
||
message_id: 102,
|
||
sticker: { emoji: '😊', file_id: 'sticker_123' },
|
||
}
|
||
});
|
||
|
||
// Test reaction update
|
||
adapter.trackMessage('expr_test', '103');
|
||
await adapter._processUpdate({
|
||
message_reaction: {
|
||
chat: { id: 12345 },
|
||
message_id: 103,
|
||
new_reaction: [{ type: 'emoji', emoji: '👍' }],
|
||
user: { id: 12345 },
|
||
}
|
||
});
|
||
|
||
adapter.destroy();
|
||
pes.destroy();
|
||
})();
|
||
|
||
|
||
// ── 13. PES Integration ──
|
||
|
||
section('PES Integration');
|
||
|
||
{
|
||
const pes = makePes('IntDog');
|
||
const adapter = makeAdapter(pes);
|
||
|
||
// Verify PES and adapter are properly linked
|
||
assert(adapter.pes === pes, 'adapter.pes points to PES instance');
|
||
assert(adapter.pes.name === 'IntDog', 'adapter.pes.name correct');
|
||
|
||
// PES praise → generates expression
|
||
const expressions = [];
|
||
pes.on('expression', e => expressions.push(e));
|
||
|
||
pes.praise('good boy');
|
||
|
||
// Give it a tick
|
||
setTimeout(() => {
|
||
assert(expressions.length > 0, 'PES praise generated expression');
|
||
|
||
adapter.destroy();
|
||
pes.destroy();
|
||
}, 50);
|
||
}
|
||
|
||
|
||
// ── 14. Sticker Pack Loading (mock) ──
|
||
|
||
section('Sticker Pack Loading');
|
||
|
||
(async () => {
|
||
const pes = makePes('StickerDog');
|
||
const adapter = makeAdapter(pes);
|
||
|
||
// Mock _getStickerSet
|
||
adapter._getStickerSet = async (name) => ({
|
||
name,
|
||
stickers: [
|
||
{ file_id: 'file_0', emoji: '😊' },
|
||
{ file_id: 'file_1', emoji: '😢' },
|
||
{ file_id: 'file_2', emoji: '😡' },
|
||
{ file_id: 'file_3', emoji: '😴' },
|
||
{ file_id: 'file_4', emoji: '🐾' },
|
||
],
|
||
});
|
||
|
||
// Auto-mapping by emoji
|
||
await adapter.loadStickerPack('TestCorgiPack');
|
||
assert(adapter.currentStickerPack === 'TestCorgiPack', 'currentStickerPack set');
|
||
assert(adapter.getMedia(MEDIA_TYPE.STICKER, 'happy') === 'file_0', 'emoji 😊 → happy');
|
||
assert(adapter.getMedia(MEDIA_TYPE.STICKER, 'sad') === 'file_1', 'emoji 😢 → sad');
|
||
assert(adapter.getMedia(MEDIA_TYPE.STICKER, 'rage') === 'file_2', 'emoji 😡 → rage');
|
||
assert(adapter.getMedia(MEDIA_TYPE.STICKER, 'sleeping') === 'file_3', 'emoji 😴 → sleeping');
|
||
assert(adapter.getMedia(MEDIA_TYPE.STICKER, 'default') === 'file_4', 'emoji 🐾 → default');
|
||
assert(adapter.getMedia(MEDIA_TYPE.STICKER, 'pack_0') === 'file_0', 'pack_0 stored');
|
||
|
||
// Explicit mapping
|
||
await adapter.loadStickerPack('ExplicitPack', { 0: 'custom_happy', 2: 'custom_angry' });
|
||
assert(adapter.getMedia(MEDIA_TYPE.STICKER, 'custom_happy') === 'file_0', 'explicit: 0 → custom_happy');
|
||
assert(adapter.getMedia(MEDIA_TYPE.STICKER, 'custom_angry') === 'file_2', 'explicit: 2 → custom_angry');
|
||
|
||
// Empty pack throws
|
||
adapter._getStickerSet = async () => ({ name: 'empty', stickers: [] });
|
||
try {
|
||
await adapter.loadStickerPack('EmptyPack');
|
||
assert(false, 'empty pack should throw');
|
||
} catch (err) {
|
||
assert(err.message.includes('empty'), 'empty pack throws error');
|
||
}
|
||
|
||
adapter.destroy();
|
||
pes.destroy();
|
||
})();
|
||
|
||
|
||
// ── 15. Command Handling ──
|
||
|
||
section('Command Handling');
|
||
|
||
{
|
||
const pes = makePes('CmdDog');
|
||
const adapter = makeAdapter(pes);
|
||
|
||
adapter._sendText = async () => ({ message_id: 1 });
|
||
|
||
// /start on alive PES
|
||
adapter._cmdStart({ chat: { id: 12345 } }).then(() => {
|
||
assert(pes.isAlive, 'PES alive after /start');
|
||
});
|
||
|
||
// /silent toggle
|
||
const wasSilent = pes.engine?.silentMode || false;
|
||
adapter._cmdSilent({}).then(() => {
|
||
assert((pes.engine?.silentMode || false) !== wasSilent, 'silent mode toggled');
|
||
});
|
||
|
||
setTimeout(() => {
|
||
adapter.destroy();
|
||
pes.destroy();
|
||
}, 100);
|
||
}
|
||
|
||
|
||
// ── 16. Edge Cases ──
|
||
|
||
section('Edge Cases');
|
||
|
||
{
|
||
const pes = makePes('EdgeDog');
|
||
const adapter = makeAdapter(pes);
|
||
|
||
// Deliver null → false
|
||
adapter.deliver(null).then(r => assert(r === false, 'deliver(null) = false'));
|
||
adapter.deliver({ suppressed: true }).then(r => assert(r === false, 'deliver(suppressed) = false'));
|
||
|
||
// Render null expression → null
|
||
assert(adapter.renderExpression(null) === null, 'renderExpression(null) = null');
|
||
assert(adapter.renderExpression({ expression: null }) === null, 'renderExpression({expression:null}) = null');
|
||
|
||
// chatId as number → stored as string
|
||
const adapter2 = new TelegramAdapter(pes, {
|
||
botToken: 'tok',
|
||
chatId: 67890,
|
||
autoListen: false,
|
||
});
|
||
assert(adapter2.chatId === '67890', 'chatId number → string');
|
||
|
||
adapter.destroy();
|
||
adapter2.destroy();
|
||
pes.destroy();
|
||
}
|
||
|
||
|
||
// ── Summary ──
|
||
|
||
setTimeout(() => {
|
||
console.log(`\n════════════════════════════════════════`);
|
||
console.log(` TOTAL: ${passed + failed} tests`);
|
||
console.log(` ✅ Passed: ${passed}`);
|
||
console.log(` ❌ Failed: ${failed}`);
|
||
console.log(`════════════════════════════════════════\n`);
|
||
|
||
// Cleanup
|
||
try { rmSync(TEST_DIR, { recursive: true, force: true }); } catch (_) {}
|
||
|
||
if (failed > 0) process.exit(1);
|
||
}, 500);
|