Skip to content

Commit ecd1d25

Browse files
committed
✅ test: add test
1 parent 32cd78f commit ecd1d25

File tree

7 files changed

+487
-11
lines changed

7 files changed

+487
-11
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
// @vitest-environment node
2+
import { AzureKeyCredential, OpenAIClient } from '@azure/openai';
3+
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4+
5+
import * as debugStreamModule from '../utils/debugStream';
6+
import { LobeAzureOpenAI } from './index';
7+
8+
// Mock the console.error to avoid polluting test output
9+
vi.spyOn(console, 'error').mockImplementation(() => {});
10+
11+
describe('LobeAzureOpenAI', () => {
12+
let instance: LobeAzureOpenAI;
13+
14+
beforeEach(() => {
15+
instance = new LobeAzureOpenAI(
16+
'https://test.openai.azure.com/',
17+
'test_key',
18+
'2023-03-15-preview',
19+
);
20+
21+
// 使用 vi.spyOn 来模拟 streamChatCompletions 方法
22+
vi.spyOn(instance['client'], 'streamChatCompletions').mockResolvedValue(
23+
new ReadableStream() as any,
24+
);
25+
});
26+
27+
afterEach(() => {
28+
vi.clearAllMocks();
29+
});
30+
31+
describe('constructor', () => {
32+
it('should throw InvalidAzureAPIKey error when apikey or endpoint is missing', () => {
33+
try {
34+
new LobeAzureOpenAI();
35+
} catch (e) {
36+
expect(e).toEqual({ errorType: 'InvalidAzureAPIKey' });
37+
}
38+
});
39+
40+
it('should create an instance of OpenAIClient with correct parameters', () => {
41+
const endpoint = 'https://test.openai.azure.com/';
42+
const apikey = 'test_key';
43+
const apiVersion = '2023-03-15-preview';
44+
45+
const instance = new LobeAzureOpenAI(endpoint, apikey, apiVersion);
46+
47+
expect(instance.client).toBeInstanceOf(OpenAIClient);
48+
expect(instance.baseURL).toBe(endpoint);
49+
});
50+
});
51+
52+
describe('chat', () => {
53+
it('should return a StreamingTextResponse on successful API call', async () => {
54+
// Arrange
55+
const mockStream = new ReadableStream();
56+
const mockResponse = Promise.resolve(mockStream);
57+
58+
(instance['client'].streamChatCompletions as Mock).mockResolvedValue(mockResponse);
59+
60+
// Act
61+
const result = await instance.chat({
62+
messages: [{ content: 'Hello', role: 'user' }],
63+
model: 'text-davinci-003',
64+
temperature: 0,
65+
});
66+
67+
// Assert
68+
expect(result).toBeInstanceOf(Response);
69+
});
70+
71+
describe('Error', () => {
72+
it('should return AzureBizError with DeploymentNotFound error', async () => {
73+
// Arrange
74+
const error = {
75+
code: 'DeploymentNotFound',
76+
message: 'Deployment not found',
77+
};
78+
79+
(instance['client'].streamChatCompletions as Mock).mockRejectedValue(error);
80+
81+
// Act
82+
try {
83+
await instance.chat({
84+
messages: [{ content: 'Hello', role: 'user' }],
85+
model: 'text-davinci-003',
86+
temperature: 0,
87+
});
88+
} catch (e) {
89+
// Assert
90+
expect(e).toEqual({
91+
endpoint: 'https://test.openai.azure.com/',
92+
error: {
93+
code: 'DeploymentNotFound',
94+
message: 'Deployment not found',
95+
deployId: 'text-davinci-003',
96+
},
97+
errorType: 'AzureBizError',
98+
provider: 'azure',
99+
});
100+
}
101+
});
102+
103+
it('should return AgentRuntimeError for non-Azure errors', async () => {
104+
// Arrange
105+
const genericError = new Error('Generic Error');
106+
107+
(instance['client'].streamChatCompletions as Mock).mockRejectedValue(genericError);
108+
109+
// Act
110+
try {
111+
await instance.chat({
112+
messages: [{ content: 'Hello', role: 'user' }],
113+
model: 'text-davinci-003',
114+
temperature: 0,
115+
});
116+
} catch (e) {
117+
// Assert
118+
expect(e).toEqual({
119+
endpoint: 'https://test.openai.azure.com/',
120+
errorType: 'AgentRuntimeError',
121+
provider: 'azure',
122+
error: {
123+
name: genericError.name,
124+
cause: genericError.cause,
125+
message: genericError.message,
126+
},
127+
});
128+
}
129+
});
130+
});
131+
132+
describe('DEBUG', () => {
133+
it('should call debugStream when DEBUG_CHAT_COMPLETION is 1', async () => {
134+
// Arrange
135+
const mockProdStream = new ReadableStream() as any;
136+
const mockDebugStream = new ReadableStream({
137+
start(controller) {
138+
controller.enqueue('Debug stream content');
139+
controller.close();
140+
},
141+
}) as any;
142+
mockDebugStream.toReadableStream = () => mockDebugStream;
143+
144+
(instance['client'].streamChatCompletions as Mock).mockResolvedValue({
145+
tee: () => [mockProdStream, { toReadableStream: () => mockDebugStream }],
146+
});
147+
148+
process.env.DEBUG_AZURE_CHAT_COMPLETION = '1';
149+
vi.spyOn(debugStreamModule, 'debugStream').mockImplementation(() => Promise.resolve());
150+
151+
// Act
152+
await instance.chat({
153+
messages: [{ content: 'Hello', role: 'user' }],
154+
model: 'text-davinci-003',
155+
temperature: 0,
156+
});
157+
158+
// Assert
159+
expect(debugStreamModule.debugStream).toHaveBeenCalled();
160+
161+
// Restore
162+
delete process.env.DEBUG_AZURE_CHAT_COMPLETION;
163+
});
164+
});
165+
});
166+
});

