@lobehub/lobehub 2.0.0-next.85 → 2.0.0-next.86
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/CHANGELOG.md +25 -0
- package/apps/desktop/src/main/modules/networkProxy/dispatcher.ts +16 -16
- package/apps/desktop/src/main/modules/networkProxy/tester.ts +11 -11
- package/apps/desktop/src/main/modules/networkProxy/urlBuilder.ts +3 -3
- package/apps/desktop/src/main/modules/networkProxy/validator.ts +10 -10
- package/changelog/v1.json +9 -0
- package/package.json +1 -1
- package/packages/agent-runtime/src/core/runtime.ts +36 -1
- package/packages/agent-runtime/src/types/event.ts +1 -0
- package/packages/agent-runtime/src/types/generalAgent.ts +16 -0
- package/packages/agent-runtime/src/types/instruction.ts +30 -0
- package/packages/agent-runtime/src/types/runtime.ts +7 -0
- package/packages/types/src/message/common/metadata.ts +3 -0
- package/packages/types/src/message/common/tools.ts +2 -2
- package/packages/types/src/tool/search/index.ts +8 -2
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/index.tsx +2 -2
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/V1Mobile/useSend.ts +7 -2
- package/src/app/[variants]/(main)/chat/components/conversation/features/ChatInput/useSend.ts +15 -14
- package/src/app/[variants]/(main)/chat/session/features/SessionListContent/List/Item/index.tsx +2 -2
- package/src/features/ChatInput/ActionBar/STT/browser.tsx +2 -2
- package/src/features/ChatInput/ActionBar/STT/openai.tsx +2 -2
- package/src/features/Conversation/Messages/Group/Tool/Inspector/index.tsx +1 -1
- package/src/features/Conversation/Messages/User/index.tsx +3 -3
- package/src/features/Conversation/Messages/index.tsx +3 -3
- package/src/features/Conversation/components/AutoScroll.tsx +2 -2
- package/src/services/search.ts +2 -2
- package/src/store/chat/agents/GeneralChatAgent.ts +98 -0
- package/src/store/chat/agents/__tests__/GeneralChatAgent.test.ts +366 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/call-llm.test.ts +1217 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/call-tool.test.ts +1976 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/finish.test.ts +453 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/index.ts +4 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockInstructions.ts +126 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockMessages.ts +94 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockOperations.ts +96 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/fixtures/mockStore.ts +138 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/assertions.ts +185 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/index.ts +3 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/operationTestUtils.ts +94 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/testExecutor.ts +139 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/request-human-approve.test.ts +545 -0
- package/src/store/chat/agents/__tests__/createAgentExecutors/resolve-aborted-tools.test.ts +686 -0
- package/src/store/chat/agents/createAgentExecutors.ts +313 -80
- package/src/store/chat/selectors.ts +1 -0
- package/src/store/chat/slices/aiChat/__tests__/ai-chat.integration.test.ts +667 -0
- package/src/store/chat/slices/aiChat/actions/__tests__/cancel-functionality.test.ts +137 -27
- package/src/store/chat/slices/aiChat/actions/__tests__/conversationControl.test.ts +163 -125
- package/src/store/chat/slices/aiChat/actions/__tests__/conversationLifecycle.test.ts +12 -2
- package/src/store/chat/slices/aiChat/actions/__tests__/fixtures.ts +0 -2
- package/src/store/chat/slices/aiChat/actions/__tests__/helpers.ts +0 -2
- package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +286 -19
- package/src/store/chat/slices/aiChat/actions/__tests__/streamingStates.test.ts +0 -112
- package/src/store/chat/slices/aiChat/actions/conversationControl.ts +42 -99
- package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +90 -57
- package/src/store/chat/slices/aiChat/actions/generateAIGroupChat.ts +5 -25
- package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +220 -98
- package/src/store/chat/slices/aiChat/actions/streamingStates.ts +0 -34
- package/src/store/chat/slices/aiChat/initialState.ts +0 -28
- package/src/store/chat/slices/aiChat/selectors.test.ts +280 -0
- package/src/store/chat/slices/aiChat/selectors.ts +31 -7
- package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +21 -30
- package/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +29 -49
- package/src/store/chat/slices/builtinTool/actions/interpreter.ts +83 -48
- package/src/store/chat/slices/builtinTool/actions/localSystem.ts +78 -28
- package/src/store/chat/slices/builtinTool/actions/search.ts +146 -59
- package/src/store/chat/slices/builtinTool/selectors.test.ts +258 -0
- package/src/store/chat/slices/builtinTool/selectors.ts +25 -4
- package/src/store/chat/slices/message/action.test.ts +134 -16
- package/src/store/chat/slices/message/actions/internals.ts +33 -7
- package/src/store/chat/slices/message/actions/optimisticUpdate.ts +85 -52
- package/src/store/chat/slices/message/initialState.ts +0 -10
- package/src/store/chat/slices/message/selectors/messageState.ts +34 -12
- package/src/store/chat/slices/operation/__tests__/actions.test.ts +712 -16
- package/src/store/chat/slices/operation/__tests__/integration.test.ts +342 -0
- package/src/store/chat/slices/operation/__tests__/selectors.test.ts +257 -17
- package/src/store/chat/slices/operation/actions.ts +218 -11
- package/src/store/chat/slices/operation/selectors.ts +135 -6
- package/src/store/chat/slices/operation/types.ts +29 -3
- package/src/store/chat/slices/plugin/action.test.ts +30 -322
- package/src/store/chat/slices/plugin/actions/internals.ts +0 -14
- package/src/store/chat/slices/plugin/actions/optimisticUpdate.ts +21 -19
- package/src/store/chat/slices/plugin/actions/pluginTypes.ts +45 -27
- package/src/store/chat/slices/plugin/actions/publicApi.ts +3 -4
- package/src/store/chat/slices/plugin/actions/workflow.ts +0 -55
- package/src/store/chat/slices/thread/selectors/index.ts +4 -2
- package/src/store/chat/slices/translate/action.ts +54 -41
- package/src/tools/web-browsing/ExecutionRuntime/index.ts +5 -2
- package/src/tools/web-browsing/Portal/Search/Footer.tsx +11 -9
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration test for Operation Management System
|
|
3
|
+
* Tests the full lifecycle of operations in realistic scenarios
|
|
4
|
+
*/
|
|
5
|
+
import { act, renderHook } from '@testing-library/react';
|
|
6
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
|
+
|
|
8
|
+
import { operationSelectors } from '@/store/chat/slices/operation/selectors';
|
|
9
|
+
import { useChatStore } from '@/store/chat/store';
|
|
10
|
+
import { messageMapKey } from '@/store/chat/utils/messageMapKey';
|
|
11
|
+
|
|
12
|
+
vi.mock('zustand/traditional');
|
|
13
|
+
|
|
14
|
+
describe('Operation Management Integration Tests', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
act(() => {
|
|
17
|
+
useChatStore.setState({
|
|
18
|
+
activeId: 'test-session',
|
|
19
|
+
activeTopicId: 'test-topic',
|
|
20
|
+
operations: {},
|
|
21
|
+
operationsByType: {} as any,
|
|
22
|
+
operationsByMessage: {},
|
|
23
|
+
operationsByContext: {},
|
|
24
|
+
messageOperationMap: {},
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('Complete Operation Lifecycle', () => {
|
|
30
|
+
it('should handle full AI generation lifecycle', async () => {
|
|
31
|
+
const { result } = renderHook(() => useChatStore());
|
|
32
|
+
const sessionId = 'test-session';
|
|
33
|
+
const topicId = 'test-topic';
|
|
34
|
+
const messageId = 'user-msg-1';
|
|
35
|
+
|
|
36
|
+
// 1. Start operation
|
|
37
|
+
let operationId: string = '';
|
|
38
|
+
act(() => {
|
|
39
|
+
const { operationId: id } = result.current.startOperation({
|
|
40
|
+
type: 'execAgentRuntime',
|
|
41
|
+
context: { sessionId, topicId, messageId },
|
|
42
|
+
label: 'AI Generation',
|
|
43
|
+
});
|
|
44
|
+
operationId = id;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Verify operation created
|
|
48
|
+
expect(result.current.operations[operationId!]).toBeDefined();
|
|
49
|
+
expect(result.current.operations[operationId!].status).toBe('running');
|
|
50
|
+
expect(result.current.operations[operationId!].context).toEqual({
|
|
51
|
+
sessionId,
|
|
52
|
+
topicId,
|
|
53
|
+
messageId,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// 2. Associate message
|
|
57
|
+
act(() => {
|
|
58
|
+
result.current.associateMessageWithOperation(messageId, operationId!);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(result.current.messageOperationMap[messageId]).toBe(operationId);
|
|
62
|
+
|
|
63
|
+
// 3. Check status during execution
|
|
64
|
+
expect(operationSelectors.hasAnyRunningOperation(result.current)).toBe(true);
|
|
65
|
+
expect(operationSelectors.canSendMessage(result.current)).toBe(false);
|
|
66
|
+
expect(operationSelectors.isAgentRuntimeRunning(result.current)).toBe(true);
|
|
67
|
+
|
|
68
|
+
// 4. Complete operation
|
|
69
|
+
act(() => {
|
|
70
|
+
result.current.completeOperation(operationId!);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(result.current.operations[operationId!].status).toBe('completed');
|
|
74
|
+
expect(operationSelectors.hasAnyRunningOperation(result.current)).toBe(false);
|
|
75
|
+
expect(operationSelectors.canSendMessage(result.current)).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should handle operation cancellation', async () => {
|
|
79
|
+
const { result } = renderHook(() => useChatStore());
|
|
80
|
+
|
|
81
|
+
let operationId: string = '';
|
|
82
|
+
let abortController: AbortController | undefined;
|
|
83
|
+
|
|
84
|
+
act(() => {
|
|
85
|
+
const res = result.current.startOperation({
|
|
86
|
+
type: 'execAgentRuntime',
|
|
87
|
+
context: { sessionId: 'test-session' },
|
|
88
|
+
});
|
|
89
|
+
operationId = res.operationId;
|
|
90
|
+
abortController = res.abortController;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const abortSpy = vi.spyOn(abortController!, 'abort');
|
|
94
|
+
|
|
95
|
+
// Cancel operation
|
|
96
|
+
act(() => {
|
|
97
|
+
result.current.cancelOperation(operationId!, 'User cancelled');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(result.current.operations[operationId!].status).toBe('cancelled');
|
|
101
|
+
expect(abortSpy).toHaveBeenCalled();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('Parent-Child Operation Relationship', () => {
|
|
106
|
+
it('should handle nested operations with context inheritance', async () => {
|
|
107
|
+
const { result } = renderHook(() => useChatStore());
|
|
108
|
+
|
|
109
|
+
// Create parent operation
|
|
110
|
+
let parentOpId: string = '';
|
|
111
|
+
act(() => {
|
|
112
|
+
const { operationId } = result.current.startOperation({
|
|
113
|
+
type: 'execAgentRuntime',
|
|
114
|
+
context: { sessionId: 'test-session', topicId: 'test-topic' },
|
|
115
|
+
});
|
|
116
|
+
parentOpId = operationId;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Create child operation (should inherit context)
|
|
120
|
+
let childOpId: string = '';
|
|
121
|
+
act(() => {
|
|
122
|
+
const { operationId } = result.current.startOperation({
|
|
123
|
+
type: 'toolCalling',
|
|
124
|
+
parentOperationId: parentOpId!,
|
|
125
|
+
});
|
|
126
|
+
childOpId = operationId;
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Verify context inheritance
|
|
130
|
+
const parentOp = result.current.operations[parentOpId!];
|
|
131
|
+
const childOp = result.current.operations[childOpId!];
|
|
132
|
+
|
|
133
|
+
expect(childOp.context.sessionId).toBe(parentOp.context.sessionId);
|
|
134
|
+
expect(childOp.context.topicId).toBe(parentOp.context.topicId);
|
|
135
|
+
expect(childOp.parentOperationId).toBe(parentOpId);
|
|
136
|
+
expect(parentOp.childOperationIds).toContain(childOpId);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should cascade cancel child operations', async () => {
|
|
140
|
+
const { result } = renderHook(() => useChatStore());
|
|
141
|
+
|
|
142
|
+
// Create parent and child operations
|
|
143
|
+
let parentOpId: string = '';
|
|
144
|
+
let childOpId1: string = '';
|
|
145
|
+
let childOpId2: string = '';
|
|
146
|
+
|
|
147
|
+
act(() => {
|
|
148
|
+
const { operationId } = result.current.startOperation({
|
|
149
|
+
type: 'execAgentRuntime',
|
|
150
|
+
context: { sessionId: 'test-session' },
|
|
151
|
+
});
|
|
152
|
+
parentOpId = operationId;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
act(() => {
|
|
156
|
+
const { operationId } = result.current.startOperation({
|
|
157
|
+
type: 'toolCalling',
|
|
158
|
+
parentOperationId: parentOpId!,
|
|
159
|
+
});
|
|
160
|
+
childOpId1 = operationId;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
act(() => {
|
|
164
|
+
const { operationId } = result.current.startOperation({
|
|
165
|
+
type: 'toolCalling',
|
|
166
|
+
parentOperationId: parentOpId!,
|
|
167
|
+
});
|
|
168
|
+
childOpId2 = operationId;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Cancel parent
|
|
172
|
+
act(() => {
|
|
173
|
+
result.current.cancelOperation(parentOpId!, 'Parent cancelled');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Verify all cancelled
|
|
177
|
+
expect(result.current.operations[parentOpId!].status).toBe('cancelled');
|
|
178
|
+
expect(result.current.operations[childOpId1!].status).toBe('cancelled');
|
|
179
|
+
expect(result.current.operations[childOpId2!].status).toBe('cancelled');
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('Context Isolation', () => {
|
|
184
|
+
it('should maintain correct context when switching topics', async () => {
|
|
185
|
+
const { result } = renderHook(() => useChatStore());
|
|
186
|
+
|
|
187
|
+
// Create operation in topic A
|
|
188
|
+
let opIdTopicA: string = '';
|
|
189
|
+
act(() => {
|
|
190
|
+
const { operationId } = result.current.startOperation({
|
|
191
|
+
type: 'execAgentRuntime',
|
|
192
|
+
context: { sessionId: 'session-1', topicId: 'topic-a' },
|
|
193
|
+
});
|
|
194
|
+
opIdTopicA = operationId;
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Switch to topic B
|
|
198
|
+
act(() => {
|
|
199
|
+
useChatStore.setState({ activeTopicId: 'topic-b' });
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Create operation in topic B
|
|
203
|
+
let opIdTopicB: string = '';
|
|
204
|
+
act(() => {
|
|
205
|
+
const { operationId } = result.current.startOperation({
|
|
206
|
+
type: 'execAgentRuntime',
|
|
207
|
+
context: { sessionId: 'session-1', topicId: 'topic-b' },
|
|
208
|
+
});
|
|
209
|
+
opIdTopicB = operationId;
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Verify context isolation
|
|
213
|
+
const opA = result.current.operations[opIdTopicA!];
|
|
214
|
+
const opB = result.current.operations[opIdTopicB!];
|
|
215
|
+
|
|
216
|
+
expect(opA.context.topicId).toBe('topic-a');
|
|
217
|
+
expect(opB.context.topicId).toBe('topic-b');
|
|
218
|
+
|
|
219
|
+
// Get operations by context
|
|
220
|
+
const contextA = messageMapKey('session-1', 'topic-a');
|
|
221
|
+
const contextB = messageMapKey('session-1', 'topic-b');
|
|
222
|
+
|
|
223
|
+
const opsInA = result.current.operationsByContext[contextA] || [];
|
|
224
|
+
const opsInB = result.current.operationsByContext[contextB] || [];
|
|
225
|
+
|
|
226
|
+
expect(opsInA).toContain(opIdTopicA);
|
|
227
|
+
expect(opsInB).toContain(opIdTopicB);
|
|
228
|
+
expect(opsInA).not.toContain(opIdTopicB);
|
|
229
|
+
expect(opsInB).not.toContain(opIdTopicA);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe('Batch Operations', () => {
|
|
234
|
+
it('should handle multiple concurrent operations', async () => {
|
|
235
|
+
const { result } = renderHook(() => useChatStore());
|
|
236
|
+
|
|
237
|
+
const opIds: string[] = [];
|
|
238
|
+
|
|
239
|
+
// Start 5 operations concurrently
|
|
240
|
+
act(() => {
|
|
241
|
+
for (let i = 0; i < 5; i++) {
|
|
242
|
+
const { operationId } = result.current.startOperation({
|
|
243
|
+
type: i % 2 === 0 ? 'execAgentRuntime' : 'toolCalling',
|
|
244
|
+
context: { sessionId: 'session-1', messageId: `msg-${i}` },
|
|
245
|
+
});
|
|
246
|
+
opIds.push(operationId);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Verify all running
|
|
251
|
+
expect(operationSelectors.getRunningOperations(result.current)).toHaveLength(5);
|
|
252
|
+
|
|
253
|
+
// Cancel all generateAI operations
|
|
254
|
+
let cancelledIds: string[];
|
|
255
|
+
act(() => {
|
|
256
|
+
cancelledIds = result.current.cancelOperations({
|
|
257
|
+
type: 'execAgentRuntime',
|
|
258
|
+
status: 'running',
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Verify correct operations cancelled
|
|
263
|
+
expect(cancelledIds!).toHaveLength(3); // 0, 2, 4
|
|
264
|
+
expect(operationSelectors.getRunningOperations(result.current)).toHaveLength(2);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
describe('Memory Management', () => {
|
|
269
|
+
it('should cleanup old completed operations', async () => {
|
|
270
|
+
const { result } = renderHook(() => useChatStore());
|
|
271
|
+
|
|
272
|
+
const oldTimestamp = Date.now() - 2 * 60 * 1000; // 2 minutes ago
|
|
273
|
+
const recentTimestamp = Date.now() - 30 * 1000; // 30 seconds ago
|
|
274
|
+
|
|
275
|
+
// Create old completed operation
|
|
276
|
+
let oldOpId: string;
|
|
277
|
+
act(() => {
|
|
278
|
+
const { operationId } = result.current.startOperation({
|
|
279
|
+
type: 'execAgentRuntime',
|
|
280
|
+
context: { sessionId: 'session-1' },
|
|
281
|
+
});
|
|
282
|
+
oldOpId = operationId;
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Complete and manually set old timestamp
|
|
286
|
+
act(() => {
|
|
287
|
+
result.current.completeOperation(oldOpId!);
|
|
288
|
+
useChatStore.setState((state) => ({
|
|
289
|
+
operations: {
|
|
290
|
+
...state.operations,
|
|
291
|
+
[oldOpId!]: {
|
|
292
|
+
...state.operations[oldOpId!],
|
|
293
|
+
status: 'completed' as const,
|
|
294
|
+
metadata: {
|
|
295
|
+
...state.operations[oldOpId!].metadata,
|
|
296
|
+
startTime: oldTimestamp,
|
|
297
|
+
endTime: oldTimestamp + 1000,
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
}));
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Create recent completed operation
|
|
305
|
+
let recentOpId: string;
|
|
306
|
+
act(() => {
|
|
307
|
+
const { operationId } = result.current.startOperation({
|
|
308
|
+
type: 'execAgentRuntime',
|
|
309
|
+
context: { sessionId: 'session-1' },
|
|
310
|
+
});
|
|
311
|
+
recentOpId = operationId;
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
act(() => {
|
|
315
|
+
result.current.completeOperation(recentOpId!);
|
|
316
|
+
useChatStore.setState((state) => ({
|
|
317
|
+
operations: {
|
|
318
|
+
...state.operations,
|
|
319
|
+
[recentOpId!]: {
|
|
320
|
+
...state.operations[recentOpId!],
|
|
321
|
+
status: 'completed' as const,
|
|
322
|
+
metadata: {
|
|
323
|
+
...state.operations[recentOpId!].metadata,
|
|
324
|
+
startTime: recentTimestamp,
|
|
325
|
+
endTime: recentTimestamp + 1000,
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
}));
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Cleanup operations older than 1 minute
|
|
333
|
+
act(() => {
|
|
334
|
+
result.current.cleanupCompletedOperations(60 * 1000);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// Verify old operation cleaned up, recent kept
|
|
338
|
+
expect(result.current.operations[oldOpId!]).toBeUndefined();
|
|
339
|
+
expect(result.current.operations[recentOpId!]).toBeDefined();
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
});
|