@lobehub/chat 1.85.0 → 1.85.1

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 CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 1.85.1](https://github.com/lobehub/lobe-chat/compare/v1.85.0...v1.85.1)
6
+
7
+ <sup>Released on **2025-05-10**</sup>
8
+
9
+ #### 💄 Styles
10
+
11
+ - **misc**: Add Qwen3 models for infiniai.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### Styles
19
+
20
+ - **misc**: Add Qwen3 models for infiniai, closes [#7657](https://github.com/lobehub/lobe-chat/issues/7657) ([edd1732](https://github.com/lobehub/lobe-chat/commit/edd1732))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
5
30
  ## [Version 1.85.0](https://github.com/lobehub/lobe-chat/compare/v1.84.27...v1.85.0)
6
31
 
7
32
  <sup>Released on **2025-05-09**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "improvements": [
5
+ "Add Qwen3 models for infiniai."
6
+ ]
7
+ },
8
+ "date": "2025-05-10",
9
+ "version": "1.85.1"
10
+ },
2
11
  {
3
12
  "children": {
4
13
  "features": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.85.0",
3
+ "version": "1.85.1",
4
4
  "description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -353,7 +353,7 @@
353
353
  "vitest": "^3.1.1",
354
354
  "vitest-canvas-mock": "^0.3.3"
355
355
  },
356
- "packageManager": "pnpm@9.15.9",
356
+ "packageManager": "pnpm@10.10.0",
357
357
  "publishConfig": {
358
358
  "access": "public",
359
359
  "registry": "https://registry.npmjs.org"
@@ -37,12 +37,16 @@ const infiniaiChatModels: AIChatModelCard[] = [
37
37
  type: 'chat',
38
38
  },
39
39
  {
40
- contextWindowTokens: 65_536,
40
+ abilities: {
41
+ functionCall: true,
42
+ reasoning: true,
43
+ },
44
+ contextWindowTokens: 128_000,
41
45
  description:
42
- 'QwQ 是 Qwen 系列的推理模型,相比传统指令调优模型,QwQ 具备思考和推理能力,在下游任务尤其是难题上能取得显著性能提升。QwQ-32B 是一款中等规模的推理模型,其性能可与最先进的推理模型相媲美,例如 DeepSeek-R1 o1-mini。',
43
- displayName: 'QwQ 32B',
46
+ 'Qwen3-235B-A22B 是 Qwen 系列第三代的大型语言模型,采用混合专家(MoE)架构,总计 2350 亿参数,每 token 激活 220 亿参数。支持无缝切换思考模式(复杂推理)和非思考模式(通用对话),在数学、编码、常识推理及多语言指令执行中表现出色。',
47
+ displayName: 'Qwen3 235B A22B',
44
48
  enabled: true,
45
- id: 'qwq-32b',
49
+ id: 'qwen3-235b-a22b',
46
50
  pricing: {
47
51
  currency: 'CNY',
48
52
  input: 0,
@@ -51,11 +55,70 @@ const infiniaiChatModels: AIChatModelCard[] = [
51
55
  type: 'chat',
52
56
  },
53
57
  {
54
- contextWindowTokens: 32_768,
58
+ abilities: {
59
+ functionCall: true,
60
+ reasoning: true,
61
+ },
62
+ contextWindowTokens: 128_000,
55
63
  description:
56
- 'DeepSeek-R1-Distill-Qwen-32B 是基于 DeepSeek-R1 蒸馏而来的模型,在 Qwen2.5-32B 的基础上使用 DeepSeek-R1 生成的样本进行微调。该模型在各种基准测试中表现出色,保持了强大的推理能力。',
57
- displayName: 'DeepSeek R1 Distill Qwen 32B',
58
- id: 'deepseek-r1-distill-qwen-32b',
64
+ 'Qwen3-30B-A3B 是 Qwen 系列第三代的大型语言模型,采用混合专家(MoE)架构,总计 305 亿参数,每 token 激活 33 亿参数。支持无缝切换思维模式(复杂推理)和非思维模式(通用对话),在数学、编码、常识推理及多语言指令执行中表现出色。',
65
+ displayName: 'Qwen3 30B A3B',
66
+ enabled: true,
67
+ id: 'qwen3-30b-a3b',
68
+ pricing: {
69
+ currency: 'CNY',
70
+ input: 0,
71
+ output: 0,
72
+ },
73
+ type: 'chat',
74
+ },
75
+ {
76
+ abilities: {
77
+ functionCall: true,
78
+ reasoning: true,
79
+ },
80
+ contextWindowTokens: 128_000,
81
+ description:
82
+ 'Qwen3-32B 是 Qwen 系列第三代的大型语言模型,拥有 328 亿参数,专为高效推理和多语言任务设计。支持无缝切换思考模式(复杂推理)和非思考模式(通用对话),在数学、编码、常识推理及多语言指令执行中表现出色。',
83
+ displayName: 'Qwen3 32B',
84
+ enabled: true,
85
+ id: 'qwen3-32b',
86
+ pricing: {
87
+ currency: 'CNY',
88
+ input: 0,
89
+ output: 0,
90
+ },
91
+ type: 'chat',
92
+ },
93
+ {
94
+ abilities: {
95
+ functionCall: true,
96
+ reasoning: true,
97
+ },
98
+ contextWindowTokens: 128_000,
99
+ description:
100
+ 'Qwen3-14B 是 Qwen 系列第三代的大型语言模型,拥有 148 亿参数,专为高效推理和多语言任务设计。支持无缝切换思维模式(复杂推理)和非思维模式(通用对话),在数学、编码、常识推理及多语言指令执行中表现出色。',
101
+ displayName: 'Qwen3 14B',
102
+ enabled: false,
103
+ id: 'qwen3-14b',
104
+ pricing: {
105
+ currency: 'CNY',
106
+ input: 0,
107
+ output: 0,
108
+ },
109
+ type: 'chat',
110
+ },
111
+ {
112
+ abilities: {
113
+ functionCall: true,
114
+ reasoning: true,
115
+ },
116
+ contextWindowTokens: 128_000,
117
+ description:
118
+ 'Qwen3-8B 是 Qwen 系列第三代的大型语言模型,拥有 82 亿参数,专为高效推理和多语言任务设计。支持无缝切换思考模式(复杂推理)和非思考模式(通用对话),在数学、编码、常识推理及多语言指令执行中表现出色。',
119
+ displayName: 'Qwen3 8B',
120
+ enabled: false,
121
+ id: 'qwen3-8b',
59
122
  pricing: {
60
123
  currency: 'CNY',
61
124
  input: 0,
@@ -114,6 +177,9 @@ const infiniaiChatModels: AIChatModelCard[] = [
114
177
  type: 'chat',
115
178
  },
116
179
  {
180
+ abilities: {
181
+ functionCall: true,
182
+ },
117
183
  contextWindowTokens: 32_768,
118
184
  description:
119
185
  'Qwen2.5 是 Qwen 大型语言模型系列的最新成果。Qwen2.5 发布了从 0.5 到 720 亿参数不等的基础语言模型及指令调优语言模型。Qwen2.5 相比 Qwen2 带来了以下改进:\n显著增加知识量,在编程与数学领域的能力得到极大提升。\n在遵循指令、生成长文本、理解结构化数据 (例如,表格) 以及生成结构化输出特别是 JSON 方面有显著提升。对系统提示的多样性更具韧性,增强了聊天机器人中的角色扮演实现和条件设定。\n支持长上下文处理。\n支持超过 29 种语言的多语言功能,包括中文、英语、法语、西班牙语、葡萄牙语、德语、意大利语、俄语、日语、韩语、越南语、泰语、阿拉伯语等。',
@@ -128,6 +194,9 @@ const infiniaiChatModels: AIChatModelCard[] = [
128
194
  type: 'chat',
129
195
  },
130
196
  {
197
+ abilities: {
198
+ functionCall: true,
199
+ },
131
200
  contextWindowTokens: 32_768,
132
201
  description:
133
202
  'Qwen2.5 是 Qwen 大型语言模型系列的最新成果。Qwen2.5 发布了从 0.5 到 720 亿参数不等的基础语言模型及指令调优语言模型。Qwen2.5 相比 Qwen2 带来了以下改进:\n显著增加知识量,在编程与数学领域的能力得到极大提升。\n在遵循指令、生成长文本、理解结构化数据 (例如,表格) 以及生成结构化输出特别是 JSON 方面有显著提升。对系统提示的多样性更具韧性,增强了聊天机器人中的角色扮演实现和条件设定。\n支持长上下文处理。\n支持超过 29 种语言的多语言功能,包括中文、英语、法语、西班牙语、葡萄牙语、德语、意大利语、俄语、日语、韩语、越南语、泰语、阿拉伯语等。',
@@ -168,6 +237,9 @@ const infiniaiChatModels: AIChatModelCard[] = [
168
237
  type: 'chat',
169
238
  },
170
239
  {
240
+ abilities: {
241
+ functionCall: true,
242
+ },
171
243
  contextWindowTokens: 32_768,
172
244
  description:
173
245
  'Qwen2.5 是 Qwen 大型语言模型系列的最新成果。Qwen2.5 发布了从 0.5 到 720 亿参数不等的基础语言模型及指令调优语言模型。Qwen2.5 相比 Qwen2 带来了以下改进:\n显著增加知识量,在编程与数学领域的能力得到极大提升。\n在遵循指令、生成长文本、理解结构化数据 (例如,表格) 以及生成结构化输出特别是 JSON 方面有显著提升。对系统提示的多样性更具韧性,增强了聊天机器人中的角色扮演实现和条件设定。\n支持长上下文处理。\n支持超过 29 种语言的多语言功能,包括中文、英语、法语、西班牙语、葡萄牙语、德语、意大利语、俄语、日语、韩语、越南语、泰语、阿拉伯语等。',
@@ -180,12 +252,44 @@ const infiniaiChatModels: AIChatModelCard[] = [
180
252
  },
181
253
  type: 'chat',
182
254
  },
255
+ {
256
+ abilities: {
257
+ functionCall: true,
258
+ reasoning: true,
259
+ },
260
+ contextWindowTokens: 65_536,
261
+ description:
262
+ 'QwQ 是 Qwen 系列的推理模型,相比传统指令调优模型,QwQ 具备思考和推理能力,在下游任务尤其是难题上能取得显著性能提升。QwQ-32B 是一款中等规模的推理模型,其性能可与最先进的推理模型相媲美,例如 DeepSeek-R1 和 o1-mini。',
263
+ displayName: 'QwQ 32B',
264
+ enabled: false,
265
+ id: 'qwq-32b',
266
+ pricing: {
267
+ currency: 'CNY',
268
+ input: 0,
269
+ output: 0,
270
+ },
271
+ type: 'chat',
272
+ },
273
+ {
274
+ contextWindowTokens: 32_768,
275
+ description:
276
+ 'DeepSeek-R1-Distill-Qwen-32B 是基于 DeepSeek-R1 蒸馏而来的模型,在 Qwen2.5-32B 的基础上使用 DeepSeek-R1 生成的样本进行微调。该模型在各种基准测试中表现出色,保持了强大的推理能力。',
277
+ displayName: 'DeepSeek R1 Distill Qwen 32B',
278
+ enabled: false,
279
+ id: 'deepseek-r1-distill-qwen-32b',
280
+ pricing: {
281
+ currency: 'CNY',
282
+ input: 0,
283
+ output: 0,
284
+ },
285
+ type: 'chat',
286
+ },
183
287
  {
184
288
  contextWindowTokens: 8192,
185
289
  description:
186
290
  'Meta 发布的 LLaMA 3.3 多语言大规模语言模型(LLMs)是一个经过预训练和指令微调的生成模型,提供 70B 规模(文本输入/文本输出)。该模型使用超过 15T 的数据进行训练,支持英语、德语、法语、意大利语、葡萄牙语、印地语、西班牙语和泰语,知识更新截止于 2023 年 12 月。',
187
291
  displayName: 'LLaMA 3.3 70B',
188
- enabled: true,
292
+ enabled: false,
189
293
  id: 'llama-3.3-70b-instruct',
190
294
  pricing: {
191
295
  currency: 'CNY',
@@ -44,7 +44,7 @@ export const LobeInfiniAI = LobeOpenAICompatibleFactory({
44
44
  models: async ({ client }) => {
45
45
  const { LOBE_DEFAULT_MODEL_LIST } = await import('@/config/aiModels');
46
46
 
47
- const reasoningKeywords = ['deepseek-r1', 'qwq'];
47
+ const reasoningKeywords = ['deepseek-r1', 'qwq', 'qwen3'];
48
48
  const visionKeywords = ['qwen2.5-vl'];
49
49
 
50
50
  const modelsPage = (await client.models.list()) as any;
@@ -0,0 +1,213 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { MessageModel } from '@/database/models/message';
4
+ import { FileService } from '@/server/services/file';
5
+ import { ChatMessage, CreateMessageParams } from '@/types/message';
6
+
7
+ vi.mock('@/database/models/message', () => ({
8
+ MessageModel: vi.fn(),
9
+ }));
10
+
11
+ vi.mock('@/server/services/file', () => ({
12
+ FileService: vi.fn(),
13
+ }));
14
+
15
+ vi.mock('@/database/server', () => ({
16
+ getServerDB: vi.fn(),
17
+ }));
18
+
19
+ describe('messageRouter', () => {
20
+ it('should handle batchCreateMessages', async () => {
21
+ const mockBatchCreate = vi.fn().mockResolvedValue({ rowCount: 2 });
22
+ vi.mocked(MessageModel).mockImplementation(
23
+ () =>
24
+ ({
25
+ batchCreate: mockBatchCreate,
26
+ }) as any,
27
+ );
28
+
29
+ const input = [
30
+ {
31
+ id: '1',
32
+ role: 'user',
33
+ content: 'test',
34
+ sessionId: 'session1',
35
+ createdAt: new Date(),
36
+ updatedAt: new Date(),
37
+ agentId: 'agent1',
38
+ clientId: 'client1',
39
+ parentId: null,
40
+ quotaId: null,
41
+ model: null,
42
+ provider: null,
43
+ topicId: null,
44
+ error: null,
45
+ favorite: false,
46
+ observationId: null,
47
+ reasoning: null,
48
+ pluginState: null,
49
+ translate: null,
50
+ tts: null,
51
+ search: null,
52
+ threadId: null,
53
+ tools: null,
54
+ traceId: null,
55
+ userId: 'user1',
56
+ } as any,
57
+ ];
58
+
59
+ const ctx = {
60
+ messageModel: new MessageModel({} as any, 'user1'),
61
+ };
62
+
63
+ const result = await ctx.messageModel.batchCreate(input);
64
+
65
+ expect(mockBatchCreate).toHaveBeenCalledWith(input);
66
+ expect(result.rowCount).toBe(2);
67
+ });
68
+
69
+ it('should handle count', async () => {
70
+ const mockCount = vi.fn().mockResolvedValue(5);
71
+ vi.mocked(MessageModel).mockImplementation(
72
+ () =>
73
+ ({
74
+ count: mockCount,
75
+ }) as any,
76
+ );
77
+
78
+ const input = { startDate: '2024-01-01' };
79
+ const ctx = {
80
+ messageModel: new MessageModel({} as any, 'user1'),
81
+ };
82
+
83
+ const result = await ctx.messageModel.count(input);
84
+
85
+ expect(mockCount).toHaveBeenCalledWith(input);
86
+ expect(result).toBe(5);
87
+ });
88
+
89
+ it('should handle createMessage', async () => {
90
+ const mockCreate = vi.fn().mockResolvedValue({ id: 'msg1' });
91
+ vi.mocked(MessageModel).mockImplementation(
92
+ () =>
93
+ ({
94
+ create: mockCreate,
95
+ }) as any,
96
+ );
97
+
98
+ const input: CreateMessageParams = {
99
+ content: 'test',
100
+ role: 'user',
101
+ sessionId: 'session1',
102
+ };
103
+
104
+ const ctx = {
105
+ messageModel: new MessageModel({} as any, 'user1'),
106
+ };
107
+
108
+ const result = await ctx.messageModel.create(input);
109
+
110
+ expect(mockCreate).toHaveBeenCalledWith(input);
111
+ expect(result.id).toBe('msg1');
112
+ });
113
+
114
+ it('should handle getMessages', async () => {
115
+ const mockQuery = vi.fn().mockResolvedValue([{ id: 'msg1' }]);
116
+ const mockGetFullFileUrl = vi
117
+ .fn()
118
+ .mockImplementation((path: string | null, file: { fileType: string }) => {
119
+ return Promise.resolve('url');
120
+ });
121
+
122
+ vi.mocked(MessageModel).mockImplementation(
123
+ () =>
124
+ ({
125
+ query: mockQuery,
126
+ }) as any,
127
+ );
128
+
129
+ vi.mocked(FileService).mockImplementation(
130
+ () =>
131
+ ({
132
+ getFullFileUrl: mockGetFullFileUrl,
133
+ }) as any,
134
+ );
135
+
136
+ const input = { sessionId: 'session1' };
137
+ const ctx = {
138
+ messageModel: new MessageModel({} as any, 'user1'),
139
+ fileService: new FileService({} as any, 'user1'),
140
+ userId: 'user1',
141
+ };
142
+
143
+ const result = await ctx.messageModel.query(input, {
144
+ postProcessUrl: mockGetFullFileUrl,
145
+ });
146
+
147
+ expect(mockQuery).toHaveBeenCalledWith(input, expect.any(Object));
148
+ expect(result).toEqual([{ id: 'msg1' }]);
149
+ });
150
+
151
+ it('should handle getAllMessages', async () => {
152
+ const mockQueryAll = vi.fn().mockResolvedValue([
153
+ {
154
+ id: 'msg1',
155
+ meta: {},
156
+ } as ChatMessage,
157
+ ]);
158
+ vi.mocked(MessageModel).mockImplementation(
159
+ () =>
160
+ ({
161
+ queryAll: mockQueryAll,
162
+ }) as any,
163
+ );
164
+
165
+ const ctx = {
166
+ messageModel: new MessageModel({} as any, 'user1'),
167
+ };
168
+
169
+ const result = await ctx.messageModel.queryAll();
170
+
171
+ expect(mockQueryAll).toHaveBeenCalled();
172
+ expect(result).toEqual([{ id: 'msg1', meta: {} }]);
173
+ });
174
+
175
+ it('should handle removeMessage', async () => {
176
+ const mockDelete = vi.fn().mockResolvedValue(undefined);
177
+ vi.mocked(MessageModel).mockImplementation(
178
+ () =>
179
+ ({
180
+ deleteMessage: mockDelete,
181
+ }) as any,
182
+ );
183
+
184
+ const input = { id: 'msg1' };
185
+ const ctx = {
186
+ messageModel: new MessageModel({} as any, 'user1'),
187
+ };
188
+
189
+ await ctx.messageModel.deleteMessage(input.id);
190
+
191
+ expect(mockDelete).toHaveBeenCalledWith(input.id);
192
+ });
193
+
194
+ it('should handle updateMessage', async () => {
195
+ const mockUpdate = vi.fn().mockResolvedValue({ success: true });
196
+ vi.mocked(MessageModel).mockImplementation(
197
+ () =>
198
+ ({
199
+ update: mockUpdate,
200
+ }) as any,
201
+ );
202
+
203
+ const input = { id: 'msg1', value: { content: 'updated' } };
204
+ const ctx = {
205
+ messageModel: new MessageModel({} as any, 'user1'),
206
+ };
207
+
208
+ const result = await ctx.messageModel.update(input.id, input.value);
209
+
210
+ expect(mockUpdate).toHaveBeenCalledWith(input.id, input.value);
211
+ expect(result).toEqual({ success: true });
212
+ });
213
+ });
@@ -0,0 +1,115 @@
1
+ import { TRPCError } from '@trpc/server';
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import { FileModel } from '@/database/models/file';
5
+ import { TempFileManager } from '@/server/utils/tempFileManager';
6
+
7
+ import { FileService } from '../index';
8
+
9
+ vi.mock('@/config/db', () => ({
10
+ serverDBEnv: {
11
+ REMOVE_GLOBAL_FILE: false,
12
+ },
13
+ }));
14
+
15
+ vi.mock('../impls', () => ({
16
+ createFileServiceModule: () => ({
17
+ deleteFile: vi.fn(),
18
+ deleteFiles: vi.fn(),
19
+ getFileContent: vi.fn(),
20
+ getFileByteArray: vi.fn(),
21
+ createPreSignedUrl: vi.fn(),
22
+ createPreSignedUrlForPreview: vi.fn(),
23
+ uploadContent: vi.fn(),
24
+ getFullFileUrl: vi.fn(),
25
+ }),
26
+ }));
27
+
28
+ vi.mock('@/database/models/file');
29
+
30
+ vi.mock('@/server/utils/tempFileManager');
31
+
32
+ vi.mock('@/utils/uuid', () => ({
33
+ nanoid: () => 'test-id',
34
+ }));
35
+
36
+ describe('FileService', () => {
37
+ let service: FileService;
38
+ const mockDb = {} as any;
39
+ const mockUserId = 'test-user';
40
+ let mockFileModel: any;
41
+ let mockTempManager: any;
42
+
43
+ beforeEach(() => {
44
+ mockFileModel = {
45
+ findById: vi.fn(),
46
+ delete: vi.fn(),
47
+ };
48
+ mockTempManager = {
49
+ writeTempFile: vi.fn(),
50
+ cleanup: vi.fn(),
51
+ };
52
+ vi.mocked(FileModel).mockImplementation(() => mockFileModel);
53
+ vi.mocked(TempFileManager).mockImplementation(() => mockTempManager);
54
+ service = new FileService(mockDb, mockUserId);
55
+ });
56
+
57
+ afterEach(() => {
58
+ vi.clearAllMocks();
59
+ });
60
+
61
+ describe('downloadFileToLocal', () => {
62
+ const mockFile = {
63
+ id: 'test-file-id',
64
+ name: 'test.txt',
65
+ url: 'test-url',
66
+ };
67
+
68
+ it('should throw error if file not found', async () => {
69
+ mockFileModel.findById.mockResolvedValue(undefined);
70
+
71
+ await expect(service.downloadFileToLocal('test-file-id')).rejects.toThrow(
72
+ new TRPCError({ code: 'BAD_REQUEST', message: 'File not found' }),
73
+ );
74
+ });
75
+
76
+ it('should throw error if file content is empty', async () => {
77
+ mockFileModel.findById.mockResolvedValue(mockFile);
78
+ vi.mocked(service['impl'].getFileByteArray).mockResolvedValue(undefined as any);
79
+
80
+ await expect(service.downloadFileToLocal('test-file-id')).rejects.toThrow(
81
+ new TRPCError({ code: 'BAD_REQUEST', message: 'File content is empty' }),
82
+ );
83
+ });
84
+
85
+ it('should delete file from db and throw error if file not found in storage', async () => {
86
+ mockFileModel.findById.mockResolvedValue(mockFile);
87
+ vi.mocked(service['impl'].getFileByteArray).mockRejectedValue({ Code: 'NoSuchKey' });
88
+
89
+ await expect(service.downloadFileToLocal('test-file-id')).rejects.toThrow(
90
+ new TRPCError({ code: 'BAD_REQUEST', message: 'File not found' }),
91
+ );
92
+
93
+ expect(mockFileModel.delete).toHaveBeenCalledWith('test-file-id', false);
94
+ });
95
+
96
+ it('should successfully download file to local', async () => {
97
+ const mockContent = new Uint8Array([1, 2, 3]);
98
+ const mockFilePath = '/tmp/test.txt';
99
+
100
+ mockFileModel.findById.mockResolvedValue(mockFile);
101
+ vi.mocked(service['impl'].getFileByteArray).mockResolvedValue(mockContent);
102
+ mockTempManager.writeTempFile.mockResolvedValue(mockFilePath);
103
+
104
+ const result = await service.downloadFileToLocal('test-file-id');
105
+
106
+ expect(result).toEqual({
107
+ cleanup: expect.any(Function),
108
+ file: mockFile,
109
+ filePath: mockFilePath,
110
+ });
111
+
112
+ expect(mockTempManager.writeTempFile).toHaveBeenCalledWith(mockContent, mockFile.name);
113
+ });
114
+ });
115
+ });
@@ -0,0 +1,94 @@
1
+ import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { describe, expect, it, vi } from 'vitest';
5
+
6
+ import { TempFileManager } from '../tempFileManager';
7
+
8
+ // Mock node modules
9
+ vi.mock('node:fs');
10
+ vi.mock('node:os');
11
+ vi.mock('node:path', () => ({
12
+ join: (...args: string[]) => args.join('/'),
13
+ default: {
14
+ join: (...args: string[]) => args.join('/'),
15
+ },
16
+ }));
17
+
18
+ describe('TempFileManager', () => {
19
+ const mockTmpDir = '/tmp';
20
+ const mockDirname = 'test-';
21
+ const mockFullTmpDir = '/tmp/test-xyz';
22
+
23
+ beforeEach(() => {
24
+ vi.clearAllMocks();
25
+ vi.mocked(tmpdir).mockReturnValue(mockTmpDir);
26
+ vi.mocked(mkdtempSync).mockReturnValue(mockFullTmpDir);
27
+ vi.mocked(existsSync).mockReturnValue(true);
28
+ });
29
+
30
+ it('should create temp directory on initialization', () => {
31
+ new TempFileManager(mockDirname);
32
+
33
+ expect(tmpdir).toHaveBeenCalled();
34
+ expect(mkdtempSync).toHaveBeenCalledWith(`${mockTmpDir}/${mockDirname}`);
35
+ });
36
+
37
+ it('should write temp file successfully', async () => {
38
+ const manager = new TempFileManager(mockDirname);
39
+ const testData = new Uint8Array([1, 2, 3]);
40
+ const fileName = 'test.txt';
41
+
42
+ const filePath = await manager.writeTempFile(testData, fileName);
43
+
44
+ expect(writeFileSync).toHaveBeenCalledWith(`${mockFullTmpDir}/${fileName}`, testData);
45
+ expect(filePath).toBe(`${mockFullTmpDir}/${fileName}`);
46
+ });
47
+
48
+ it('should cleanup on write failure', async () => {
49
+ const manager = new TempFileManager(mockDirname);
50
+ const testData = new Uint8Array([1, 2, 3]);
51
+ const fileName = 'test.txt';
52
+
53
+ vi.mocked(writeFileSync).mockImplementation(() => {
54
+ throw new Error('Write failed');
55
+ });
56
+
57
+ await expect(manager.writeTempFile(testData, fileName)).rejects.toThrow(
58
+ 'Failed to write temp file: Write failed',
59
+ );
60
+
61
+ expect(existsSync).toHaveBeenCalledWith(mockFullTmpDir);
62
+ expect(rmSync).toHaveBeenCalledWith(mockFullTmpDir, { force: true, recursive: true });
63
+ });
64
+
65
+ it('should cleanup temp directory', () => {
66
+ const manager = new TempFileManager(mockDirname);
67
+ vi.mocked(existsSync).mockReturnValue(true);
68
+
69
+ manager.cleanup();
70
+
71
+ expect(existsSync).toHaveBeenCalledWith(mockFullTmpDir);
72
+ expect(rmSync).toHaveBeenCalledWith(mockFullTmpDir, { force: true, recursive: true });
73
+ });
74
+
75
+ it('should skip cleanup if directory does not exist', () => {
76
+ const manager = new TempFileManager(mockDirname);
77
+ vi.mocked(existsSync).mockReturnValue(false);
78
+
79
+ manager.cleanup();
80
+
81
+ expect(existsSync).toHaveBeenCalledWith(mockFullTmpDir);
82
+ expect(rmSync).not.toHaveBeenCalled();
83
+ });
84
+
85
+ it('should register cleanup hooks on process events', () => {
86
+ const processOnSpy = vi.spyOn(process, 'on');
87
+ new TempFileManager(mockDirname);
88
+
89
+ expect(processOnSpy).toHaveBeenCalledWith('exit', expect.any(Function));
90
+ expect(processOnSpy).toHaveBeenCalledWith('uncaughtException', expect.any(Function));
91
+ expect(processOnSpy).toHaveBeenCalledWith('SIGINT', expect.any(Function));
92
+ expect(processOnSpy).toHaveBeenCalledWith('SIGTERM', expect.any(Function));
93
+ });
94
+ });