chz-telegram-bot 0.7.13 → 0.7.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/dist/dtos/propertyProviderSets.d.ts +0 -1
  2. package/dist/dtos/propertyProviderSets.d.ts.map +1 -1
  3. package/dist/entities/actions/commandAction.d.ts +1 -1
  4. package/dist/entities/actions/commandAction.d.ts.map +1 -1
  5. package/dist/entities/actions/commandAction.js +2 -2
  6. package/dist/helpers/builders/commandActionBuilder.d.ts.map +1 -1
  7. package/dist/helpers/builders/commandActionBuilder.js +2 -3
  8. package/package.json +4 -1
  9. package/eslint.config.ts +0 -62
  10. package/src/builtin/helpAction.ts +0 -17
  11. package/src/dtos/chatHistoryMessage.ts +0 -22
  12. package/src/dtos/chatInfo.ts +0 -12
  13. package/src/dtos/commandTriggerCheckResult.ts +0 -40
  14. package/src/dtos/cooldownInfo.ts +0 -10
  15. package/src/dtos/incomingMessage.ts +0 -71
  16. package/src/dtos/incomingQuery.ts +0 -14
  17. package/src/dtos/messageInfo.ts +0 -15
  18. package/src/dtos/propertyProviderSets.ts +0 -21
  19. package/src/dtos/replyInfo.ts +0 -9
  20. package/src/dtos/responses/delay.ts +0 -28
  21. package/src/dtos/responses/imageMessage.ts +0 -41
  22. package/src/dtos/responses/inlineQueryResponse.ts +0 -26
  23. package/src/dtos/responses/reaction.ts +0 -30
  24. package/src/dtos/responses/textMessage.ts +0 -44
  25. package/src/dtos/responses/unpin.ts +0 -27
  26. package/src/dtos/responses/videoMessage.ts +0 -41
  27. package/src/dtos/userInfo.ts +0 -8
  28. package/src/entities/actions/commandAction.ts +0 -275
  29. package/src/entities/actions/inlineQueryAction.ts +0 -83
  30. package/src/entities/actions/replyCaptureAction.ts +0 -110
  31. package/src/entities/actions/scheduledAction.ts +0 -182
  32. package/src/entities/botInstance.ts +0 -92
  33. package/src/entities/cachedStateFactory.ts +0 -14
  34. package/src/entities/context/baseContext.ts +0 -111
  35. package/src/entities/context/chatContext.ts +0 -135
  36. package/src/entities/context/inlineQueryContext.ts +0 -63
  37. package/src/entities/context/messageContext.ts +0 -250
  38. package/src/entities/context/replyContext.ts +0 -260
  39. package/src/entities/states/actionStateBase.ts +0 -6
  40. package/src/entities/taskRecord.ts +0 -11
  41. package/src/helpers/builders/commandActionBuilder.ts +0 -214
  42. package/src/helpers/builders/inlineQueryActionBuilder.ts +0 -71
  43. package/src/helpers/builders/scheduledActionBuilder.ts +0 -143
  44. package/src/helpers/mapUtils.ts +0 -28
  45. package/src/helpers/noop.ts +0 -20
  46. package/src/helpers/objectFromEntries.ts +0 -7
  47. package/src/helpers/timeConvertions.ts +0 -13
  48. package/src/helpers/toArray.ts +0 -3
  49. package/src/helpers/traceFactory.ts +0 -11
  50. package/src/index.ts +0 -33
  51. package/src/main.ts +0 -76
  52. package/src/services/actionProcessingService.ts +0 -125
  53. package/src/services/actionProcessors/baseProcessor.ts +0 -67
  54. package/src/services/actionProcessors/commandActionProcessor.ts +0 -231
  55. package/src/services/actionProcessors/inlineQueryActionProcessor.ts +0 -165
  56. package/src/services/actionProcessors/scheduledActionProcessor.ts +0 -136
  57. package/src/services/jsonFileStorage.ts +0 -181
  58. package/src/services/nodeTimeoutScheduler.ts +0 -79
  59. package/src/services/responseProcessingQueue.ts +0 -57
  60. package/src/services/telegramApi.ts +0 -278
  61. package/src/types/action.ts +0 -15
  62. package/src/types/actionState.ts +0 -4
  63. package/src/types/cachedValueAccessor.ts +0 -1
  64. package/src/types/capture.ts +0 -33
  65. package/src/types/commandCondition.ts +0 -9
  66. package/src/types/commandTrigger.ts +0 -1
  67. package/src/types/events.ts +0 -286
  68. package/src/types/externalAliases.ts +0 -18
  69. package/src/types/handlers.ts +0 -26
  70. package/src/types/inputFile.ts +0 -4
  71. package/src/types/messageSendingOptions.ts +0 -10
  72. package/src/types/messageTypes.ts +0 -21
  73. package/src/types/propertyProvider.ts +0 -14
  74. package/src/types/response.ts +0 -51
  75. package/src/types/scheduler.ts +0 -20
  76. package/src/types/storage.ts +0 -23
  77. package/src/types/timeValues.ts +0 -33
  78. package/src/types/trace.ts +0 -5
  79. package/tests/dtos/commandTriggerCheckResult.test.ts +0 -301
  80. package/tests/entities/actions/inlineQueryAction.test.ts +0 -359
  81. package/tests/entities/actions/replyCaptureAction.test.ts +0 -501
  82. package/tests/entities/cachedStateFactory.test.ts +0 -98
  83. package/tests/entities/context/chatContext.test.ts +0 -606
  84. package/tests/entities/context/messageContext.test.ts +0 -370
  85. package/tests/entities/states/actionStateBase.test.ts +0 -138
  86. package/tests/entities/taskRecord.test.ts +0 -195
  87. package/tests/helpers/mapUtils.test.ts +0 -163
  88. package/tests/helpers/timeConvertions.test.ts +0 -129
  89. package/tests/services/actionProcessors/baseActionProcessor.test.ts +0 -359
  90. package/tests/services/actionProcessors/commandActionProcessor.test.ts +0 -268
  91. package/tests/services/actionProcessors/inlineQueryActionProcessor.test.ts +0 -616
  92. package/tests/services/actionProcessors/processorTestHelpers.ts +0 -147
  93. package/tests/services/actionProcessors/scheduledActionProcessor.test.ts +0 -153
  94. package/tests/services/jsonFileStorage.test.ts +0 -927
  95. package/tests/services/nodeTimeoutScheduler.test.ts +0 -421
  96. package/tests/services/responseProcessingQueue.test.ts +0 -388
  97. package/tsconfig.build.json +0 -8
  98. package/tsconfig.json +0 -118
