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.
- package/dist/dtos/propertyProviderSets.d.ts +0 -1
- package/dist/dtos/propertyProviderSets.d.ts.map +1 -1
- package/dist/entities/actions/commandAction.d.ts +1 -1
- package/dist/entities/actions/commandAction.d.ts.map +1 -1
- package/dist/entities/actions/commandAction.js +2 -2
- package/dist/helpers/builders/commandActionBuilder.d.ts.map +1 -1
- package/dist/helpers/builders/commandActionBuilder.js +2 -3
- package/package.json +4 -1
- package/eslint.config.ts +0 -62
- package/src/builtin/helpAction.ts +0 -17
- package/src/dtos/chatHistoryMessage.ts +0 -22
- package/src/dtos/chatInfo.ts +0 -12
- package/src/dtos/commandTriggerCheckResult.ts +0 -40
- package/src/dtos/cooldownInfo.ts +0 -10
- package/src/dtos/incomingMessage.ts +0 -71
- package/src/dtos/incomingQuery.ts +0 -14
- package/src/dtos/messageInfo.ts +0 -15
- package/src/dtos/propertyProviderSets.ts +0 -21
- package/src/dtos/replyInfo.ts +0 -9
- package/src/dtos/responses/delay.ts +0 -28
- package/src/dtos/responses/imageMessage.ts +0 -41
- package/src/dtos/responses/inlineQueryResponse.ts +0 -26
- package/src/dtos/responses/reaction.ts +0 -30
- package/src/dtos/responses/textMessage.ts +0 -44
- package/src/dtos/responses/unpin.ts +0 -27
- package/src/dtos/responses/videoMessage.ts +0 -41
- package/src/dtos/userInfo.ts +0 -8
- package/src/entities/actions/commandAction.ts +0 -275
- package/src/entities/actions/inlineQueryAction.ts +0 -83
- package/src/entities/actions/replyCaptureAction.ts +0 -110
- package/src/entities/actions/scheduledAction.ts +0 -182
- package/src/entities/botInstance.ts +0 -92
- package/src/entities/cachedStateFactory.ts +0 -14
- package/src/entities/context/baseContext.ts +0 -111
- package/src/entities/context/chatContext.ts +0 -135
- package/src/entities/context/inlineQueryContext.ts +0 -63
- package/src/entities/context/messageContext.ts +0 -250
- package/src/entities/context/replyContext.ts +0 -260
- package/src/entities/states/actionStateBase.ts +0 -6
- package/src/entities/taskRecord.ts +0 -11
- package/src/helpers/builders/commandActionBuilder.ts +0 -214
- package/src/helpers/builders/inlineQueryActionBuilder.ts +0 -71
- package/src/helpers/builders/scheduledActionBuilder.ts +0 -143
- package/src/helpers/mapUtils.ts +0 -28
- package/src/helpers/noop.ts +0 -20
- package/src/helpers/objectFromEntries.ts +0 -7
- package/src/helpers/timeConvertions.ts +0 -13
- package/src/helpers/toArray.ts +0 -3
- package/src/helpers/traceFactory.ts +0 -11
- package/src/index.ts +0 -33
- package/src/main.ts +0 -76
- package/src/services/actionProcessingService.ts +0 -125
- package/src/services/actionProcessors/baseProcessor.ts +0 -67
- package/src/services/actionProcessors/commandActionProcessor.ts +0 -231
- package/src/services/actionProcessors/inlineQueryActionProcessor.ts +0 -165
- package/src/services/actionProcessors/scheduledActionProcessor.ts +0 -136
- package/src/services/jsonFileStorage.ts +0 -181
- package/src/services/nodeTimeoutScheduler.ts +0 -79
- package/src/services/responseProcessingQueue.ts +0 -57
- package/src/services/telegramApi.ts +0 -278
- package/src/types/action.ts +0 -15
- package/src/types/actionState.ts +0 -4
- package/src/types/cachedValueAccessor.ts +0 -1
- package/src/types/capture.ts +0 -33
- package/src/types/commandCondition.ts +0 -9
- package/src/types/commandTrigger.ts +0 -1
- package/src/types/events.ts +0 -286
- package/src/types/externalAliases.ts +0 -18
- package/src/types/handlers.ts +0 -26
- package/src/types/inputFile.ts +0 -4
- package/src/types/messageSendingOptions.ts +0 -10
- package/src/types/messageTypes.ts +0 -21
- package/src/types/propertyProvider.ts +0 -14
- package/src/types/response.ts +0 -51
- package/src/types/scheduler.ts +0 -20
- package/src/types/storage.ts +0 -23
- package/src/types/timeValues.ts +0 -33
- package/src/types/trace.ts +0 -5
- package/tests/dtos/commandTriggerCheckResult.test.ts +0 -301
- package/tests/entities/actions/inlineQueryAction.test.ts +0 -359
- package/tests/entities/actions/replyCaptureAction.test.ts +0 -501
- package/tests/entities/cachedStateFactory.test.ts +0 -98
- package/tests/entities/context/chatContext.test.ts +0 -606
- package/tests/entities/context/messageContext.test.ts +0 -370
- package/tests/entities/states/actionStateBase.test.ts +0 -138
- package/tests/entities/taskRecord.test.ts +0 -195
- package/tests/helpers/mapUtils.test.ts +0 -163
- package/tests/helpers/timeConvertions.test.ts +0 -129
- package/tests/services/actionProcessors/baseActionProcessor.test.ts +0 -359
- package/tests/services/actionProcessors/commandActionProcessor.test.ts +0 -268
- package/tests/services/actionProcessors/inlineQueryActionProcessor.test.ts +0 -616
- package/tests/services/actionProcessors/processorTestHelpers.ts +0 -147
- package/tests/services/actionProcessors/scheduledActionProcessor.test.ts +0 -153
- package/tests/services/jsonFileStorage.test.ts +0 -927
- package/tests/services/nodeTimeoutScheduler.test.ts +0 -421
- package/tests/services/responseProcessingQueue.test.ts +0 -388
- package/tsconfig.build.json +0 -8
- 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
|
-
});
|