src/libs/agent-runtime/azureOpenai/index.ts

+16-8
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,15 @@ import { AgentRuntimeErrorType } from '../error';
1111
import { ChatStreamPayload, ModelProvider } from '../types';
1212
import { AgentRuntimeError } from '../utils/createError';
1313
import { debugStream } from '../utils/debugStream';
14-
import { DEBUG_CHAT_COMPLETION } from '../utils/env';
1514

1615
export class LobeAzureOpenAI implements LobeRuntimeAI {
17-
private _client: OpenAIClient;
16+
client: OpenAIClient;
1817

1918
constructor(endpoint?: string, apikey?: string, apiVersion?: string) {
2019
if (!apikey || !endpoint)
2120
throw AgentRuntimeError.createError(AgentRuntimeErrorType.InvalidAzureAPIKey);
2221

23-
this._client = new OpenAIClient(endpoint, new AzureKeyCredential(apikey), { apiVersion });
22+
this.client = new OpenAIClient(endpoint, new AzureKeyCredential(apikey), { apiVersion });
2423

2524
this.baseURL = endpoint;
2625
}
@@ -34,7 +33,7 @@ export class LobeAzureOpenAI implements LobeRuntimeAI {
3433
// ============ 2. send api ============ //
3534

3635
try {
37-
const response = await this._client.streamChatCompletions(
36+
const response = await this.client.streamChatCompletions(
3837
model,
3938
messages as ChatRequestMessage[],
4039
params as GetChatCompletionsOptions,
@@ -45,25 +44,34 @@ export class LobeAzureOpenAI implements LobeRuntimeAI {
4544

4645
const [debug, prod] = stream.tee();
4746

48-
if (DEBUG_CHAT_COMPLETION) {
47+
if (process.env.DEBUG_AZURE_CHAT_COMPLETION === '1') {
4948
debugStream(debug).catch(console.error);
5049
}
5150

5251
return new StreamingTextResponse(prod);
5352
} catch (e) {
5453
let error = e as { [key: string]: any; code: string; message: string };
5554

56-
switch (error.code) {
57-
case 'DeploymentNotFound': {
58-
error = { ...error, deployId: model };
55+
if (error.code) {
56+
switch (error.code) {
57+
case 'DeploymentNotFound': {
58+
error = { ...error, deployId: model };
59+
}
5960
}
61+
} else {
62+
error = {
63+
cause: error.cause,
64+
message: error.message,
65+
name: error.name,
66+
} as any;
6067
}
6168

6269
const errorType = error.code
6370
? AgentRuntimeErrorType.AzureBizError
6471
: AgentRuntimeErrorType.AgentRuntimeError;
6572

6673
throw AgentRuntimeError.chat({
74+
endpoint: this.baseURL,
6775
error,
6876
errorType,
6977
provider: ModelProvider.Azure,

src/libs/agent-runtime/utils/env.ts

-1
This file was deleted.

src/store/chat/slices/share/action.test.ts

+113
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { act, renderHook } from '@testing-library/react';
33
import { DEFAULT_USER_AVATAR_URL } from '@/const/meta';
44
import { shareGPTService } from '@/services/share';
55
import { useChatStore } from '@/store/chat';
6+
import { ChatMessage } from '@/types/message';
67

78
describe('shareSlice actions', () => {
89
let shareGPTServiceSpy: any;
@@ -82,5 +83,117 @@ describe('shareSlice actions', () => {
8283
expect(result.current.shareLoading).toBe(false);
8384
// 注意:这里的验证可能需要你根据实际的状态管理逻辑进行调整
8485
});
86+
87+
it('should include plugin information when withPluginInfo is true', async () => {
88+
// 模拟带有插件信息的消息
89+
const pluginMessage = {
90+
role: 'function',
91+
content: 'plugin content',
92+
plugin: {
93+
type: 'default',
94+
arguments: '{}',
95+
apiName: 'test-api',
96+
identifier: 'test-identifier',
97+
},
98+
id: 'abc',
99+
} as ChatMessage;
100+
101+
act(() => {
102+
useChatStore.setState({ messages: [pluginMessage] });
103+
});
104+
105+
const { result } = renderHook(() => useChatStore());
106+
await act(async () => {
107+
result.current.shareToShareGPT({ withPluginInfo: true });
108+
});
109+
expect(shareGPTServiceSpy).toHaveBeenCalledWith(
110+
expect.objectContaining({
111+
items: expect.arrayContaining([
112+
expect.objectContaining({
113+
from: 'gpt',
114+
value: expect.stringContaining('Function Calling Plugin'),
115+
}),
116+
]),
117+
}),
118+
);
119+
});
120+
121+
it('should not include plugin information when withPluginInfo is false', async () => {
122+
const pluginMessage = {
123+
role: 'function',
124+
content: 'plugin content',
125+
plugin: {
126+
type: 'default',
127+
arguments: '{}',
128+
apiName: 'test-api',
129+
identifier: 'test-identifier',
130+
},
131+
id: 'abc',
132+
} as ChatMessage;
133+
134+
act(() => {
135+
useChatStore.setState({ messages: [pluginMessage] });
136+
});
137+
138+
const { result } = renderHook(() => useChatStore());
139+
await act(async () => {
140+
result.current.shareToShareGPT({ withPluginInfo: false });
141+
});
142+
expect(shareGPTServiceSpy).toHaveBeenCalledWith(
143+
expect.objectContaining({
144+
items: expect.not.arrayContaining([
145+
expect.objectContaining({
146+
from: 'gpt',
147+
value: expect.stringContaining('Function Calling Plugin'),
148+
}),
149+
]),
150+
}),
151+
);
152+
});
153+
154+
it('should handle messages from different roles correctly', async () => {
155+
const messages = [
156+
{ role: 'user', content: 'user message', id: '1' },
157+
{ role: 'assistant', content: 'assistant message', id: '2' },
158+
{
159+
role: 'function',
160+
content: 'plugin content',
161+
plugin: {
162+
type: 'default',
163+
arguments: '{}',
164+
apiName: 'test-api',
165+
identifier: 'test-identifier',
166+
},
167+
id: '3',
168+
},
169+
] as ChatMessage[];
170+
171+
act(() => {
172+
useChatStore.setState({ messages });
173+
});
174+
175+
const { result } = renderHook(() => useChatStore());
176+
await act(async () => {
177+
await result.current.shareToShareGPT({
178+
withPluginInfo: true,
179+
withSystemRole: true,
180+
});
181+
});
182+
183+
expect(shareGPTServiceSpy).toHaveBeenCalledWith(
184+
expect.objectContaining({
185+
items: [
186+
expect.objectContaining({ from: 'gpt' }), // Agent meta info
187+
expect.objectContaining({ from: 'human', value: 'user message' }),
188+
expect.objectContaining({ from: 'gpt', value: 'assistant message' }),
189+
expect.objectContaining({
190+
from: 'gpt',
191+
value: expect.stringContaining('Function Calling Plugin'),
192+
}),
193+
expect.objectContaining({ from: 'gpt', value: expect.stringContaining('Share from') }), // Footer
194+
],
195+
}),
196+
);
197+
});
85198
});
86199
});

src/store/chat/slices/share/action.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export interface ShareAction {
4747
avatar?: string;
4848
withPluginInfo?: boolean;
4949
withSystemRole?: boolean;
50-
}) => void;
50+
}) => Promise<void>;
5151
}
5252

5353
export const chatShare: StateCreator<ChatStore, [['zustand/devtools', never]], [], ShareAction> = (

0 commit comments

Comments
 (0)