@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,96 @@
|
|
|
1
|
+
import { nanoid } from '@lobechat/utils';
|
|
2
|
+
|
|
3
|
+
import type { Operation, OperationType } from '@/store/chat/slices/operation/types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create a mock Operation object for testing
|
|
7
|
+
*/
|
|
8
|
+
export const createMockOperation = (
|
|
9
|
+
type: OperationType,
|
|
10
|
+
context: Record<string, any> = {},
|
|
11
|
+
overrides: Partial<Operation> = {},
|
|
12
|
+
): Operation => {
|
|
13
|
+
return {
|
|
14
|
+
abortController: new AbortController(),
|
|
15
|
+
childOperationIds: [],
|
|
16
|
+
context,
|
|
17
|
+
id: `op_${nanoid()}`,
|
|
18
|
+
metadata: {
|
|
19
|
+
startTime: Date.now(),
|
|
20
|
+
},
|
|
21
|
+
status: 'running',
|
|
22
|
+
type,
|
|
23
|
+
...overrides,
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create a cancelled operation
|
|
29
|
+
*/
|
|
30
|
+
export const createCancelledOperation = (
|
|
31
|
+
type: OperationType,
|
|
32
|
+
context: Record<string, any> = {},
|
|
33
|
+
): Operation => {
|
|
34
|
+
const operation = createMockOperation(type, context, { status: 'cancelled' });
|
|
35
|
+
operation.abortController.abort();
|
|
36
|
+
operation.metadata.cancelReason = 'Test cancellation';
|
|
37
|
+
return operation;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create a completed operation
|
|
42
|
+
*/
|
|
43
|
+
export const createCompletedOperation = (
|
|
44
|
+
type: OperationType,
|
|
45
|
+
context: Record<string, any> = {},
|
|
46
|
+
): Operation => {
|
|
47
|
+
return createMockOperation(type, context, {
|
|
48
|
+
metadata: {
|
|
49
|
+
duration: 1000,
|
|
50
|
+
endTime: Date.now(),
|
|
51
|
+
startTime: Date.now() - 1000,
|
|
52
|
+
},
|
|
53
|
+
status: 'completed',
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Create a failed operation
|
|
59
|
+
*/
|
|
60
|
+
export const createFailedOperation = (
|
|
61
|
+
type: OperationType,
|
|
62
|
+
context: Record<string, any> = {},
|
|
63
|
+
// eslint-disable-next-line unicorn/no-object-as-default-parameter
|
|
64
|
+
error: { message: string; type: string } = { message: 'Test error', type: 'TestError' },
|
|
65
|
+
): Operation => {
|
|
66
|
+
return createMockOperation(type, context, {
|
|
67
|
+
metadata: {
|
|
68
|
+
duration: 1000,
|
|
69
|
+
endTime: Date.now(),
|
|
70
|
+
error,
|
|
71
|
+
startTime: Date.now() - 1000,
|
|
72
|
+
},
|
|
73
|
+
status: 'failed',
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Create an operation tree (parent with children)
|
|
79
|
+
*/
|
|
80
|
+
export const createOperationTree = (
|
|
81
|
+
parentType: OperationType,
|
|
82
|
+
childTypes: OperationType[],
|
|
83
|
+
context: Record<string, any> = {},
|
|
84
|
+
) => {
|
|
85
|
+
const parent = createMockOperation(parentType, context);
|
|
86
|
+
|
|
87
|
+
const children = childTypes.map((childType) =>
|
|
88
|
+
createMockOperation(childType, context, {
|
|
89
|
+
parentOperationId: parent.id,
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
parent.childOperationIds = children.map((c) => c.id);
|
|
94
|
+
|
|
95
|
+
return { children, parent };
|
|
96
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { nanoid } from '@lobechat/utils';
|
|
2
|
+
import { vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import type { ChatStore } from '@/store/chat/store';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create a mock ChatStore for testing executors
|
|
8
|
+
* All methods are mocked with vi.fn() and can be customized
|
|
9
|
+
*/
|
|
10
|
+
export const createMockStore = (overrides: Partial<ChatStore> = {}): ChatStore => {
|
|
11
|
+
const operations: Record<string, any> = {};
|
|
12
|
+
const messageOperationMap: Record<string, string> = {};
|
|
13
|
+
const operationsByMessage: Record<string, string[]> = {};
|
|
14
|
+
const dbMessagesMap: Record<string, any[]> = {};
|
|
15
|
+
|
|
16
|
+
const store = {
|
|
17
|
+
// Other store properties (add as needed)
|
|
18
|
+
activeId: 'test-session',
|
|
19
|
+
|
|
20
|
+
activeTopicId: 'test-topic',
|
|
21
|
+
|
|
22
|
+
associateMessageWithOperation: vi.fn().mockImplementation((messageId, operationId) => {
|
|
23
|
+
messageOperationMap[messageId] = operationId;
|
|
24
|
+
|
|
25
|
+
if (!operationsByMessage[messageId]) {
|
|
26
|
+
operationsByMessage[messageId] = [];
|
|
27
|
+
}
|
|
28
|
+
if (!operationsByMessage[messageId].includes(operationId)) {
|
|
29
|
+
operationsByMessage[messageId].push(operationId);
|
|
30
|
+
}
|
|
31
|
+
}),
|
|
32
|
+
|
|
33
|
+
cancelOperation: vi.fn().mockImplementation((operationId) => {
|
|
34
|
+
if (operations[operationId]) {
|
|
35
|
+
operations[operationId].abortController.abort();
|
|
36
|
+
operations[operationId].status = 'cancelled';
|
|
37
|
+
}
|
|
38
|
+
}),
|
|
39
|
+
|
|
40
|
+
completeOperation: vi.fn().mockImplementation((operationId) => {
|
|
41
|
+
if (operations[operationId]) {
|
|
42
|
+
operations[operationId].status = 'completed';
|
|
43
|
+
operations[operationId].metadata.endTime = Date.now();
|
|
44
|
+
}
|
|
45
|
+
}),
|
|
46
|
+
|
|
47
|
+
// Message state
|
|
48
|
+
dbMessagesMap,
|
|
49
|
+
|
|
50
|
+
failOperation: vi.fn().mockImplementation((operationId, error) => {
|
|
51
|
+
if (operations[operationId]) {
|
|
52
|
+
operations[operationId].status = 'failed';
|
|
53
|
+
operations[operationId].metadata.error = error;
|
|
54
|
+
operations[operationId].metadata.endTime = Date.now();
|
|
55
|
+
}
|
|
56
|
+
}),
|
|
57
|
+
|
|
58
|
+
// AI chat methods
|
|
59
|
+
internal_fetchAIChatMessage: vi.fn().mockResolvedValue(undefined),
|
|
60
|
+
|
|
61
|
+
internal_invokeDifferentTypePlugin: vi.fn().mockResolvedValue({ error: null }),
|
|
62
|
+
|
|
63
|
+
messageOperationMap,
|
|
64
|
+
|
|
65
|
+
onOperationCancel: vi.fn(),
|
|
66
|
+
|
|
67
|
+
// Operation state
|
|
68
|
+
operations,
|
|
69
|
+
|
|
70
|
+
operationsByContext: {},
|
|
71
|
+
|
|
72
|
+
operationsByMessage,
|
|
73
|
+
|
|
74
|
+
operationsByType: {} as any,
|
|
75
|
+
|
|
76
|
+
optimisticAddToolToAssistantMessage: vi.fn().mockResolvedValue(undefined),
|
|
77
|
+
|
|
78
|
+
// Message management methods
|
|
79
|
+
optimisticCreateMessage: vi.fn().mockImplementation(async (params) => {
|
|
80
|
+
const id = nanoid();
|
|
81
|
+
const message = { id, ...params, createdAt: Date.now(), updatedAt: Date.now() };
|
|
82
|
+
return message;
|
|
83
|
+
}),
|
|
84
|
+
|
|
85
|
+
optimisticUpdateMessageContent: vi.fn().mockResolvedValue(undefined),
|
|
86
|
+
|
|
87
|
+
optimisticUpdateMessagePlugin: vi.fn().mockResolvedValue(undefined),
|
|
88
|
+
|
|
89
|
+
optimisticUpdateMessagePluginError: vi.fn().mockResolvedValue(undefined),
|
|
90
|
+
|
|
91
|
+
optimisticUpdatePluginArguments: vi.fn().mockResolvedValue(undefined),
|
|
92
|
+
|
|
93
|
+
optimisticUpdatePluginState: vi.fn().mockResolvedValue(undefined),
|
|
94
|
+
|
|
95
|
+
// Operation management methods
|
|
96
|
+
startOperation: vi.fn().mockImplementation((config) => {
|
|
97
|
+
const operationId = `op_${nanoid()}`;
|
|
98
|
+
const abortController = new AbortController();
|
|
99
|
+
|
|
100
|
+
const operation = {
|
|
101
|
+
abortController,
|
|
102
|
+
childOperationIds: [],
|
|
103
|
+
context: config.context || {},
|
|
104
|
+
id: operationId,
|
|
105
|
+
metadata: config.metadata || { startTime: Date.now() },
|
|
106
|
+
parentOperationId: config.parentOperationId,
|
|
107
|
+
status: 'running',
|
|
108
|
+
type: config.type,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
operations[operationId] = operation;
|
|
112
|
+
|
|
113
|
+
// Auto-associate message with operation if messageId exists
|
|
114
|
+
if (config.context?.messageId) {
|
|
115
|
+
messageOperationMap[config.context.messageId] = operationId;
|
|
116
|
+
|
|
117
|
+
if (!operationsByMessage[config.context.messageId]) {
|
|
118
|
+
operationsByMessage[config.context.messageId] = [];
|
|
119
|
+
}
|
|
120
|
+
operationsByMessage[config.context.messageId].push(operationId);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { abortController, operationId };
|
|
124
|
+
}),
|
|
125
|
+
updateOperationMetadata: vi.fn().mockImplementation((operationId, metadata) => {
|
|
126
|
+
if (operations[operationId]) {
|
|
127
|
+
operations[operationId].metadata = {
|
|
128
|
+
...operations[operationId].metadata,
|
|
129
|
+
...metadata,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}),
|
|
133
|
+
|
|
134
|
+
...overrides,
|
|
135
|
+
} as unknown as ChatStore;
|
|
136
|
+
|
|
137
|
+
return store;
|
|
138
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import type { OperationType } from '@/store/chat/slices/operation/types';
|
|
4
|
+
import type { ChatStore } from '@/store/chat/store';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Assert that an operation was created with specific type
|
|
8
|
+
*/
|
|
9
|
+
export const expectOperationCreated = (mockStore: ChatStore, type: OperationType) => {
|
|
10
|
+
expect(mockStore.startOperation).toHaveBeenCalledWith(
|
|
11
|
+
expect.objectContaining({
|
|
12
|
+
type,
|
|
13
|
+
}),
|
|
14
|
+
);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Assert that a message was created with specific role
|
|
19
|
+
*/
|
|
20
|
+
export const expectMessageCreated = (mockStore: ChatStore, role: 'assistant' | 'tool' | 'user') => {
|
|
21
|
+
expect(mockStore.optimisticCreateMessage).toHaveBeenCalledWith(
|
|
22
|
+
expect.objectContaining({
|
|
23
|
+
role,
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Assert that a cancel handler was registered
|
|
30
|
+
*/
|
|
31
|
+
export const expectCancelHandlerRegistered = (mockStore: ChatStore, operationId?: string) => {
|
|
32
|
+
if (operationId) {
|
|
33
|
+
expect(mockStore.onOperationCancel).toHaveBeenCalledWith(operationId, expect.any(Function));
|
|
34
|
+
} else {
|
|
35
|
+
expect(mockStore.onOperationCancel).toHaveBeenCalled();
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Assert that an operation was completed
|
|
41
|
+
*/
|
|
42
|
+
export const expectOperationCompleted = (mockStore: ChatStore, operationId: string) => {
|
|
43
|
+
expect(mockStore.completeOperation).toHaveBeenCalledWith(operationId);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Assert that an operation was failed
|
|
48
|
+
*/
|
|
49
|
+
export const expectOperationFailed = (
|
|
50
|
+
mockStore: ChatStore,
|
|
51
|
+
operationId: string,
|
|
52
|
+
errorType?: string,
|
|
53
|
+
) => {
|
|
54
|
+
if (errorType) {
|
|
55
|
+
expect(mockStore.failOperation).toHaveBeenCalledWith(
|
|
56
|
+
operationId,
|
|
57
|
+
expect.objectContaining({
|
|
58
|
+
type: errorType,
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
} else {
|
|
62
|
+
expect(mockStore.failOperation).toHaveBeenCalledWith(operationId, expect.any(Object));
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Assert that message content was updated
|
|
68
|
+
*/
|
|
69
|
+
export const expectMessageContentUpdated = (
|
|
70
|
+
mockStore: ChatStore,
|
|
71
|
+
messageId: string,
|
|
72
|
+
content?: string,
|
|
73
|
+
) => {
|
|
74
|
+
if (content) {
|
|
75
|
+
expect(mockStore.optimisticUpdateMessageContent).toHaveBeenCalledWith(
|
|
76
|
+
messageId,
|
|
77
|
+
content,
|
|
78
|
+
expect.anything(),
|
|
79
|
+
expect.anything(),
|
|
80
|
+
);
|
|
81
|
+
} else {
|
|
82
|
+
expect(mockStore.optimisticUpdateMessageContent).toHaveBeenCalledWith(
|
|
83
|
+
messageId,
|
|
84
|
+
expect.any(String),
|
|
85
|
+
expect.anything(),
|
|
86
|
+
expect.anything(),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Assert that message plugin was updated
|
|
93
|
+
*/
|
|
94
|
+
export const expectMessagePluginUpdated = (
|
|
95
|
+
mockStore: ChatStore,
|
|
96
|
+
messageId: string,
|
|
97
|
+
interventionStatus?: string,
|
|
98
|
+
) => {
|
|
99
|
+
if (interventionStatus) {
|
|
100
|
+
expect(mockStore.optimisticUpdateMessagePlugin).toHaveBeenCalledWith(
|
|
101
|
+
messageId,
|
|
102
|
+
expect.objectContaining({
|
|
103
|
+
intervention: expect.objectContaining({
|
|
104
|
+
status: interventionStatus,
|
|
105
|
+
}),
|
|
106
|
+
}),
|
|
107
|
+
expect.anything(),
|
|
108
|
+
);
|
|
109
|
+
} else {
|
|
110
|
+
expect(mockStore.optimisticUpdateMessagePlugin).toHaveBeenCalled();
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Assert that internal_fetchAIChatMessage was called with correct params
|
|
116
|
+
*/
|
|
117
|
+
export const expectFetchAIChatMessageCalled = (mockStore: ChatStore, messageId?: string) => {
|
|
118
|
+
if (messageId) {
|
|
119
|
+
expect(mockStore.internal_fetchAIChatMessage).toHaveBeenCalledWith(
|
|
120
|
+
messageId,
|
|
121
|
+
expect.anything(),
|
|
122
|
+
);
|
|
123
|
+
} else {
|
|
124
|
+
expect(mockStore.internal_fetchAIChatMessage).toHaveBeenCalled();
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Assert that internal_invokeDifferentTypePlugin was called
|
|
130
|
+
*/
|
|
131
|
+
export const expectInvokePluginCalled = (mockStore: ChatStore, messageId?: string) => {
|
|
132
|
+
if (messageId) {
|
|
133
|
+
expect(mockStore.internal_invokeDifferentTypePlugin).toHaveBeenCalledWith(
|
|
134
|
+
messageId,
|
|
135
|
+
expect.anything(),
|
|
136
|
+
);
|
|
137
|
+
} else {
|
|
138
|
+
expect(mockStore.internal_invokeDifferentTypePlugin).toHaveBeenCalled();
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Assert that operation metadata was updated
|
|
144
|
+
*/
|
|
145
|
+
export const expectOperationMetadataUpdated = (
|
|
146
|
+
mockStore: ChatStore,
|
|
147
|
+
operationId: string,
|
|
148
|
+
metadata?: Record<string, any>,
|
|
149
|
+
) => {
|
|
150
|
+
if (metadata) {
|
|
151
|
+
expect(mockStore.updateOperationMetadata).toHaveBeenCalledWith(
|
|
152
|
+
operationId,
|
|
153
|
+
expect.objectContaining(metadata),
|
|
154
|
+
);
|
|
155
|
+
} else {
|
|
156
|
+
expect(mockStore.updateOperationMetadata).toHaveBeenCalledWith(operationId, expect.any(Object));
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Assert executor result structure
|
|
162
|
+
*/
|
|
163
|
+
export const expectValidExecutorResult = (result: any) => {
|
|
164
|
+
expect(result).toHaveProperty('events');
|
|
165
|
+
expect(result).toHaveProperty('newState');
|
|
166
|
+
expect(Array.isArray(result.events)).toBe(true);
|
|
167
|
+
expect(result.newState).toBeDefined();
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Assert executor returned specific event type
|
|
172
|
+
*/
|
|
173
|
+
export const expectEventType = (result: any, eventType: string) => {
|
|
174
|
+
expect(result.events.some((e: any) => e.type === eventType)).toBe(true);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Assert executor returned next context
|
|
179
|
+
*/
|
|
180
|
+
export const expectNextContext = (result: any, phase?: string) => {
|
|
181
|
+
expect(result.nextContext).toBeDefined();
|
|
182
|
+
if (phase) {
|
|
183
|
+
expect(result.nextContext.phase).toBe(phase);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { Operation } from '@/store/chat/slices/operation/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Simulate operation cancellation
|
|
5
|
+
*/
|
|
6
|
+
export const simulateOperationCancellation = (
|
|
7
|
+
operation: Operation,
|
|
8
|
+
reason: string = 'Test cancellation',
|
|
9
|
+
) => {
|
|
10
|
+
operation.abortController.abort();
|
|
11
|
+
operation.status = 'cancelled';
|
|
12
|
+
if (operation.metadata) {
|
|
13
|
+
operation.metadata.cancelReason = reason;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Simulate cascading cancellation through operation tree
|
|
19
|
+
*/
|
|
20
|
+
export const simulateCascadingCancellation = (
|
|
21
|
+
parentOperation: Operation,
|
|
22
|
+
childOperations: Operation[],
|
|
23
|
+
reason: string = 'Parent operation cancelled',
|
|
24
|
+
) => {
|
|
25
|
+
simulateOperationCancellation(parentOperation, reason);
|
|
26
|
+
childOperations.forEach((child) => simulateOperationCancellation(child, reason));
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Wait for operation status change
|
|
31
|
+
*/
|
|
32
|
+
export const waitForOperationStatus = async (
|
|
33
|
+
getOperation: () => Operation | undefined,
|
|
34
|
+
targetStatus: Operation['status'],
|
|
35
|
+
timeout: number = 1000,
|
|
36
|
+
): Promise<boolean> => {
|
|
37
|
+
const startTime = Date.now();
|
|
38
|
+
while (Date.now() - startTime < timeout) {
|
|
39
|
+
const operation = getOperation();
|
|
40
|
+
if (operation?.status === targetStatus) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
await new Promise((resolve) => {
|
|
44
|
+
setTimeout(resolve, 10);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Verify operation tree structure
|
|
52
|
+
*/
|
|
53
|
+
export const verifyOperationTree = (parent: Operation, expectedChildIds: string[]): boolean => {
|
|
54
|
+
if (!parent.childOperationIds) return false;
|
|
55
|
+
if (parent.childOperationIds.length !== expectedChildIds.length) return false;
|
|
56
|
+
return expectedChildIds.every((id) => parent.childOperationIds!.includes(id));
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get all operations in a tree (parent + all descendants)
|
|
61
|
+
*/
|
|
62
|
+
export const getAllOperationsInTree = (
|
|
63
|
+
operations: Record<string, Operation>,
|
|
64
|
+
rootOperationId: string,
|
|
65
|
+
): Operation[] => {
|
|
66
|
+
const result: Operation[] = [];
|
|
67
|
+
const visited = new Set<string>();
|
|
68
|
+
|
|
69
|
+
const traverse = (operationId: string) => {
|
|
70
|
+
if (visited.has(operationId)) return;
|
|
71
|
+
visited.add(operationId);
|
|
72
|
+
|
|
73
|
+
const operation = operations[operationId];
|
|
74
|
+
if (!operation) return;
|
|
75
|
+
|
|
76
|
+
result.push(operation);
|
|
77
|
+
|
|
78
|
+
if (operation.childOperationIds) {
|
|
79
|
+
operation.childOperationIds.forEach(traverse);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
traverse(rootOperationId);
|
|
84
|
+
return result;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Create AbortSignal that aborts after delay
|
|
89
|
+
*/
|
|
90
|
+
export const createDelayedAbortSignal = (delayMs: number): AbortSignal => {
|
|
91
|
+
const controller = new AbortController();
|
|
92
|
+
setTimeout(() => controller.abort(), delayMs);
|
|
93
|
+
return controller.signal;
|
|
94
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentInstruction,
|
|
3
|
+
AgentState,
|
|
4
|
+
} from '@lobechat/agent-runtime';
|
|
5
|
+
|
|
6
|
+
import { createAgentExecutors } from '@/store/chat/agents/createAgentExecutors';
|
|
7
|
+
import type { OperationType } from '@/store/chat/slices/operation/types';
|
|
8
|
+
import type { ChatStore } from '@/store/chat/store';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Execute an executor with mock context
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const result = await executeWithMockContext({
|
|
15
|
+
* executor: 'call_llm',
|
|
16
|
+
* instruction: createCallLLMInstruction(),
|
|
17
|
+
* state: createInitialState(),
|
|
18
|
+
* mockStore,
|
|
19
|
+
* context: { operationId: 'op_123', messageKey: 'session_topic', parentId: 'msg_456' }
|
|
20
|
+
* });
|
|
21
|
+
*/
|
|
22
|
+
export const executeWithMockContext = async ({
|
|
23
|
+
executor,
|
|
24
|
+
instruction,
|
|
25
|
+
state,
|
|
26
|
+
mockStore,
|
|
27
|
+
context,
|
|
28
|
+
skipCreateFirstMessage = false,
|
|
29
|
+
}: {
|
|
30
|
+
context: {
|
|
31
|
+
messageKey: string;
|
|
32
|
+
operationId: string;
|
|
33
|
+
parentId: string;
|
|
34
|
+
sessionId?: string;
|
|
35
|
+
topicId?: string | null;
|
|
36
|
+
};
|
|
37
|
+
executor: AgentInstruction['type'];
|
|
38
|
+
instruction: AgentInstruction;
|
|
39
|
+
mockStore: ChatStore;
|
|
40
|
+
skipCreateFirstMessage?: boolean;
|
|
41
|
+
state: AgentState;
|
|
42
|
+
}) => {
|
|
43
|
+
// Ensure operation exists in store
|
|
44
|
+
if (!mockStore.operations[context.operationId]) {
|
|
45
|
+
mockStore.operations[context.operationId] = {
|
|
46
|
+
abortController: new AbortController(),
|
|
47
|
+
childOperationIds: [],
|
|
48
|
+
context: {
|
|
49
|
+
messageId: context.parentId,
|
|
50
|
+
sessionId: context.sessionId || 'test-session',
|
|
51
|
+
topicId: context.topicId !== undefined ? context.topicId : 'test-topic',
|
|
52
|
+
},
|
|
53
|
+
id: context.operationId,
|
|
54
|
+
metadata: { startTime: Date.now() },
|
|
55
|
+
status: 'running',
|
|
56
|
+
type: 'execAgentRuntime' as OperationType,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Create executors with mock context
|
|
61
|
+
const executors = createAgentExecutors({
|
|
62
|
+
get: () => mockStore,
|
|
63
|
+
messageKey: context.messageKey,
|
|
64
|
+
operationId: context.operationId,
|
|
65
|
+
parentId: context.parentId,
|
|
66
|
+
skipCreateFirstMessage,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const executorFn = executors[executor];
|
|
70
|
+
if (!executorFn) {
|
|
71
|
+
throw new Error(`Executor ${executor} not found`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Execute
|
|
75
|
+
const result = await executorFn(instruction, state);
|
|
76
|
+
|
|
77
|
+
return result;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Create initial agent runtime state for testing
|
|
82
|
+
*/
|
|
83
|
+
export const createInitialState = (overrides: Partial<AgentState> = {}): AgentState => {
|
|
84
|
+
const defaultState: any = {
|
|
85
|
+
lastModified: new Date().toISOString(),
|
|
86
|
+
messages: [],
|
|
87
|
+
sessionId: 'test-session',
|
|
88
|
+
status: 'running',
|
|
89
|
+
stepCount: 1,
|
|
90
|
+
usage: {
|
|
91
|
+
humanInteraction: {
|
|
92
|
+
approvalRequests: 0,
|
|
93
|
+
promptRequests: 0,
|
|
94
|
+
selectRequests: 0,
|
|
95
|
+
totalWaitingTimeMs: 0,
|
|
96
|
+
},
|
|
97
|
+
llm: {
|
|
98
|
+
apiCalls: 0,
|
|
99
|
+
processingTimeMs: 0,
|
|
100
|
+
tokens: {
|
|
101
|
+
input: 0,
|
|
102
|
+
output: 0,
|
|
103
|
+
total: 0,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
tools: {
|
|
107
|
+
byTool: [],
|
|
108
|
+
totalCalls: 0,
|
|
109
|
+
totalTimeMs: 0,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
...defaultState,
|
|
116
|
+
...overrides,
|
|
117
|
+
} as AgentState;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Create a test context object for executor
|
|
122
|
+
*/
|
|
123
|
+
export const createTestContext = (
|
|
124
|
+
overrides: {
|
|
125
|
+
messageKey?: string;
|
|
126
|
+
operationId?: string;
|
|
127
|
+
parentId?: string;
|
|
128
|
+
sessionId?: string;
|
|
129
|
+
topicId?: string | null;
|
|
130
|
+
} = {},
|
|
131
|
+
) => {
|
|
132
|
+
return {
|
|
133
|
+
messageKey:
|
|
134
|
+
overrides.messageKey ||
|
|
135
|
+
`${overrides.sessionId || 'test-session'}_${overrides.topicId !== undefined ? overrides.topicId : 'test-topic'}`,
|
|
136
|
+
operationId: overrides.operationId || 'op_test',
|
|
137
|
+
parentId: overrides.parentId || 'msg_parent',
|
|
138
|
+
};
|
|
139
|
+
};
|