vvlad1973-telegram-framework
Version:
Current version: *7.9.5*
330 lines (268 loc) • 8.85 kB
JavaScript
'use strict';
/**
* Test suite for Scenario user tracing functionality
* Tests memory leak prevention, tracing events, and integration
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { Scenario } from '../src/classes/scenario.js';
import SimpleLogger from '@vvlad1973/simple-logger';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import fs from 'fs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Mock logger that suppresses output during tests
class TestLogger extends SimpleLogger {
constructor() {
super();
}
trace() {}
info() {}
debug() {}
warn() {}
error() {}
}
// Mock user tracing service
class MockUserTracingService {
constructor() {
this.traces = new Map();
this.trackedUsers = new Set();
}
isTracing(userId) {
return this.trackedUsers.has(userId);
}
trace(userId, event, data) {
if (!this.traces.has(userId)) {
this.traces.set(userId, []);
}
this.traces.get(userId).push({
timestamp: Date.now(),
event,
data,
});
}
enableTracing(userId) {
this.trackedUsers.add(userId);
}
disableTracing(userId) {
this.trackedUsers.delete(userId);
}
getTraces(userId) {
return this.traces.get(userId) || [];
}
clear() {
this.traces.clear();
this.trackedUsers.clear();
}
}
// Test dialog content
const dialogContent = `
[
{
"_id": "START_DIALOG",
"received": {
"type": "command",
"text": "start"
},
"actions": [
{
"action": "testAction",
"options": {
"message": "Hello from start dialog"
}
}
]
},
{
"_id": "NESTED_DIALOG",
"state": "active",
"actions": [
{
"action": "doDialog",
"options": {
"dialog": "CHILD_DIALOG"
}
}
]
},
{
"_id": "CHILD_DIALOG",
"actions": [
{
"action": "testAction",
"options": {
"message": "Child dialog executed"
}
}
]
},
{
"_id": "SWITCH_DIALOG",
"received": {
"type": "message"
},
"actions": [
{
"action": "doSwitch",
"options": {
"value": "test"
},
"cases": [
{
"case": "test",
"actions": [
{
"action": "testAction",
"options": {
"message": "Switch case matched"
}
}
]
}
]
}
]
}
]
`.trim();
describe('Scenario User Tracing', () => {
let dialogPath;
beforeEach(() => {
dialogPath = join(__dirname, 'test-dialogs.jsonc');
fs.writeFileSync(dialogPath, dialogContent, 'utf8');
});
afterEach(() => {
if (fs.existsSync(dialogPath)) {
fs.unlinkSync(dialogPath);
}
});
it('should cleanup context after successful execution', async () => {
const scenario = new Scenario(new TestLogger());
const tracingService = new MockUserTracingService();
scenario.userTracing = tracingService;
tracingService.enableTracing(12345);
await scenario.loadDialogs([dialogPath]);
scenario.actions['testAction'] = async () => true;
const request = { type: 'command', contents: 'start' };
const user = { id: 12345 };
const context = {};
await scenario.process(request, user, context);
// Access private field via reflection (for testing only)
const traceContext = scenario['#traceContext'];
expect(traceContext).toBeUndefined();
});
it('should cleanup context after error', async () => {
const scenario = new Scenario(new TestLogger());
const tracingService = new MockUserTracingService();
scenario.userTracing = tracingService;
tracingService.enableTracing(67890);
await scenario.loadDialogs([dialogPath]);
scenario.actions['testAction'] = async () => {
throw new Error('Test error');
};
const request = { type: 'command', contents: 'start' };
const user = { id: 67890 };
const context = {};
await scenario.process(request, user, context);
const traceContext = scenario['#traceContext'];
expect(traceContext).toBeUndefined();
});
it('should capture tracing events correctly', async () => {
const scenario = new Scenario(new TestLogger());
const tracingService = new MockUserTracingService();
scenario.userTracing = tracingService;
tracingService.enableTracing(111);
await scenario.loadDialogs([dialogPath]);
scenario.actions['testAction'] = async () => true;
const request = { type: 'command', contents: 'start' };
const user = { id: 111 };
const context = {};
await scenario.process(request, user, context);
const traces = tracingService.getTraces(111);
expect(traces.length).toBeGreaterThan(0);
const events = traces.map((t) => t.event);
expect(events).toContain('request_received');
expect(events).toContain('dialog_matched');
expect(events).toContain('action_start');
expect(events).toContain('action_result');
expect(events).toContain('request_complete');
});
it('should work without userTracing service', async () => {
const scenario = new Scenario(new TestLogger());
// userTracing is null by default
await scenario.loadDialogs([dialogPath]);
scenario.actions['testAction'] = async () => true;
const request = { type: 'command', contents: 'start' };
const user = { id: 222 };
const context = {};
// Should not throw error
await expect(scenario.process(request, user, context)).resolves.toBeUndefined();
});
it('should trace only for enabled users', async () => {
const scenario = new Scenario(new TestLogger());
const tracingService = new MockUserTracingService();
scenario.userTracing = tracingService;
tracingService.enableTracing(333);
await scenario.loadDialogs([dialogPath]);
scenario.actions['testAction'] = async () => true;
const request = { type: 'command', contents: 'start' };
await scenario.process(request, { id: 333 }, {});
await scenario.process(request, { id: 444 }, {});
const traces333 = tracingService.getTraces(333);
const traces444 = tracingService.getTraces(444);
expect(traces333.length).toBeGreaterThan(0);
expect(traces444.length).toBe(0);
});
it('should trace doDialog correctly', async () => {
const scenario = new Scenario(new TestLogger());
const tracingService = new MockUserTracingService();
scenario.userTracing = tracingService;
tracingService.enableTracing(555);
await scenario.loadDialogs([dialogPath]);
scenario.actions['testAction'] = async () => true;
const request = { type: 'message', contents: 'test' };
const user = { id: 555, state: 'active' };
const context = {};
await scenario.process(request, user, context);
const traces = tracingService.getTraces(555);
const events = traces.map((t) => t.event);
expect(events).toContain('do_dialog');
expect(events).toContain('do_dialog_return');
});
it('should trace doSwitch correctly', async () => {
const scenario = new Scenario(new TestLogger());
const tracingService = new MockUserTracingService();
scenario.userTracing = tracingService;
tracingService.enableTracing(666);
await scenario.loadDialogs([dialogPath]);
scenario.actions['testAction'] = async () => true;
const request = { type: 'message', contents: 'anything' };
const user = { id: 666 };
const context = {};
await scenario.process(request, user, context);
const traces = tracingService.getTraces(666);
const events = traces.map((t) => t.event);
expect(events).toContain('switch_start');
expect(events).toContain('switch_case_matched');
});
it('should format large strings in traces', async () => {
const scenario = new Scenario(new TestLogger());
const tracingService = new MockUserTracingService();
scenario.userTracing = tracingService;
tracingService.enableTracing(777);
await scenario.loadDialogs([dialogPath]);
scenario.actions['testAction'] = async () => true;
const largeString = 'x'.repeat(300);
const request = { type: 'command', contents: largeString };
const user = { id: 777 };
const context = {};
await scenario.process(request, user, context);
const traces = tracingService.getTraces(777);
const requestReceived = traces.find((t) => t.event === 'request_received');
expect(requestReceived).toBeDefined();
const contentsData = requestReceived.data.request.contents;
expect(contentsData).toHaveProperty('_type', 'string');
expect(contentsData).toHaveProperty('_len', 300);
expect(contentsData).toHaveProperty('_preview');
});
});