@@ -1,927 +0,0 @@
1
- import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
2
- import { JsonFileStorage } from '../../src/services/jsonFileStorage';
3
- import { IActionState } from '../../src/types/actionState';
4
- import { ActionKey, IActionWithState } from '../../src/types/action';
5
- import { rmSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
6
- import { dirname } from 'node:path';
7
-
8
- interface TestActionState extends IActionState {
9
- customField: string;
10
- }
11
-
12
- function buildPath(storagePath: string, botName: string, actionKey: string) {
13
- return `${storagePath}/${botName}/${actionKey.replaceAll(':', '/')}.json`;
14
- }
15
-
16
- function createTestAction(
17
- key: string,
18
- defaultState?: Partial<TestActionState>
19
- ): IActionWithState<TestActionState> {
20
- return {
21
- key: key as ActionKey,
22
- stateConstructor: () => ({
23
- lastExecutedDate: 0,
24
- pinnedMessages: [],
25
- customField: 'default',
26
- ...defaultState
27
- }),
28
- exec: () => Promise.resolve([])
29
- };
30
- }
31
-
32
- function ensureActionFileExists(
33
- storagePath: string,
34
- botName: string,
35
- actionKey: string,
36
- content: Record<number, unknown> = {}
37
- ) {
38
- const filePath = buildPath(storagePath, botName, actionKey);
39
- const dir = dirname(filePath);
40
- if (!existsSync(dir)) {
41
- mkdirSync(dir, { recursive: true });
42
- }
43
- writeFileSync(filePath, JSON.stringify(content));
44
- }
45
-
46
- const TEST_STORAGE_PATH = 'test-storage';
47
- const TEST_BOT_NAME = 'test-bot';
48
-
49
- describe('JsonFileStorage', () => {
50
- let storage: JsonFileStorage | null;
51
- let testAction: IActionWithState<TestActionState>;
52
-
53
- beforeEach(() => {
54
- // Clean up test storage before each test
55
- if (existsSync(TEST_STORAGE_PATH)) {
56
- rmSync(TEST_STORAGE_PATH, { recursive: true, force: true });
57
- }
58
-
59
- testAction = createTestAction('test:action');
60
- storage = null;
61
-
62
- // Pre-create the storage structure for the default test action
63
- ensureActionFileExists(TEST_STORAGE_PATH, TEST_BOT_NAME, 'test:action');
64
- });
65
-
66
- afterEach(() => {
67
- // Don't call close() in cleanup - it acquires locks permanently
68
- // Just reset the reference
69
- storage = null;
70
-
71
- // Clean up test storage after each test
72
- if (existsSync(TEST_STORAGE_PATH)) {
73
- rmSync(TEST_STORAGE_PATH, { recursive: true, force: true });
74
- }
75
- });
76
-
77
- describe('constructor', () => {
78
- test('should create storage directory if it does not exist', () => {
79
- storage = new JsonFileStorage(
80
- TEST_BOT_NAME,
81
- [testAction],
82
- TEST_STORAGE_PATH
83
- );
84
-
85
- expect(existsSync(`${TEST_STORAGE_PATH}/${TEST_BOT_NAME}/`)).toBe(
86
- true
87
- );
88
- });
89
-
90
- test('should initialize locks for all actions', () => {
91
- const action1 = createTestAction('action:one');
92
- const action2 = createTestAction('action:two');
93
-
94
- // Pre-create files for these actions
95
- ensureActionFileExists(
96
- TEST_STORAGE_PATH,
97
- TEST_BOT_NAME,
98
- 'action:one'
99
- );
100
- ensureActionFileExists(
101
- TEST_STORAGE_PATH,
102
- TEST_BOT_NAME,
103
- 'action:two'
104
- );
105
-
106
- storage = new JsonFileStorage(
107
- TEST_BOT_NAME,
108
- [action1, action2],
109
- TEST_STORAGE_PATH
110
- );
111
-
112
- // Both actions should be loadable without deadlock
113
- const [result1, result2] = [
114
- storage.load(action1),
115
- storage.load(action2)
116
- ];
117
-
118
- expect(result1).toEqual({});
119
- expect(result2).toEqual({});
120
- });
121
- });
122
-
123
- describe('load', () => {
124
- test('should return empty object for new action', () => {
125
- storage = new JsonFileStorage(
126
- TEST_BOT_NAME,
127
- [testAction],
128
- TEST_STORAGE_PATH
129
- );
130
-
131
- const result = storage.load(testAction);
132
-
133
- expect(result).toEqual({});
134
- });
135
-
136
- test('should load existing data from file', () => {
137
- // Pre-create the file with data
138
- const dirPath = `${TEST_STORAGE_PATH}/${TEST_BOT_NAME}/test`;
139
- mkdirSync(dirPath, { recursive: true });
140
-
141
- const existingData: Record<number, TestActionState> = {
142
- 123: {
143
- lastExecutedDate: 1000,
144
- pinnedMessages: [1, 2, 3],
145
- customField: 'existing'
146
- }
147
- };
148
- writeFileSync(
149
- `${dirPath}/action.json`,
150
- JSON.stringify(existingData)
151
- );
152
-
153
- storage = new JsonFileStorage(
154
- TEST_BOT_NAME,
155
- [testAction],
156
- TEST_STORAGE_PATH
157
- );
158
-
159
- const result = storage.load(testAction);
160
-
161
- expect(result).toEqual(existingData);
162
- });
163
-
164
- test('should cache loaded data and not re-read from file', () => {
165
- // Pre-create the file with initial data
166
- const dirPath = `${TEST_STORAGE_PATH}/${TEST_BOT_NAME}/test`;
167
- mkdirSync(dirPath, { recursive: true });
168
-
169
- const initialData: Record<number, TestActionState> = {
170
- 123: {
171
- lastExecutedDate: 1000,
172
- pinnedMessages: [1],
173
- customField: 'initial'
174
- }
175
- };
176
- writeFileSync(
177
- `${dirPath}/action.json`,
178
- JSON.stringify(initialData)
179
- );
180
-
181
- storage = new JsonFileStorage(
182
- TEST_BOT_NAME,
183
- [testAction],
184
- TEST_STORAGE_PATH
185
- );
186
-
187
- // First load - reads from file
188
- const result1 = storage.load(testAction);
189
- expect(result1).toEqual(initialData);
190
-
191
- // Modify the file directly (simulating external change)
192
- const modifiedData: Record<number, TestActionState> = {
193
- 123: {
194
- lastExecutedDate: 9999,
195
- pinnedMessages: [9, 9, 9],
196
- customField: 'modified-externally'
197
- }
198
- };
199
- writeFileSync(
200
- `${dirPath}/action.json`,
201
- JSON.stringify(modifiedData)
202
- );
203
-
204
- // Second load - should return cached data, not the modified file
205
- const result2 = storage.load(testAction);
206
- expect(result2).toEqual(initialData);
207
- expect(result2).not.toEqual(modifiedData);
208
- });
209
- });
210
-
211
- describe('getActionState', () => {
212
- test('should return default state for new chat', () => {
213
- storage = new JsonFileStorage(
214
- TEST_BOT_NAME,
215
- [testAction],
216
- TEST_STORAGE_PATH
217
- );
218
-
219
- const result = storage.getActionState(testAction, 123);
220
-
221
- expect(result).toEqual({
222
- lastExecutedDate: 0,
223
- pinnedMessages: [],
224
- customField: 'default'
225
- });
226
- });
227
-
228
- test('should return existing state for known chat', () => {
229
- // Pre-create file with data
230
- const dirPath = `${TEST_STORAGE_PATH}/${TEST_BOT_NAME}/test`;
231
- mkdirSync(dirPath, { recursive: true });
232
-
233
- const existingData: Record<number, TestActionState> = {
234
- 456: {
235
- lastExecutedDate: 2000,
236
- pinnedMessages: [10, 20],
237
- customField: 'saved'
238
- }
239
- };
240
- writeFileSync(
241
- `${dirPath}/action.json`,
242
- JSON.stringify(existingData)
243
- );
244
-
245
- storage = new JsonFileStorage(
246
- TEST_BOT_NAME,
247
- [testAction],
248
- TEST_STORAGE_PATH
249
- );
250
-
251
- const result = storage.getActionState(testAction, 456);
252
-
253
- expect(result).toEqual(existingData[456]);
254
- });
255
- });
256
-
257
- describe('saveActionExecutionResult', () => {
258
- test('should save state to file', async () => {
259
- storage = new JsonFileStorage(
260
- TEST_BOT_NAME,
261
- [testAction],
262
- TEST_STORAGE_PATH
263
- );
264
-
265
- const newState: TestActionState = {
266
- lastExecutedDate: 5000,
267
- pinnedMessages: [100],
268
- customField: 'newValue'
269
- };
270
-
271
- await storage.saveActionExecutionResult(testAction, 123, newState);
272
-
273
- // Verify by loading
274
- const result = storage.getActionState(testAction, 123);
275
- expect(result).toEqual(newState);
276
- });
277
-
278
- test('should preserve existing chat states when saving new one', async () => {
279
- storage = new JsonFileStorage(
280
- TEST_BOT_NAME,
281
- [testAction],
282
- TEST_STORAGE_PATH
283
- );
284
-
285
- const state1: TestActionState = {
286
- lastExecutedDate: 1000,
287
- pinnedMessages: [1],
288
- customField: 'chat1'
289
- };
290
- const state2: TestActionState = {
291
- lastExecutedDate: 2000,
292
- pinnedMessages: [2],
293
- customField: 'chat2'
294
- };
295
-
296
- await storage.saveActionExecutionResult(testAction, 111, state1);
297
- await storage.saveActionExecutionResult(testAction, 222, state2);
298
-
299
- const result1 = storage.getActionState(testAction, 111);
300
- const result2 = storage.getActionState(testAction, 222);
301
-
302
- expect(result1).toEqual(state1);
303
- expect(result2).toEqual(state2);
304
- });
305
- });
306
-
307
- describe('updateStateFor', () => {
308
- test('should update existing state', async () => {
309
- storage = new JsonFileStorage(
310
- TEST_BOT_NAME,
311
- [testAction],
312
- TEST_STORAGE_PATH
313
- );
314
-
315
- // First save initial state
316
- await storage.saveActionExecutionResult(testAction, 123, {
317
- lastExecutedDate: 1000,
318
- pinnedMessages: [1],
319
- customField: 'initial'
320
- });
321
-
322
- // Update the state
323
- await storage.updateStateFor(testAction, 123, (state) => {
324
- state.customField = 'updated';
325
- state.pinnedMessages.push(2);
326
- });
327
-
328
- const result = storage.getActionState(testAction, 123);
329
-
330
- expect(result.customField).toBe('updated');
331
- expect(result.pinnedMessages).toEqual([1, 2]);
332
- });
333
-
334
- test('should support async update function', async () => {
335
- storage = new JsonFileStorage(
336
- TEST_BOT_NAME,
337
- [testAction],
338
- TEST_STORAGE_PATH
339
- );
340
-
341
- await storage.saveActionExecutionResult(testAction, 123, {
342
- lastExecutedDate: 1000,
343
- pinnedMessages: [],
344
- customField: 'initial'
345
- });
346
-
347
- const asyncUpdate = async (state: TestActionState) => {
348
- await new Promise((resolve) => setTimeout(resolve, 10));
349
- state.customField = 'async-updated';
350
- };
351
- await storage.updateStateFor(testAction, 123, asyncUpdate);
352
-
353
- const result = storage.getActionState(testAction, 123);
354
-
355
- expect(result.customField).toBe('async-updated');
356
- });
357
- });
358
-
359
- describe('close', () => {
360
- test('should have close method defined', () => {
361
- const localStorage = new JsonFileStorage(
362
- TEST_BOT_NAME,
363
- [testAction],
364
- TEST_STORAGE_PATH
365
- );
366
-
367
- // Just verify the method exists and is callable
368
- expect(typeof localStorage.close).toBe('function');
369
- });
370
- });
371
-
372
- describe('locking behavior', () => {
373
- test('should not block on concurrent read operations', async () => {
374
- const localStorage = new JsonFileStorage(
375
- TEST_BOT_NAME,
376
- [testAction],
377
- TEST_STORAGE_PATH
378
- );
379
-
380
- // Pre-populate with data
381
- await localStorage.saveActionExecutionResult(testAction, 123, {
382
- lastExecutedDate: 1000,
383
- pinnedMessages: [1, 2, 3],
384
- customField: 'test-data'
385
- });
386
-
387
- // Perform many concurrent reads - should complete quickly without blocking
388
- const startTime = Date.now();
389
- const readPromises = Array.from({ length: 100 }, () =>
390
- Promise.resolve(localStorage.load(testAction))
391
- );
392
-
393
- const results = await Promise.all(readPromises);
394
- const elapsed = Date.now() - startTime;
395
-
396
- // All reads should return the same cached data
397
- for (const result of results) {
398
- expect(result[123].customField).toBe('test-data');
399
- }
400
-
401
- // Reads should complete quickly (no lock contention)
402
- // If reads were blocking, 100 sequential operations would take much longer
403
- expect(elapsed).toBeLessThan(100);
404
- });
405
-
406
- test('should serialize concurrent operations on same action', async () => {
407
- const localStorage = new JsonFileStorage(
408
- TEST_BOT_NAME,
409
- [testAction],
410
- TEST_STORAGE_PATH
411
- );
412
-
413
- const operationOrder: number[] = [];
414
-
415
- // Start multiple concurrent save operations
416
- const promises = [1, 2, 3].map(async (num) => {
417
- await localStorage.saveActionExecutionResult(testAction, 123, {
418
- lastExecutedDate: num * 1000,
419
- pinnedMessages: [num],
420
- customField: `value${num}`
421
- });
422
- operationOrder.push(num);
423
- });
424
-
425
- await Promise.all(promises);
426
-
427
- // All operations should complete (order may vary due to async)
428
- const sortedOrder = [...operationOrder].sort((a, b) => a - b);
429
- expect(sortedOrder).toEqual([1, 2, 3]);
430
- });
431
- });
432
-
433
- describe('path handling', () => {
434
- test('should handle action keys with colons by converting to path separators', async () => {
435
- const nestedAction = createTestAction('nested:path:action');
436
-
437
- // Pre-create the nested action file
438
- ensureActionFileExists(
439
- TEST_STORAGE_PATH,
440
- TEST_BOT_NAME,
441
- 'nested:path:action'
442
- );
443
-
444
- storage = new JsonFileStorage(
445
- TEST_BOT_NAME,
446
- [nestedAction],
447
- TEST_STORAGE_PATH
448
- );
449
-
450
- await storage.saveActionExecutionResult(nestedAction, 123, {
451
- lastExecutedDate: 1000,
452
- pinnedMessages: [],
453
- customField: 'nested'
454
- });
455
-
456
- // Verify the file was created at the correct nested path
457
- expect(
458
- existsSync(
459
- `${TEST_STORAGE_PATH}/${TEST_BOT_NAME}/nested/path/action.json`
460
- )
461
- ).toBe(true);
462
- });
463
-
464
- test('should use default storage path when not provided', () => {
465
- storage = new JsonFileStorage(
466
- TEST_BOT_NAME,
467
- [testAction]
468
- // No path provided - should use 'storage' default
469
- );
470
-
471
- expect(existsSync(`storage/${TEST_BOT_NAME}/`)).toBe(true);
472
-
473
- // Cleanup default storage
474
- rmSync(`storage/${TEST_BOT_NAME}/`, {
475
- recursive: true,
476
- force: true
477
- });
478
- });
479
- });
480
-
481
- describe('dynamically registered actions', () => {
482
- test('should handle actions not registered in constructor', async () => {
483
- const dynamicAction = createTestAction('dynamic:action');
484
-
485
- // Pre-create file for the dynamic action
486
- ensureActionFileExists(
487
- TEST_STORAGE_PATH,
488
- TEST_BOT_NAME,
489
- 'dynamic:action'
490
- );
491
-
492
- storage = new JsonFileStorage(
493
- TEST_BOT_NAME,
494
- [], // No actions registered initially
495
- TEST_STORAGE_PATH
496
- );
497
-
498
- // Should be able to load and save for unregistered action
499
- const loadResult = storage.load(dynamicAction);
500
- expect(loadResult).toEqual({});
501
-
502
- await storage.saveActionExecutionResult(dynamicAction, 123, {
503
- lastExecutedDate: 1000,
504
- pinnedMessages: [],
505
- customField: 'dynamic'
506
- });
507
-
508
- const result = storage.getActionState(dynamicAction, 123);
509
- expect(result.customField).toBe('dynamic');
510
- });
511
- });
512
-
513
- // Tests based on real usage patterns in the codebase
514
- describe('command/scheduled action workflow', () => {
515
- // Pattern: getActionState -> handler executes -> saveActionExecutionResult
516
- // Used by CommandAction and ScheduledAction
517
-
518
- test('should support get-modify-save workflow (command action pattern)', async () => {
519
- storage = new JsonFileStorage(
520
- TEST_BOT_NAME,
521
- [testAction],
522
- TEST_STORAGE_PATH
523
- );
524
-
525
- const chatId = 12345;
526
-
527
- // Step 1: Get initial state (like CommandAction.exec does)
528
- const initialState = storage.getActionState(testAction, chatId);
529
- expect(initialState.lastExecutedDate).toBe(0);
530
-
531
- // Step 2: Modify state (simulating action execution)
532
- const newState: TestActionState = {
533
- ...initialState,
534
- lastExecutedDate: Date.now(),
535
- customField: 'executed'
536
- };
537
-
538
- // Step 3: Save the result (like CommandAction.exec does after handler)
539
- await storage.saveActionExecutionResult(
540
- testAction,
541
- chatId,
542
- newState
543
- );
544
-
545
- // Verify persistence
546
- const loadedState = storage.getActionState(testAction, chatId);
547
- expect(loadedState.lastExecutedDate).toBe(
548
- newState.lastExecutedDate
549
- );
550
- expect(loadedState.customField).toBe('executed');
551
- });
552
-
553
- test('should maintain isolation between different chats', async () => {
554
- storage = new JsonFileStorage(
555
- TEST_BOT_NAME,
556
- [testAction],
557
- TEST_STORAGE_PATH
558
- );
559
-
560
- const chat1 = 111;
561
- const chat2 = 222;
562
- const chat3 = 333;
563
-
564
- // Save different states for different chats
565
- await storage.saveActionExecutionResult(testAction, chat1, {
566
- lastExecutedDate: 1000,
567
- pinnedMessages: [1],
568
- customField: 'chat1'
569
- });
570
- await storage.saveActionExecutionResult(testAction, chat2, {
571
- lastExecutedDate: 2000,
572
- pinnedMessages: [2],
573
- customField: 'chat2'
574
- });
575
-
576
- // Verify each chat has its own state
577
- const state1 = storage.getActionState(testAction, chat1);
578
- const state2 = storage.getActionState(testAction, chat2);
579
- const state3 = storage.getActionState(testAction, chat3);
580
-
581
- expect(state1.customField).toBe('chat1');
582
- expect(state2.customField).toBe('chat2');
583
- expect(state3.customField).toBe('default'); // Unmodified chat gets default
584
- });
585
-
586
- test('should maintain isolation between different actions', async () => {
587
- const commandAction = createTestAction('command:myCommand');
588
- const scheduledAction = createTestAction('scheduled:myScheduled');
589
-
590
- ensureActionFileExists(
591
- TEST_STORAGE_PATH,
592
- TEST_BOT_NAME,
593
- 'command:myCommand'
594
- );
595
- ensureActionFileExists(
596
- TEST_STORAGE_PATH,
597
- TEST_BOT_NAME,
598
- 'scheduled:myScheduled'
599
- );
600
-
601
- storage = new JsonFileStorage(
602
- TEST_BOT_NAME,
603
- [commandAction, scheduledAction],
604
- TEST_STORAGE_PATH
605
- );
606
-
607
- const chatId = 123;
608
-
609
- // Save states for same chat but different actions
610
- await storage.saveActionExecutionResult(commandAction, chatId, {
611
- lastExecutedDate: 1000,
612
- pinnedMessages: [],
613
- customField: 'from-command'
614
- });
615
- await storage.saveActionExecutionResult(scheduledAction, chatId, {
616
- lastExecutedDate: 2000,
617
- pinnedMessages: [],
618
- customField: 'from-scheduled'
619
- });
620
-
621
- // Each action should have its own state
622
- const cmdState = storage.getActionState(commandAction, chatId);
623
- const schedState = storage.getActionState(scheduledAction, chatId);
624
-
625
- expect(cmdState.customField).toBe('from-command');
626
- expect(schedState.customField).toBe('from-scheduled');
627
- });
628
- });
629
-
630
- describe('cross-action state access (BaseContext pattern)', () => {
631
- // Pattern: load() to get all states, then access specific chat
632
- // Used by BaseContext.loadStateOf()
633
-
634
- test('should support loading all states for an action', async () => {
635
- storage = new JsonFileStorage(
636
- TEST_BOT_NAME,
637
- [testAction],
638
- TEST_STORAGE_PATH
639
- );
640
-
641
- // Populate states for multiple chats
642
- await storage.saveActionExecutionResult(testAction, 100, {
643
- lastExecutedDate: 1000,
644
- pinnedMessages: [],
645
- customField: 'chat100'
646
- });
647
- await storage.saveActionExecutionResult(testAction, 200, {
648
- lastExecutedDate: 2000,
649
- pinnedMessages: [],
650
- customField: 'chat200'
651
- });
652
-
653
- // Load all states (like BaseContext.loadStateOf does)
654
- const allStates = storage.load(testAction);
655
-
656
- expect(Object.keys(allStates).length).toBe(2);
657
- expect(allStates[100].customField).toBe('chat100');
658
- expect(allStates[200].customField).toBe('chat200');
659
- });
660
-
661
- test('should support updateStateFor from another action context', async () => {
662
- const otherAction = createTestAction('other:action');
663
- ensureActionFileExists(
664
- TEST_STORAGE_PATH,
665
- TEST_BOT_NAME,
666
- 'other:action'
667
- );
668
-
669
- storage = new JsonFileStorage(
670
- TEST_BOT_NAME,
671
- [testAction, otherAction],
672
- TEST_STORAGE_PATH
673
- );
674
-
675
- const chatId = 456;
676
-
677
- // First, save initial state
678
- await storage.saveActionExecutionResult(otherAction, chatId, {
679
- lastExecutedDate: 1000,
680
- pinnedMessages: [1, 2],
681
- customField: 'initial'
682
- });
683
-
684
- // Update state from different context (like BaseContext.updateStateOf)
685
- await storage.updateStateFor(otherAction, chatId, (state) => {
686
- state.pinnedMessages.push(3);
687
- state.customField = 'modified-externally';
688
- });
689
-
690
- const result = storage.getActionState(otherAction, chatId);
691
- expect(result.pinnedMessages).toEqual([1, 2, 3]);
692
- expect(result.customField).toBe('modified-externally');
693
- });
694
- });
695
-
696
- describe('pinnedMessages array handling', () => {
697
- // IActionState includes pinnedMessages array - test array operations
698
-
699
- test('should correctly persist and restore arrays', async () => {
700
- storage = new JsonFileStorage(
701
- TEST_BOT_NAME,
702
- [testAction],
703
- TEST_STORAGE_PATH
704
- );
705
-
706
- await storage.saveActionExecutionResult(testAction, 123, {
707
- lastExecutedDate: 0,
708
- pinnedMessages: [101, 102, 103, 104, 105],
709
- customField: 'test'
710
- });
711
-
712
- const loaded = storage.getActionState(testAction, 123);
713
- expect(loaded.pinnedMessages).toEqual([101, 102, 103, 104, 105]);
714
- expect(loaded.pinnedMessages.length).toBe(5);
715
- });
716
-
717
- test('should handle empty arrays', async () => {
718
- storage = new JsonFileStorage(
719
- TEST_BOT_NAME,
720
- [testAction],
721
- TEST_STORAGE_PATH
722
- );
723
-
724
- await storage.saveActionExecutionResult(testAction, 123, {
725
- lastExecutedDate: 0,
726
- pinnedMessages: [],
727
- customField: 'test'
728
- });
729
-
730
- const loaded = storage.getActionState(testAction, 123);
731
- expect(loaded.pinnedMessages).toEqual([]);
732
- expect(Array.isArray(loaded.pinnedMessages)).toBe(true);
733
- });
734
- });
735
-
736
- describe('lastExecutedDate handling', () => {
737
- // CommandAction and ScheduledAction update lastExecutedDate after execution
738
-
739
- test('should persist timestamp values correctly', async () => {
740
- storage = new JsonFileStorage(
741
- TEST_BOT_NAME,
742
- [testAction],
743
- TEST_STORAGE_PATH
744
- );
745
-
746
- const timestamp = Date.now();
747
-
748
- await storage.saveActionExecutionResult(testAction, 123, {
749
- lastExecutedDate: timestamp,
750
- pinnedMessages: [],
751
- customField: 'test'
752
- });
753
-
754
- const loaded = storage.getActionState(testAction, 123);
755
- expect(loaded.lastExecutedDate).toBe(timestamp);
756
- });
757
-
758
- test('should handle zero timestamps (never executed)', () => {
759
- storage = new JsonFileStorage(
760
- TEST_BOT_NAME,
761
- [testAction],
762
- TEST_STORAGE_PATH
763
- );
764
-
765
- const state = storage.getActionState(testAction, 999);
766
- expect(state.lastExecutedDate).toBe(0);
767
- });
768
- });
769
-
770
- describe('state constructor usage', () => {
771
- // Actions provide stateConstructor for default state
772
-
773
- test('should use stateConstructor for new chats', () => {
774
- const actionWithDefaults = createTestAction('action:defaults', {
775
- lastExecutedDate: 0,
776
- pinnedMessages: [999],
777
- customField: 'custom-default'
778
- });
779
- ensureActionFileExists(
780
- TEST_STORAGE_PATH,
781
- TEST_BOT_NAME,
782
- 'action:defaults'
783
- );
784
-
785
- storage = new JsonFileStorage(
786
- TEST_BOT_NAME,
787
- [actionWithDefaults],
788
- TEST_STORAGE_PATH
789
- );
790
-
791
- const state = storage.getActionState(actionWithDefaults, 12345);
792
- expect(state.customField).toBe('custom-default');
793
- expect(state.pinnedMessages).toEqual([999]);
794
- });
795
-
796
- test('should not use stateConstructor for existing chats', () => {
797
- const actionWithDefaults = createTestAction('action:defaults', {
798
- customField: 'should-not-see-this'
799
- });
800
-
801
- // Pre-populate with existing data
802
- ensureActionFileExists(
803
- TEST_STORAGE_PATH,
804
- TEST_BOT_NAME,
805
- 'action:defaults',
806
- {
807
- 123: {
808
- lastExecutedDate: 5000,
809
- pinnedMessages: [1],
810
- customField: 'existing-value'
811
- }
812
- }
813
- );
814
-
815
- storage = new JsonFileStorage(
816
- TEST_BOT_NAME,
817
- [actionWithDefaults],
818
- TEST_STORAGE_PATH
819
- );
820
-
821
- const state = storage.getActionState(actionWithDefaults, 123);
822
- expect(state.customField).toBe('existing-value');
823
- expect(state.lastExecutedDate).toBe(5000);
824
- });
825
- });
826
-
827
- describe('concurrent action execution (multiple chats)', () => {
828
- // Bot can receive messages from multiple chats simultaneously
829
-
830
- test('should handle concurrent operations on different chats', async () => {
831
- const localStorage = new JsonFileStorage(
832
- TEST_BOT_NAME,
833
- [testAction],
834
- TEST_STORAGE_PATH
835
- );
836
-
837
- const chatIds = [1001, 1002, 1003, 1004, 1005];
838
-
839
- // Simulate concurrent message handling for different chats
840
- const operations = chatIds.map(async (chatId) => {
841
- const state = localStorage.getActionState(testAction, chatId);
842
- state.customField = `processed-${chatId}`;
843
- state.lastExecutedDate = chatId * 1000;
844
- await localStorage.saveActionExecutionResult(
845
- testAction,
846
- chatId,
847
- state
848
- );
849
- });
850
-
851
- await Promise.all(operations);
852
-
853
- // Verify all states were saved correctly
854
- for (const chatId of chatIds) {
855
- const state = localStorage.getActionState(testAction, chatId);
856
- expect(state.customField).toBe(`processed-${chatId}`);
857
- expect(state.lastExecutedDate).toBe(chatId * 1000);
858
- }
859
- });
860
-
861
- test('should handle rapid sequential operations on same chat', async () => {
862
- storage = new JsonFileStorage(
863
- TEST_BOT_NAME,
864
- [testAction],
865
- TEST_STORAGE_PATH
866
- );
867
-
868
- const chatId = 9999;
869
-
870
- // Rapid sequential updates (simulating spam)
871
- for (let i = 1; i <= 10; i++) {
872
- await storage.saveActionExecutionResult(testAction, chatId, {
873
- lastExecutedDate: i * 100,
874
- pinnedMessages: [],
875
- customField: `update-${i}`
876
- });
877
- }
878
-
879
- const finalState = storage.getActionState(testAction, chatId);
880
- expect(finalState.customField).toBe('update-10');
881
- expect(finalState.lastExecutedDate).toBe(1000);
882
- });
883
- });
884
-
885
- describe('data integrity', () => {
886
- test('should not lose data when updating single chat in multi-chat file', async () => {
887
- storage = new JsonFileStorage(
888
- TEST_BOT_NAME,
889
- [testAction],
890
- TEST_STORAGE_PATH
891
- );
892
-
893
- // Create states for 3 chats
894
- await storage.saveActionExecutionResult(testAction, 1, {
895
- lastExecutedDate: 1,
896
- pinnedMessages: [1],
897
- customField: 'one'
898
- });
899
- await storage.saveActionExecutionResult(testAction, 2, {
900
- lastExecutedDate: 2,
901
- pinnedMessages: [2],
902
- customField: 'two'
903
- });
904
- await storage.saveActionExecutionResult(testAction, 3, {
905
- lastExecutedDate: 3,
906
- pinnedMessages: [3],
907
- customField: 'three'
908
- });
909
-
910
- // Update only chat 2
911
- await storage.saveActionExecutionResult(testAction, 2, {
912
- lastExecutedDate: 200,
913
- pinnedMessages: [20],
914
- customField: 'two-updated'
915
- });
916
-
917
- // Verify chat 1 and 3 are unchanged
918
- const state1 = storage.getActionState(testAction, 1);
919
- const state2 = storage.getActionState(testAction, 2);
920
- const state3 = storage.getActionState(testAction, 3);
921
-
922
- expect(state1.customField).toBe('one');
923
- expect(state2.customField).toBe('two-updated');
924
- expect(state3.customField).toBe('three');
925
- });
926
- });
927
- });