@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
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { act, renderHook } from '@testing-library/react';
|
|
2
|
-
import { describe, expect, it
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
3
|
|
|
4
4
|
import { useChatStore } from '../../../../store';
|
|
5
|
+
import { messageMapKey } from '../../../../utils/messageMapKey';
|
|
5
6
|
|
|
6
7
|
describe('Cancel send message functionality tests', () => {
|
|
7
8
|
describe('cancelSendMessageInServer', () => {
|
|
@@ -13,7 +14,8 @@ describe('Cancel send message functionality tests', () => {
|
|
|
13
14
|
useChatStore.setState({
|
|
14
15
|
activeId: 'session-1',
|
|
15
16
|
activeTopicId: 'topic-1',
|
|
16
|
-
|
|
17
|
+
operations: {},
|
|
18
|
+
operationsByContext: {},
|
|
17
19
|
});
|
|
18
20
|
});
|
|
19
21
|
|
|
@@ -28,13 +30,91 @@ describe('Cancel send message functionality tests', () => {
|
|
|
28
30
|
}).not.toThrow();
|
|
29
31
|
});
|
|
30
32
|
|
|
33
|
+
it('should cancel running sendMessage operations', () => {
|
|
34
|
+
const { result } = renderHook(() => useChatStore());
|
|
35
|
+
|
|
36
|
+
const sessionId = 'session-1';
|
|
37
|
+
const topicId = 'topic-1';
|
|
38
|
+
|
|
39
|
+
act(() => {
|
|
40
|
+
useChatStore.setState({
|
|
41
|
+
activeId: sessionId,
|
|
42
|
+
activeTopicId: topicId,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Start a sendMessage operation
|
|
47
|
+
let operationId: string;
|
|
48
|
+
act(() => {
|
|
49
|
+
const res = result.current.startOperation({
|
|
50
|
+
type: 'sendMessage',
|
|
51
|
+
context: { sessionId, topicId },
|
|
52
|
+
});
|
|
53
|
+
operationId = res.operationId;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(result.current.operations[operationId!].status).toBe('running');
|
|
57
|
+
|
|
58
|
+
// Cancel the operation
|
|
59
|
+
act(() => {
|
|
60
|
+
result.current.cancelSendMessageInServer();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(result.current.operations[operationId!].status).toBe('cancelled');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should restore editor state when cancelling', () => {
|
|
67
|
+
const { result } = renderHook(() => useChatStore());
|
|
68
|
+
|
|
69
|
+
const sessionId = 'session-1';
|
|
70
|
+
const topicId = 'topic-1';
|
|
71
|
+
const mockEditorState = { content: 'test message' };
|
|
72
|
+
|
|
73
|
+
// Mock editor
|
|
74
|
+
const mockEditor = {
|
|
75
|
+
setJSONState: vi.fn(),
|
|
76
|
+
getJSONState: vi.fn().mockReturnValue(mockEditorState),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
act(() => {
|
|
80
|
+
useChatStore.setState({
|
|
81
|
+
activeId: sessionId,
|
|
82
|
+
activeTopicId: topicId,
|
|
83
|
+
mainInputEditor: mockEditor as any,
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Create operation with editor state
|
|
88
|
+
let operationId: string;
|
|
89
|
+
act(() => {
|
|
90
|
+
const res = result.current.startOperation({
|
|
91
|
+
type: 'sendMessage',
|
|
92
|
+
context: { sessionId, topicId },
|
|
93
|
+
});
|
|
94
|
+
operationId = res.operationId;
|
|
95
|
+
|
|
96
|
+
result.current.updateOperationMetadata(res.operationId, {
|
|
97
|
+
inputEditorTempState: mockEditorState,
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Cancel
|
|
102
|
+
act(() => {
|
|
103
|
+
result.current.cancelSendMessageInServer();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Verify editor state was restored
|
|
107
|
+
expect(mockEditor.setJSONState).toHaveBeenCalledWith(mockEditorState);
|
|
108
|
+
});
|
|
109
|
+
|
|
31
110
|
it('should be able to call with specified topic ID', () => {
|
|
32
111
|
const { result } = renderHook(() => useChatStore());
|
|
33
112
|
|
|
34
113
|
act(() => {
|
|
35
114
|
useChatStore.setState({
|
|
36
115
|
activeId: 'session-1',
|
|
37
|
-
|
|
116
|
+
operations: {},
|
|
117
|
+
operationsByContext: {},
|
|
38
118
|
});
|
|
39
119
|
});
|
|
40
120
|
|
|
@@ -54,7 +134,8 @@ describe('Cancel send message functionality tests', () => {
|
|
|
54
134
|
useChatStore.setState({
|
|
55
135
|
activeId: 'session-1',
|
|
56
136
|
activeTopicId: 'topic-1',
|
|
57
|
-
|
|
137
|
+
operations: {},
|
|
138
|
+
operationsByContext: {},
|
|
58
139
|
});
|
|
59
140
|
});
|
|
60
141
|
|
|
@@ -66,42 +147,71 @@ describe('Cancel send message functionality tests', () => {
|
|
|
66
147
|
});
|
|
67
148
|
}).not.toThrow();
|
|
68
149
|
});
|
|
69
|
-
});
|
|
70
150
|
|
|
71
|
-
|
|
72
|
-
it('should have internal state management methods', () => {
|
|
151
|
+
it('should clear error messages from sendMessage operations', () => {
|
|
73
152
|
const { result } = renderHook(() => useChatStore());
|
|
74
153
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('internal_toggleSendMessageOperation should work normally', () => {
|
|
80
|
-
const { result } = renderHook(() => useChatStore());
|
|
154
|
+
const sessionId = 'session-1';
|
|
155
|
+
const topicId = 'topic-1';
|
|
81
156
|
|
|
82
157
|
act(() => {
|
|
83
|
-
useChatStore.setState({
|
|
158
|
+
useChatStore.setState({
|
|
159
|
+
activeId: sessionId,
|
|
160
|
+
activeTopicId: topicId,
|
|
161
|
+
});
|
|
84
162
|
});
|
|
85
163
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
expect(abortController).toBeInstanceOf(AbortController);
|
|
164
|
+
// Create operation with error
|
|
165
|
+
let operationId: string;
|
|
166
|
+
act(() => {
|
|
167
|
+
const res = result.current.startOperation({
|
|
168
|
+
type: 'sendMessage',
|
|
169
|
+
context: { sessionId, topicId },
|
|
93
170
|
});
|
|
94
|
-
|
|
171
|
+
operationId = res.operationId;
|
|
172
|
+
|
|
173
|
+
result.current.updateOperationMetadata(res.operationId, {
|
|
174
|
+
inputSendErrorMsg: 'Test error',
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
expect(result.current.operations[operationId!].metadata.inputSendErrorMsg).toBe('Test error');
|
|
179
|
+
|
|
180
|
+
// Clear error
|
|
181
|
+
act(() => {
|
|
182
|
+
result.current.clearSendMessageError();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
expect(result.current.operations[operationId!].metadata.inputSendErrorMsg).toBeUndefined();
|
|
95
186
|
});
|
|
96
187
|
});
|
|
97
188
|
|
|
98
|
-
describe('
|
|
99
|
-
it('should have
|
|
189
|
+
describe('Operation system', () => {
|
|
190
|
+
it('should have operation management methods', () => {
|
|
191
|
+
const { result } = renderHook(() => useChatStore());
|
|
192
|
+
|
|
193
|
+
expect(typeof result.current.startOperation).toBe('function');
|
|
194
|
+
expect(typeof result.current.cancelOperation).toBe('function');
|
|
195
|
+
expect(typeof result.current.updateOperationMetadata).toBe('function');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should track operations by context', () => {
|
|
100
199
|
const { result } = renderHook(() => useChatStore());
|
|
101
200
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
201
|
+
const sessionId = 'session-1';
|
|
202
|
+
const topicId = 'topic-1';
|
|
203
|
+
|
|
204
|
+
let operationId: string;
|
|
205
|
+
act(() => {
|
|
206
|
+
const res = result.current.startOperation({
|
|
207
|
+
type: 'sendMessage',
|
|
208
|
+
context: { sessionId, topicId },
|
|
209
|
+
});
|
|
210
|
+
operationId = res.operationId;
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const contextKey = messageMapKey(sessionId, topicId);
|
|
214
|
+
expect(result.current.operationsByContext[contextKey]).toContain(operationId!);
|
|
105
215
|
});
|
|
106
216
|
});
|
|
107
217
|
});
|
|
@@ -19,103 +19,156 @@ afterEach(() => {
|
|
|
19
19
|
|
|
20
20
|
describe('ConversationControl actions', () => {
|
|
21
21
|
describe('stopGenerateMessage', () => {
|
|
22
|
-
it('should
|
|
23
|
-
const
|
|
22
|
+
it('should cancel running generateAI operations in current context', () => {
|
|
23
|
+
const { result } = renderHook(() => useChatStore());
|
|
24
24
|
|
|
25
25
|
act(() => {
|
|
26
|
-
useChatStore.setState({
|
|
26
|
+
useChatStore.setState({
|
|
27
|
+
activeId: TEST_IDS.SESSION_ID,
|
|
28
|
+
activeTopicId: TEST_IDS.TOPIC_ID,
|
|
29
|
+
});
|
|
27
30
|
});
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
// Create a generateAI operation
|
|
33
|
+
let operationId: string;
|
|
34
|
+
act(() => {
|
|
35
|
+
const res = result.current.startOperation({
|
|
36
|
+
type: 'execAgentRuntime',
|
|
37
|
+
context: {
|
|
38
|
+
sessionId: TEST_IDS.SESSION_ID,
|
|
39
|
+
topicId: TEST_IDS.TOPIC_ID,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
operationId = res.operationId;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(result.current.operations[operationId!].status).toBe('running');
|
|
31
46
|
|
|
47
|
+
// Stop generation
|
|
32
48
|
act(() => {
|
|
33
49
|
result.current.stopGenerateMessage();
|
|
34
50
|
});
|
|
35
51
|
|
|
36
|
-
expect(
|
|
37
|
-
expect(toggleLoadingSpy).toHaveBeenCalledWith(false, undefined, expect.any(String));
|
|
52
|
+
expect(result.current.operations[operationId!].status).toBe('cancelled');
|
|
38
53
|
});
|
|
39
54
|
|
|
40
|
-
it('should
|
|
55
|
+
it('should not cancel operations from different context', () => {
|
|
56
|
+
const { result } = renderHook(() => useChatStore());
|
|
57
|
+
|
|
41
58
|
act(() => {
|
|
42
|
-
useChatStore.setState({
|
|
59
|
+
useChatStore.setState({
|
|
60
|
+
activeId: TEST_IDS.SESSION_ID,
|
|
61
|
+
activeTopicId: TEST_IDS.TOPIC_ID,
|
|
62
|
+
});
|
|
43
63
|
});
|
|
44
64
|
|
|
45
|
-
|
|
46
|
-
|
|
65
|
+
// Create a generateAI operation in a different context
|
|
66
|
+
let operationId: string;
|
|
67
|
+
act(() => {
|
|
68
|
+
const res = result.current.startOperation({
|
|
69
|
+
type: 'execAgentRuntime',
|
|
70
|
+
context: {
|
|
71
|
+
sessionId: 'different-session',
|
|
72
|
+
topicId: 'different-topic',
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
operationId = res.operationId;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(result.current.operations[operationId!].status).toBe('running');
|
|
47
79
|
|
|
80
|
+
// Stop generation - should not affect different context
|
|
48
81
|
act(() => {
|
|
49
82
|
result.current.stopGenerateMessage();
|
|
50
83
|
});
|
|
51
84
|
|
|
52
|
-
expect(
|
|
85
|
+
expect(result.current.operations[operationId!].status).toBe('running');
|
|
53
86
|
});
|
|
54
87
|
});
|
|
55
88
|
|
|
56
89
|
describe('cancelSendMessageInServer', () => {
|
|
57
|
-
it('should
|
|
90
|
+
it('should cancel operation and restore editor state', () => {
|
|
58
91
|
const { result } = renderHook(() => useChatStore());
|
|
59
|
-
const mockAbort = vi.fn();
|
|
60
92
|
const mockSetJSONState = vi.fn();
|
|
93
|
+
const editorState = { content: 'saved content' };
|
|
61
94
|
|
|
62
95
|
act(() => {
|
|
63
96
|
useChatStore.setState({
|
|
64
97
|
activeId: TEST_IDS.SESSION_ID,
|
|
65
98
|
activeTopicId: TEST_IDS.TOPIC_ID,
|
|
66
|
-
mainSendMessageOperations: {
|
|
67
|
-
[messageMapKey(TEST_IDS.SESSION_ID, TEST_IDS.TOPIC_ID)]: {
|
|
68
|
-
isLoading: true,
|
|
69
|
-
abortController: { abort: mockAbort, signal: {} as any },
|
|
70
|
-
inputEditorTempState: { content: 'saved content' },
|
|
71
|
-
},
|
|
72
|
-
},
|
|
73
99
|
mainInputEditor: { setJSONState: mockSetJSONState } as any,
|
|
74
100
|
});
|
|
75
101
|
});
|
|
76
102
|
|
|
103
|
+
// Create operation
|
|
104
|
+
let operationId: string;
|
|
105
|
+
act(() => {
|
|
106
|
+
const res = result.current.startOperation({
|
|
107
|
+
type: 'sendMessage',
|
|
108
|
+
context: {
|
|
109
|
+
sessionId: TEST_IDS.SESSION_ID,
|
|
110
|
+
topicId: TEST_IDS.TOPIC_ID,
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
operationId = res.operationId;
|
|
114
|
+
|
|
115
|
+
result.current.updateOperationMetadata(res.operationId, {
|
|
116
|
+
inputEditorTempState: editorState,
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(result.current.operations[operationId!].status).toBe('running');
|
|
121
|
+
|
|
122
|
+
// Cancel
|
|
77
123
|
act(() => {
|
|
78
124
|
result.current.cancelSendMessageInServer();
|
|
79
125
|
});
|
|
80
126
|
|
|
81
|
-
expect(
|
|
82
|
-
expect(
|
|
83
|
-
result.current.mainSendMessageOperations[
|
|
84
|
-
messageMapKey(TEST_IDS.SESSION_ID, TEST_IDS.TOPIC_ID)
|
|
85
|
-
]?.isLoading,
|
|
86
|
-
).toBe(false);
|
|
87
|
-
expect(mockSetJSONState).toHaveBeenCalledWith({ content: 'saved content' });
|
|
127
|
+
expect(result.current.operations[operationId!].status).toBe('cancelled');
|
|
128
|
+
expect(mockSetJSONState).toHaveBeenCalledWith(editorState);
|
|
88
129
|
});
|
|
89
130
|
|
|
90
131
|
it('should cancel operation for specified topic ID', () => {
|
|
91
132
|
const { result } = renderHook(() => useChatStore());
|
|
92
|
-
const mockAbort = vi.fn();
|
|
93
133
|
const customTopicId = 'custom-topic-id';
|
|
94
134
|
|
|
95
135
|
act(() => {
|
|
96
136
|
useChatStore.setState({
|
|
97
137
|
activeId: TEST_IDS.SESSION_ID,
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Create operation
|
|
142
|
+
let operationId: string;
|
|
143
|
+
act(() => {
|
|
144
|
+
const res = result.current.startOperation({
|
|
145
|
+
type: 'sendMessage',
|
|
146
|
+
context: {
|
|
147
|
+
sessionId: TEST_IDS.SESSION_ID,
|
|
148
|
+
topicId: customTopicId,
|
|
103
149
|
},
|
|
104
150
|
});
|
|
151
|
+
operationId = res.operationId;
|
|
105
152
|
});
|
|
106
153
|
|
|
154
|
+
expect(result.current.operations[operationId!].status).toBe('running');
|
|
155
|
+
|
|
156
|
+
// Cancel
|
|
107
157
|
act(() => {
|
|
108
158
|
result.current.cancelSendMessageInServer(customTopicId);
|
|
109
159
|
});
|
|
110
160
|
|
|
111
|
-
expect(
|
|
161
|
+
expect(result.current.operations[operationId!].status).toBe('cancelled');
|
|
112
162
|
});
|
|
113
163
|
|
|
114
164
|
it('should handle gracefully when operation does not exist', () => {
|
|
115
165
|
const { result } = renderHook(() => useChatStore());
|
|
116
166
|
|
|
117
167
|
act(() => {
|
|
118
|
-
useChatStore.setState({
|
|
168
|
+
useChatStore.setState({
|
|
169
|
+
operations: {},
|
|
170
|
+
operationsByContext: {},
|
|
171
|
+
});
|
|
119
172
|
});
|
|
120
173
|
|
|
121
174
|
expect(() => {
|
|
@@ -134,31 +187,44 @@ describe('ConversationControl actions', () => {
|
|
|
134
187
|
useChatStore.setState({
|
|
135
188
|
activeId: TEST_IDS.SESSION_ID,
|
|
136
189
|
activeTopicId: TEST_IDS.TOPIC_ID,
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Create operation with error
|
|
194
|
+
let operationId: string;
|
|
195
|
+
act(() => {
|
|
196
|
+
const res = result.current.startOperation({
|
|
197
|
+
type: 'sendMessage',
|
|
198
|
+
context: {
|
|
199
|
+
sessionId: TEST_IDS.SESSION_ID,
|
|
200
|
+
topicId: TEST_IDS.TOPIC_ID,
|
|
142
201
|
},
|
|
143
202
|
});
|
|
203
|
+
operationId = res.operationId;
|
|
204
|
+
|
|
205
|
+
result.current.updateOperationMetadata(res.operationId, {
|
|
206
|
+
inputSendErrorMsg: 'Some error',
|
|
207
|
+
});
|
|
144
208
|
});
|
|
145
209
|
|
|
210
|
+
expect(result.current.operations[operationId!].metadata.inputSendErrorMsg).toBe('Some error');
|
|
211
|
+
|
|
212
|
+
// Clear error
|
|
146
213
|
act(() => {
|
|
147
214
|
result.current.clearSendMessageError();
|
|
148
215
|
});
|
|
149
216
|
|
|
150
|
-
expect(
|
|
151
|
-
result.current.mainSendMessageOperations[
|
|
152
|
-
messageMapKey(TEST_IDS.SESSION_ID, TEST_IDS.TOPIC_ID)
|
|
153
|
-
],
|
|
154
|
-
).toBeUndefined();
|
|
217
|
+
expect(result.current.operations[operationId!].metadata.inputSendErrorMsg).toBeUndefined();
|
|
155
218
|
});
|
|
156
219
|
|
|
157
220
|
it('should handle gracefully when no error operation exists', () => {
|
|
158
221
|
const { result } = renderHook(() => useChatStore());
|
|
159
222
|
|
|
160
223
|
act(() => {
|
|
161
|
-
useChatStore.setState({
|
|
224
|
+
useChatStore.setState({
|
|
225
|
+
operations: {},
|
|
226
|
+
operationsByContext: {},
|
|
227
|
+
});
|
|
162
228
|
});
|
|
163
229
|
|
|
164
230
|
expect(() => {
|
|
@@ -169,112 +235,80 @@ describe('ConversationControl actions', () => {
|
|
|
169
235
|
});
|
|
170
236
|
});
|
|
171
237
|
|
|
172
|
-
describe('
|
|
173
|
-
it('should create
|
|
238
|
+
describe('Operation system integration', () => {
|
|
239
|
+
it('should create operation with abort controller', () => {
|
|
174
240
|
const { result } = renderHook(() => useChatStore());
|
|
241
|
+
|
|
242
|
+
let operationId: string = '';
|
|
175
243
|
let abortController: AbortController | undefined;
|
|
176
244
|
|
|
177
245
|
act(() => {
|
|
178
|
-
|
|
246
|
+
const res = result.current.startOperation({
|
|
247
|
+
type: 'sendMessage',
|
|
248
|
+
context: { sessionId: 'test-session' },
|
|
249
|
+
});
|
|
250
|
+
operationId = res.operationId;
|
|
251
|
+
abortController = res.abortController;
|
|
179
252
|
});
|
|
180
253
|
|
|
181
254
|
expect(abortController!).toBeInstanceOf(AbortController);
|
|
182
|
-
expect(result.current.
|
|
183
|
-
expect(result.current.
|
|
184
|
-
abortController,
|
|
185
|
-
);
|
|
255
|
+
expect(result.current.operations[operationId!].abortController).toBe(abortController);
|
|
256
|
+
expect(result.current.operations[operationId!].status).toBe('running');
|
|
186
257
|
});
|
|
187
258
|
|
|
188
|
-
it('should
|
|
259
|
+
it('should update operation metadata', () => {
|
|
189
260
|
const { result } = renderHook(() => useChatStore());
|
|
190
|
-
const mockAbortController = { abort: vi.fn() } as any;
|
|
191
261
|
|
|
192
|
-
let
|
|
193
|
-
act(() => {
|
|
194
|
-
result.current.internal_updateSendMessageOperation('test-key', {
|
|
195
|
-
isLoading: true,
|
|
196
|
-
abortController: mockAbortController,
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
abortController = result.current.internal_toggleSendMessageOperation('test-key', false);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
expect(abortController).toBeUndefined();
|
|
203
|
-
expect(result.current.mainSendMessageOperations['test-key']?.isLoading).toBe(false);
|
|
204
|
-
expect(result.current.mainSendMessageOperations['test-key']?.abortController).toBeNull();
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it('should call abort with cancel reason when stopping', () => {
|
|
208
|
-
const { result } = renderHook(() => useChatStore());
|
|
209
|
-
const mockAbortController = { abort: vi.fn() } as any;
|
|
262
|
+
let operationId: string;
|
|
210
263
|
|
|
211
264
|
act(() => {
|
|
212
|
-
result.current.
|
|
213
|
-
|
|
214
|
-
|
|
265
|
+
const res = result.current.startOperation({
|
|
266
|
+
type: 'sendMessage',
|
|
267
|
+
context: { sessionId: 'test-session' },
|
|
215
268
|
});
|
|
269
|
+
operationId = res.operationId;
|
|
216
270
|
|
|
217
|
-
result.current.
|
|
271
|
+
result.current.updateOperationMetadata(res.operationId, {
|
|
272
|
+
inputSendErrorMsg: 'test error',
|
|
273
|
+
inputEditorTempState: { content: 'test' },
|
|
274
|
+
});
|
|
218
275
|
});
|
|
219
276
|
|
|
220
|
-
expect(
|
|
277
|
+
expect(result.current.operations[operationId!].metadata.inputSendErrorMsg).toBe('test error');
|
|
278
|
+
expect(result.current.operations[operationId!].metadata.inputEditorTempState).toEqual({
|
|
279
|
+
content: 'test',
|
|
280
|
+
});
|
|
221
281
|
});
|
|
222
282
|
|
|
223
283
|
it('should support multiple parallel operations', () => {
|
|
224
284
|
const { result } = renderHook(() => useChatStore());
|
|
225
285
|
|
|
226
|
-
let
|
|
227
|
-
|
|
228
|
-
abortController1 = result.current.internal_toggleSendMessageOperation('key1', true);
|
|
229
|
-
abortController2 = result.current.internal_toggleSendMessageOperation('key2', true);
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
expect(result.current.mainSendMessageOperations['key1']?.isLoading).toBe(true);
|
|
233
|
-
expect(result.current.mainSendMessageOperations['key2']?.isLoading).toBe(true);
|
|
234
|
-
expect(abortController1).not.toBe(abortController2);
|
|
235
|
-
});
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
describe('internal_updateSendMessageOperation', () => {
|
|
239
|
-
it('should update operation state', () => {
|
|
240
|
-
const { result } = renderHook(() => useChatStore());
|
|
241
|
-
const mockAbortController = new AbortController();
|
|
286
|
+
let opId1: string = '';
|
|
287
|
+
let opId2: string = '';
|
|
242
288
|
|
|
243
289
|
act(() => {
|
|
244
|
-
result.current.
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
290
|
+
const res1 = result.current.startOperation({
|
|
291
|
+
type: 'sendMessage',
|
|
292
|
+
context: { sessionId: 'session-1', topicId: 'topic-1' },
|
|
293
|
+
});
|
|
294
|
+
const res2 = result.current.startOperation({
|
|
295
|
+
type: 'sendMessage',
|
|
296
|
+
context: { sessionId: 'session-1', topicId: 'topic-2' },
|
|
248
297
|
});
|
|
249
|
-
});
|
|
250
298
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
abortController: mockAbortController,
|
|
254
|
-
inputSendErrorMsg: 'test error',
|
|
299
|
+
opId1 = res1.operationId;
|
|
300
|
+
opId2 = res2.operationId;
|
|
255
301
|
});
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it('should support partial update of operation state', () => {
|
|
259
|
-
const { result } = renderHook(() => useChatStore());
|
|
260
|
-
const initialController = new AbortController();
|
|
261
302
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
abortController: initialController,
|
|
266
|
-
});
|
|
303
|
+
expect(result.current.operations[opId1!].status).toBe('running');
|
|
304
|
+
expect(result.current.operations[opId2!].status).toBe('running');
|
|
305
|
+
expect(opId1).not.toBe(opId2);
|
|
267
306
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
});
|
|
271
|
-
});
|
|
307
|
+
const contextKey1 = messageMapKey('session-1', 'topic-1');
|
|
308
|
+
const contextKey2 = messageMapKey('session-1', 'topic-2');
|
|
272
309
|
|
|
273
|
-
expect(result.current.
|
|
274
|
-
|
|
275
|
-
abortController: initialController,
|
|
276
|
-
inputSendErrorMsg: 'new error',
|
|
277
|
-
});
|
|
310
|
+
expect(result.current.operationsByContext[contextKey1]).toContain(opId1!);
|
|
311
|
+
expect(result.current.operationsByContext[contextKey2]).toContain(opId2!);
|
|
278
312
|
});
|
|
279
313
|
});
|
|
280
314
|
|
|
@@ -292,7 +326,9 @@ describe('ConversationControl actions', () => {
|
|
|
292
326
|
await result.current.switchMessageBranch(messageId, branchIndex);
|
|
293
327
|
});
|
|
294
328
|
|
|
295
|
-
expect(optimisticUpdateSpy).toHaveBeenCalledWith(messageId, {
|
|
329
|
+
expect(optimisticUpdateSpy).toHaveBeenCalledWith(messageId, {
|
|
330
|
+
activeBranchIndex: branchIndex,
|
|
331
|
+
});
|
|
296
332
|
});
|
|
297
333
|
|
|
298
334
|
it('should handle switching to branch 0', async () => {
|
|
@@ -326,7 +362,9 @@ describe('ConversationControl actions', () => {
|
|
|
326
362
|
}),
|
|
327
363
|
).rejects.toThrow('Update failed');
|
|
328
364
|
|
|
329
|
-
expect(optimisticUpdateSpy).toHaveBeenCalledWith(messageId, {
|
|
365
|
+
expect(optimisticUpdateSpy).toHaveBeenCalledWith(messageId, {
|
|
366
|
+
activeBranchIndex: branchIndex,
|
|
367
|
+
});
|
|
330
368
|
});
|
|
331
369
|
});
|
|
332
370
|
});
|
|
@@ -151,9 +151,14 @@ describe('ConversationLifecycle actions', () => {
|
|
|
151
151
|
it('should not regenerate when already regenerating', async () => {
|
|
152
152
|
const { result } = renderHook(() => useChatStore());
|
|
153
153
|
|
|
154
|
+
// Create a regenerate operation to simulate already regenerating
|
|
154
155
|
act(() => {
|
|
156
|
+
const { operationId } = result.current.startOperation({
|
|
157
|
+
type: 'regenerate',
|
|
158
|
+
context: { sessionId: TEST_IDS.SESSION_ID, messageId: TEST_IDS.USER_MESSAGE_ID },
|
|
159
|
+
});
|
|
160
|
+
|
|
155
161
|
useChatStore.setState({
|
|
156
|
-
regeneratingIds: [TEST_IDS.USER_MESSAGE_ID],
|
|
157
162
|
internal_execAgentRuntime: vi.fn(),
|
|
158
163
|
});
|
|
159
164
|
});
|
|
@@ -204,9 +209,14 @@ describe('ConversationLifecycle actions', () => {
|
|
|
204
209
|
it('should not regenerate when already regenerating', async () => {
|
|
205
210
|
const { result } = renderHook(() => useChatStore());
|
|
206
211
|
|
|
212
|
+
// Create a regenerate operation to simulate already regenerating
|
|
207
213
|
act(() => {
|
|
214
|
+
result.current.startOperation({
|
|
215
|
+
type: 'regenerate',
|
|
216
|
+
context: { sessionId: TEST_IDS.SESSION_ID, messageId: TEST_IDS.MESSAGE_ID },
|
|
217
|
+
});
|
|
218
|
+
|
|
208
219
|
useChatStore.setState({
|
|
209
|
-
regeneratingIds: [TEST_IDS.MESSAGE_ID],
|
|
210
220
|
internal_execAgentRuntime: vi.fn(),
|
|
211
221
|
});
|
|
212
222
|
});
|
|
@@ -59,8 +59,6 @@ export const createMockChatConfig = (overrides = {}) => ({
|
|
|
59
59
|
export const createMockStoreState = (overrides = {}) => ({
|
|
60
60
|
activeId: TEST_IDS.SESSION_ID,
|
|
61
61
|
activeTopicId: TEST_IDS.TOPIC_ID,
|
|
62
|
-
chatLoadingIds: [],
|
|
63
|
-
chatLoadingIdsAbortController: undefined,
|
|
64
62
|
messagesMap: {},
|
|
65
63
|
toolCallingStreamIds: {},
|
|
66
64
|
...overrides,
|