Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
800 lines
24 KiB
JavaScript
800 lines
24 KiB
JavaScript
// ============================================================
|
|
// Tests for PES Platform — Base Adapter
|
|
// ============================================================
|
|
|
|
import { strict as assert } from 'node:assert';
|
|
import { EventEmitter } from 'node:events';
|
|
import { PesPlatformAdapter, RENDER_MODE, PRESENCE, MEDIA_TYPE } from './base.js';
|
|
|
|
let passed = 0;
|
|
let total = 0;
|
|
|
|
function test(name, fn) {
|
|
total++;
|
|
try {
|
|
fn();
|
|
passed++;
|
|
console.log(` ✅ ${name}`);
|
|
} catch (err) {
|
|
console.log(` ❌ ${name}: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
async function testAsync(name, fn) {
|
|
total++;
|
|
try {
|
|
await fn();
|
|
passed++;
|
|
console.log(` ✅ ${name}`);
|
|
} catch (err) {
|
|
console.log(` ❌ ${name}: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
|
|
// ── Mock PES ────────────────────────────────────────────────
|
|
|
|
class MockPes extends EventEmitter {
|
|
constructor() {
|
|
super();
|
|
this.name = 'Тестик';
|
|
this.ownerId = 'nikitron';
|
|
this.mode = 'active';
|
|
this._reactions = [];
|
|
}
|
|
|
|
react(expressionId, reaction) {
|
|
this._reactions.push({ expressionId, reaction });
|
|
}
|
|
}
|
|
|
|
|
|
// ── Test Adapter (concrete implementation) ──────────────────
|
|
|
|
class TestAdapter extends PesPlatformAdapter {
|
|
constructor(pes, config = {}) {
|
|
super(pes, { platformName: 'test', ...config });
|
|
this.sentMessages = [];
|
|
this.collecting = false;
|
|
this.presenceState = null;
|
|
}
|
|
|
|
async sendMessage(output) {
|
|
const msgId = `msg_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
this.sentMessages.push({ output, msgId });
|
|
return msgId;
|
|
}
|
|
|
|
async startCollecting() {
|
|
this.collecting = true;
|
|
}
|
|
|
|
async stopCollecting() {
|
|
this.collecting = false;
|
|
}
|
|
|
|
async setPresence(state) {
|
|
this.presenceState = state;
|
|
}
|
|
}
|
|
|
|
|
|
// ── Mock ExpressionResult ───────────────────────────────────
|
|
|
|
function mockExpressionResult(overrides = {}) {
|
|
const mockExpression = {
|
|
bodyKey: 'butt_wiggle_fast',
|
|
voiceKey: 'bark_excitement',
|
|
glyphKeys: ['paw', 'arrow', 'heart'],
|
|
renderBody() { return '~🍑~🍑~'; },
|
|
renderVoice() { return 'ГАВгавгавгавГАВ!'; },
|
|
renderGlyphs() { return '🐾→💛'; },
|
|
toJSON() {
|
|
return {
|
|
id: this.id || 'expr_test',
|
|
bodyKey: this.bodyKey,
|
|
voiceKey: this.voiceKey,
|
|
glyphKeys: this.glyphKeys,
|
|
};
|
|
},
|
|
...overrides.expression,
|
|
};
|
|
|
|
return {
|
|
id: 'er_test_001',
|
|
expression: mockExpression,
|
|
priority: 50,
|
|
triggerName: 'owner_returned',
|
|
combo: null,
|
|
suppressed: false,
|
|
shouldNotify: false,
|
|
emotionSnapshot: { current: 'happy', intensity: 0.7 },
|
|
platformHints: {},
|
|
timestamp: Date.now(),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
|
|
// ── TESTS ───────────────────────────────────────────────────
|
|
|
|
console.log('\n══════ PesPlatformAdapter — Constructor ══════');
|
|
|
|
test('creates adapter with pes', () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
assert.equal(adapter.platformName, 'test');
|
|
assert.equal(adapter.renderMode, RENDER_MODE.FULL);
|
|
assert.equal(adapter.isConnected, false);
|
|
});
|
|
|
|
test('throws without pes', () => {
|
|
assert.throws(() => new TestAdapter(null), /requires a Pes instance/);
|
|
});
|
|
|
|
test('custom config', () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes, { renderMode: RENDER_MODE.COMPACT });
|
|
assert.equal(adapter.renderMode, RENDER_MODE.COMPACT);
|
|
});
|
|
|
|
test('initial stats are zero', () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
assert.deepEqual(adapter.deliveryStats, { sent: 0, failed: 0, reactions: 0 });
|
|
});
|
|
|
|
|
|
console.log('\n══════ Media Registry ══════');
|
|
|
|
test('registerMedia and getMedia', () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
adapter.registerMedia(MEDIA_TYPE.STICKER, 'butt_wiggle_fast', 'sticker_file_id_123');
|
|
assert.equal(adapter.getMedia(MEDIA_TYPE.STICKER, 'butt_wiggle_fast'), 'sticker_file_id_123');
|
|
});
|
|
|
|
test('getMedia returns null for missing', () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
assert.equal(adapter.getMedia(MEDIA_TYPE.STICKER, 'nonexistent'), null);
|
|
});
|
|
|
|
test('loadMediaPack — bulk load', () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
adapter.loadMediaPack({
|
|
[MEDIA_TYPE.STICKER]: {
|
|
'butt_wiggle_fast': 'sticker_1',
|
|
'sploot': 'sticker_2',
|
|
'play_bow': 'sticker_3',
|
|
},
|
|
[MEDIA_TYPE.SOUND]: {
|
|
'bark_excitement': 'bark.ogg',
|
|
'whine': 'whine.ogg',
|
|
},
|
|
[MEDIA_TYPE.REACTION]: {
|
|
'happy': '❤️',
|
|
'angry': '😡',
|
|
},
|
|
});
|
|
|
|
assert.equal(adapter.getMedia(MEDIA_TYPE.STICKER, 'butt_wiggle_fast'), 'sticker_1');
|
|
assert.equal(adapter.getMedia(MEDIA_TYPE.STICKER, 'sploot'), 'sticker_2');
|
|
assert.equal(adapter.getMedia(MEDIA_TYPE.SOUND, 'bark_excitement'), 'bark.ogg');
|
|
assert.equal(adapter.getMedia(MEDIA_TYPE.REACTION, 'happy'), '❤️');
|
|
});
|
|
|
|
test('hasMedia', () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
adapter.registerMedia(MEDIA_TYPE.STICKER, 'test_key', 'value');
|
|
assert.equal(adapter.hasMedia(MEDIA_TYPE.STICKER, 'test_key'), true);
|
|
assert.equal(adapter.hasMedia(MEDIA_TYPE.STICKER, 'missing'), false);
|
|
});
|
|
|
|
test('getMediaKeys', () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
adapter.registerMedia(MEDIA_TYPE.STICKER, 'a', '1');
|
|
adapter.registerMedia(MEDIA_TYPE.STICKER, 'b', '2');
|
|
const keys = adapter.getMediaKeys(MEDIA_TYPE.STICKER);
|
|
assert.deepEqual(keys, ['a', 'b']);
|
|
});
|
|
|
|
|
|
console.log('\n══════ Render Expression — FULL mode ══════');
|
|
|
|
test('renders full expression with text fallback', () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
const result = mockExpressionResult();
|
|
const output = adapter.renderExpression(result);
|
|
|
|
assert.ok(output);
|
|
assert.equal(output.text, '🐾→💛');
|
|
assert.equal(output.sticker, '~🍑~🍑~'); // falls back to renderBody()
|
|
assert.ok(output.meta.voiceText);
|
|
assert.equal(output.expressionId, 'er_test_001');
|
|
});
|
|
|
|
test('renders with sticker from registry', () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
adapter.registerMedia(MEDIA_TYPE.STICKER, 'butt_wiggle_fast', 'CAACAgI_corgi_wiggle');
|
|
|
|
const result = mockExpressionResult();
|
|
const output = adapter.renderExpression(result);
|
|
|
|
assert.equal(output.sticker, 'CAACAgI_corgi_wiggle');
|
|
});
|
|
|
|
test('renders with sticker from emotion fallback', () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
adapter.registerMedia(MEDIA_TYPE.STICKER, 'happy', 'CAACAgI_happy_corgi');
|
|
|
|
const result = mockExpressionResult();
|
|
const output = adapter.renderExpression(result);
|
|
|
|
// bodyKey 'butt_wiggle_fast' not in registry, but emotion 'happy' is
|
|
assert.equal(output.sticker, 'CAACAgI_happy_corgi');
|
|
});
|
|
|
|
test('renders with sound from registry', () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
adapter.registerMedia(MEDIA_TYPE.SOUND, 'bark_excitement', 'bark.ogg');
|
|
|
|
const result = mockExpressionResult();
|
|
const output = adapter.renderExpression(result);
|
|
|
|
assert.equal(output.sound, 'bark.ogg');
|
|
assert.ok(output.meta.voiceText);
|
|
});
|
|
|
|
test('renders reaction from emotion registry', () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
adapter.registerMedia(MEDIA_TYPE.REACTION, 'happy', '🐾');
|
|
|
|
const result = mockExpressionResult();
|
|
const output = adapter.renderExpression(result);
|
|
|
|
assert.equal(output.reaction, '🐾');
|
|
});
|
|
|
|
test('renders reaction from platformHints', () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
|
|
const result = mockExpressionResult({ platformHints: { reaction: '🔥' } });
|
|
const output = adapter.renderExpression(result);
|
|
|
|
assert.equal(output.reaction, '🔥');
|
|
});
|
|
|
|
test('null expression returns null', () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
assert.equal(adapter.renderExpression(null), null);
|
|
assert.equal(adapter.renderExpression({ expression: null }), null);
|
|
});
|
|
|
|
|
|
console.log('\n══════ Render Expression — COMPACT mode ══════');
|
|
|
|
test('compact mode: only glyphs', () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes, { renderMode: RENDER_MODE.COMPACT });
|
|
|
|
const result = mockExpressionResult();
|
|
const output = adapter.renderExpression(result);
|
|
|
|
assert.equal(output.text, '🐾→💛');
|
|
assert.equal(output.sticker, null); // no body in compact
|
|
assert.equal(output.sound, null); // no voice in compact
|
|
});
|
|
|
|
|
|
console.log('\n══════ Render Expression — DEBUG mode ══════');
|
|
|
|
test('debug mode: includes internal state', () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes, { renderMode: RENDER_MODE.DEBUG });
|
|
|
|
const result = mockExpressionResult();
|
|
const output = adapter.renderExpression(result);
|
|
|
|
assert.ok(output.meta.emotion);
|
|
assert.equal(output.meta.triggerName, 'owner_returned');
|
|
assert.equal(output.meta.emotion.current, 'happy');
|
|
});
|
|
|
|
|
|
console.log('\n══════ Render Expression — RICH mode ══════');
|
|
|
|
test('rich mode: sticker + caption', () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes, { renderMode: RENDER_MODE.RICH });
|
|
adapter.registerMedia(MEDIA_TYPE.STICKER, 'butt_wiggle_fast', 'sticker_rich');
|
|
|
|
const result = mockExpressionResult();
|
|
const output = adapter.renderExpression(result);
|
|
|
|
assert.equal(output.sticker, 'sticker_rich');
|
|
assert.ok(output.meta.caption);
|
|
});
|
|
|
|
|
|
console.log('\n══════ Delivery ══════');
|
|
|
|
await testAsync('deliver sends message and tracks', async () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
|
|
const result = mockExpressionResult();
|
|
const success = await adapter.deliver(result);
|
|
|
|
assert.equal(success, true);
|
|
assert.equal(adapter.stats.sent, 1);
|
|
assert.equal(adapter.sentMessages.length, 1);
|
|
assert.equal(adapter.trackedMessages, 1);
|
|
});
|
|
|
|
await testAsync('deliver skips suppressed', async () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
|
|
const result = mockExpressionResult({ suppressed: true });
|
|
const success = await adapter.deliver(result);
|
|
|
|
assert.equal(success, false);
|
|
assert.equal(adapter.stats.sent, 0);
|
|
});
|
|
|
|
await testAsync('deliver tracks failure', async () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
// Override sendMessage to throw
|
|
adapter.sendMessage = async () => { throw new Error('network error'); };
|
|
|
|
const result = mockExpressionResult();
|
|
const success = await adapter.deliver(result);
|
|
|
|
assert.equal(success, false);
|
|
assert.equal(adapter.stats.failed, 1);
|
|
});
|
|
|
|
await testAsync('deliver null returns false', async () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
assert.equal(await adapter.deliver(null), false);
|
|
});
|
|
|
|
|
|
console.log('\n══════ Feedback Bridge ══════');
|
|
|
|
await testAsync('onUserReaction feeds back to PES', async () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
|
|
// Deliver a message first to create tracking
|
|
const result = mockExpressionResult();
|
|
await adapter.deliver(result);
|
|
|
|
// Get the platform message ID
|
|
const msgId = adapter.sentMessages[0].msgId;
|
|
|
|
// Simulate owner reaction
|
|
adapter.onUserReaction(msgId, '👍', 'nikitron', { isOwner: true, speed_ms: 2000 });
|
|
|
|
assert.equal(pes._reactions.length, 1);
|
|
assert.equal(pes._reactions[0].expressionId, 'er_test_001');
|
|
assert.equal(pes._reactions[0].reaction.from, 'owner');
|
|
assert.equal(pes._reactions[0].reaction.type, 'emoji_positive');
|
|
assert.equal(pes._reactions[0].reaction.value, '👍');
|
|
assert.equal(adapter.stats.reactions, 1);
|
|
});
|
|
|
|
test('onUserReaction — unknown message ignored', () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
adapter.onUserReaction('unknown_msg', '👍', 'nikitron');
|
|
assert.equal(pes._reactions.length, 0);
|
|
});
|
|
|
|
await testAsync('onUserReaction — group member', async () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
|
|
const result = mockExpressionResult();
|
|
await adapter.deliver(result);
|
|
const msgId = adapter.sentMessages[0].msgId;
|
|
|
|
adapter.onUserReaction(msgId, '😂', 'dev1', { isOwner: false });
|
|
|
|
assert.equal(pes._reactions[0].reaction.from, 'group:dev1');
|
|
assert.equal(pes._reactions[0].reaction.type, 'emoji_positive');
|
|
});
|
|
|
|
await testAsync('onUserReaction — negative emoji', async () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
|
|
const result = mockExpressionResult();
|
|
await adapter.deliver(result);
|
|
const msgId = adapter.sentMessages[0].msgId;
|
|
|
|
adapter.onUserReaction(msgId, '👎', 'nikitron', { isOwner: true });
|
|
|
|
assert.equal(pes._reactions[0].reaction.type, 'emoji_negative');
|
|
});
|
|
|
|
await testAsync('onUserReaction — text reaction', async () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
|
|
const result = mockExpressionResult();
|
|
await adapter.deliver(result);
|
|
const msgId = adapter.sentMessages[0].msgId;
|
|
|
|
adapter.onUserReaction(msgId, 'молодец!', 'nikitron', { isOwner: true });
|
|
|
|
assert.equal(pes._reactions[0].reaction.type, 'text');
|
|
assert.equal(pes._reactions[0].reaction.value, 'молодец!');
|
|
});
|
|
|
|
|
|
console.log('\n══════ Reaction Classification ══════');
|
|
|
|
test('classifies positive emojis', () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
assert.equal(adapter._classifyReaction('👍'), 'emoji_positive');
|
|
assert.equal(adapter._classifyReaction('❤️'), 'emoji_positive');
|
|
assert.equal(adapter._classifyReaction('🔥'), 'emoji_positive');
|
|
assert.equal(adapter._classifyReaction('😂'), 'emoji_positive');
|
|
assert.equal(adapter._classifyReaction('💯'), 'emoji_positive');
|
|
});
|
|
|
|
test('classifies negative emojis', () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
assert.equal(adapter._classifyReaction('👎'), 'emoji_negative');
|
|
assert.equal(adapter._classifyReaction('😡'), 'emoji_negative');
|
|
assert.equal(adapter._classifyReaction('💩'), 'emoji_negative');
|
|
});
|
|
|
|
test('classifies text', () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
assert.equal(adapter._classifyReaction('молодец'), 'text');
|
|
assert.equal(adapter._classifyReaction('плохо'), 'text');
|
|
});
|
|
|
|
test('classifies silence', () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
assert.equal(adapter._classifyReaction(null), 'silence');
|
|
assert.equal(adapter._classifyReaction(''), 'silence');
|
|
});
|
|
|
|
|
|
console.log('\n══════ Message Tracking ══════');
|
|
|
|
test('trackMessage and lookup', () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
adapter.trackMessage('expr_1', 'msg_1');
|
|
adapter.trackMessage('expr_2', 'msg_2');
|
|
|
|
assert.equal(adapter._lookupExpression('msg_1'), 'expr_1');
|
|
assert.equal(adapter._lookupExpression('msg_2'), 'expr_2');
|
|
assert.equal(adapter._lookupExpression('msg_3'), null);
|
|
});
|
|
|
|
test('trackMessage evicts oldest when full', () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
adapter.messageMapMaxSize = 3;
|
|
|
|
adapter.trackMessage('expr_1', 'msg_1');
|
|
adapter.trackMessage('expr_2', 'msg_2');
|
|
adapter.trackMessage('expr_3', 'msg_3');
|
|
adapter.trackMessage('expr_4', 'msg_4'); // should evict msg_1
|
|
|
|
assert.equal(adapter._lookupExpression('msg_1'), null); // evicted
|
|
assert.equal(adapter._lookupExpression('msg_4'), 'expr_4');
|
|
assert.equal(adapter.trackedMessages, 3);
|
|
});
|
|
|
|
|
|
console.log('\n══════ Listening ══════');
|
|
|
|
await testAsync('startListening sets connected and presence', async () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
|
|
await adapter.startListening();
|
|
|
|
assert.equal(adapter.isConnected, true);
|
|
assert.equal(adapter.collecting, true);
|
|
assert.equal(adapter.presenceState, PRESENCE.ONLINE);
|
|
});
|
|
|
|
await testAsync('stopListening disconnects', async () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
|
|
await adapter.startListening();
|
|
await adapter.stopListening();
|
|
|
|
assert.equal(adapter.isConnected, false);
|
|
assert.equal(adapter.collecting, false);
|
|
assert.equal(adapter.presenceState, PRESENCE.OFFLINE);
|
|
});
|
|
|
|
|
|
console.log('\n══════ Auto-delivery on PES expression event ══════');
|
|
|
|
await testAsync('auto-delivers when connected', async () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
await adapter.startListening();
|
|
|
|
// Simulate PES emitting an expression
|
|
const result = mockExpressionResult();
|
|
pes.emit('expression', result);
|
|
|
|
// Wait for async delivery
|
|
await new Promise(r => setTimeout(r, 50));
|
|
|
|
assert.equal(adapter.sentMessages.length, 1);
|
|
assert.equal(adapter.stats.sent, 1);
|
|
});
|
|
|
|
await testAsync('does not deliver when disconnected', async () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
// NOT connected
|
|
|
|
const result = mockExpressionResult();
|
|
pes.emit('expression', result);
|
|
|
|
await new Promise(r => setTimeout(r, 50));
|
|
|
|
assert.equal(adapter.sentMessages.length, 0);
|
|
});
|
|
|
|
|
|
console.log('\n══════ Lifecycle Events ══════');
|
|
|
|
await testAsync('onBorn emits lifecycle event', async () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
|
|
let received = null;
|
|
adapter.on('lifecycle', (data) => { received = data; });
|
|
|
|
pes.emit('born', { name: 'Тестик' });
|
|
await new Promise(r => setTimeout(r, 20));
|
|
|
|
assert.ok(received);
|
|
assert.equal(received.event, 'born');
|
|
});
|
|
|
|
await testAsync('onWake sets presence ONLINE', async () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
|
|
pes.emit('wake', {});
|
|
await new Promise(r => setTimeout(r, 20));
|
|
|
|
assert.equal(adapter.presenceState, PRESENCE.ONLINE);
|
|
});
|
|
|
|
await testAsync('onSleep sets presence SLEEPING', async () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
|
|
pes.emit('sleep', {});
|
|
await new Promise(r => setTimeout(r, 20));
|
|
|
|
assert.equal(adapter.presenceState, PRESENCE.SLEEPING);
|
|
});
|
|
|
|
await testAsync('onRunaway sets presence OFFLINE', async () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
|
|
pes.emit('runaway', { letter: 'прощай' });
|
|
await new Promise(r => setTimeout(r, 20));
|
|
|
|
assert.equal(adapter.presenceState, PRESENCE.OFFLINE);
|
|
});
|
|
|
|
await testAsync('onModeChange updates presence', async () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
|
|
pes.emit('modeChange', { oldMode: 'active', newMode: 'idle' });
|
|
await new Promise(r => setTimeout(r, 20));
|
|
assert.equal(adapter.presenceState, PRESENCE.IDLE);
|
|
|
|
pes.emit('modeChange', { oldMode: 'idle', newMode: 'sleeping' });
|
|
await new Promise(r => setTimeout(r, 20));
|
|
assert.equal(adapter.presenceState, PRESENCE.SLEEPING);
|
|
});
|
|
|
|
|
|
console.log('\n══════ Render Mode ══════');
|
|
|
|
test('setRenderMode changes mode', () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
adapter.setRenderMode(RENDER_MODE.DEBUG);
|
|
assert.equal(adapter.renderMode, RENDER_MODE.DEBUG);
|
|
});
|
|
|
|
test('setRenderMode ignores invalid', () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
adapter.setRenderMode('invalid_mode');
|
|
assert.equal(adapter.renderMode, RENDER_MODE.FULL); // unchanged
|
|
});
|
|
|
|
|
|
console.log('\n══════ Destroy ══════');
|
|
|
|
test('destroy cleans up', () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
adapter.trackMessage('e1', 'm1');
|
|
|
|
adapter.destroy();
|
|
|
|
assert.equal(adapter.isConnected, false);
|
|
assert.equal(adapter.trackedMessages, 0);
|
|
});
|
|
|
|
|
|
console.log('\n══════ Custom Emoji in Glyphs ══════');
|
|
|
|
test('custom emoji replaces default glyphs', () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
adapter.registerMedia(MEDIA_TYPE.EMOJI, 'paw', '🐕'); // custom emoji for 'paw' glyph
|
|
|
|
const result = mockExpressionResult();
|
|
const output = adapter.renderExpression(result);
|
|
|
|
// Should contain custom emoji
|
|
assert.ok(output.text.includes('🐕'));
|
|
});
|
|
|
|
|
|
console.log('\n══════ Format Methods ══════');
|
|
|
|
test('formatBody — no body returns null', () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
assert.equal(adapter.formatBody(null, {}), null);
|
|
});
|
|
|
|
test('formatVoice — no voice returns null', () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
assert.equal(adapter.formatVoice(null, {}), null);
|
|
});
|
|
|
|
test('formatGlyphs — empty returns null', () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
assert.equal(adapter.formatGlyphs([], {}), null);
|
|
assert.equal(adapter.formatGlyphs(null, {}), null);
|
|
});
|
|
|
|
test('formatReaction — null returns null', () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
assert.equal(adapter.formatReaction(null), null);
|
|
});
|
|
|
|
test('formatAnimation — with registry', () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
adapter.registerMedia(MEDIA_TYPE.ANIMATION, 'butt_wiggle_fast', 'corgi_run.gif');
|
|
|
|
const result = mockExpressionResult();
|
|
assert.equal(adapter.formatAnimation('butt_wiggle_fast', result), 'corgi_run.gif');
|
|
});
|
|
|
|
test('formatAnimation — emotion fallback', () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
adapter.registerMedia(MEDIA_TYPE.ANIMATION, 'happy', 'happy_corgi.gif');
|
|
|
|
const result = mockExpressionResult();
|
|
assert.equal(adapter.formatAnimation('unknown_body', result), 'happy_corgi.gif');
|
|
});
|
|
|
|
|
|
console.log('\n══════ Events Emitted ══════');
|
|
|
|
await testAsync('emits delivered event', async () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
|
|
let deliveredEvent = null;
|
|
adapter.on('delivered', (data) => { deliveredEvent = data; });
|
|
|
|
await adapter.deliver(mockExpressionResult());
|
|
|
|
assert.ok(deliveredEvent);
|
|
assert.equal(deliveredEvent.expressionId, 'er_test_001');
|
|
assert.ok(deliveredEvent.messageId);
|
|
});
|
|
|
|
await testAsync('emits deliveryFailed event', async () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
adapter.sendMessage = async () => { throw new Error('timeout'); };
|
|
|
|
let failedEvent = null;
|
|
adapter.on('deliveryFailed', (data) => { failedEvent = data; });
|
|
|
|
await adapter.deliver(mockExpressionResult());
|
|
|
|
assert.ok(failedEvent);
|
|
assert.equal(failedEvent.error, 'timeout');
|
|
});
|
|
|
|
await testAsync('emits reactionProcessed event', async () => {
|
|
const pes = new MockPes();
|
|
const adapter = new TestAdapter(pes);
|
|
|
|
let reactionEvent = null;
|
|
adapter.on('reactionProcessed', (data) => { reactionEvent = data; });
|
|
|
|
await adapter.deliver(mockExpressionResult());
|
|
const msgId = adapter.sentMessages[0].msgId;
|
|
adapter.onUserReaction(msgId, '🔥', 'nikitron');
|
|
|
|
assert.ok(reactionEvent);
|
|
assert.equal(reactionEvent.reaction, '🔥');
|
|
});
|
|
|
|
|
|
console.log('\n══════ Constants ══════');
|
|
|
|
test('RENDER_MODE constants', () => {
|
|
assert.equal(RENDER_MODE.FULL, 'full');
|
|
assert.equal(RENDER_MODE.COMPACT, 'compact');
|
|
assert.equal(RENDER_MODE.RICH, 'rich');
|
|
assert.equal(RENDER_MODE.DEBUG, 'debug');
|
|
});
|
|
|
|
test('PRESENCE constants', () => {
|
|
assert.equal(PRESENCE.ONLINE, 'online');
|
|
assert.equal(PRESENCE.TYPING, 'typing');
|
|
assert.equal(PRESENCE.SLEEPING, 'sleeping');
|
|
assert.equal(PRESENCE.OFFLINE, 'offline');
|
|
});
|
|
|
|
test('MEDIA_TYPE constants', () => {
|
|
assert.equal(MEDIA_TYPE.STICKER, 'sticker');
|
|
assert.equal(MEDIA_TYPE.ANIMATION, 'animation');
|
|
assert.equal(MEDIA_TYPE.SOUND, 'sound');
|
|
assert.equal(MEDIA_TYPE.EMOJI, 'emoji');
|
|
assert.equal(MEDIA_TYPE.REACTION, 'reaction');
|
|
});
|
|
|
|
|
|
// ── SUMMARY ─────────────────────────────────────────────────
|
|
|
|
console.log(`\n${'═'.repeat(50)}`);
|
|
console.log(` РЕЗУЛЬТАТ: ${passed}/${total} тестов пройдено`);
|
|
console.log(`${'═'.repeat(50)}\n`);
|
|
|
|
if (passed !== total) process.exit(1);
|