返回 AiToEarn
media.mcp.spec.ts
根目录 / project / aitoearn-backend / apps / aitoearn-ai / src / core / agent / mcp / media.mcp.spec.ts
1 import type { AiAvailabilityService } from '../../ai-availability'
2 import { Logger } from '@nestjs/common'
3 import { UserType } from '@yikart/common'
4 import { AiLogStatus } from '@yikart/mongodb'
5 import { vi } from 'vitest'
6 import { ImageService } from '../../ai/image'
7 import { GrokVideoService, OpenAIVideoService } from '../../ai/video'
8 import { MediaMcp, MediaToolName } from './media.mcp'
9
10 describe('mediaMcp', () => {
11 let mediaMcp: MediaMcp
12 let mockLogger: Logger
13 let mockOpenaiVideoService: vi.Mocked<OpenAIVideoService>
14 let mockImageService: vi.Mocked<ImageService>
15 let mockAiAvailability: vi.Mocked<Pick<AiAvailabilityService, 'execute'>>
16 let mockGrokVideoService: vi.Mocked<GrokVideoService>
17
18 const userId = 'test-user-id'
19 const userType = UserType.User
20
21 beforeEach(() => {
22 mockLogger = {
23 debug: vi.fn(),
24 error: vi.fn(),
25 fatal: vi.fn(),
26 } as unknown as Logger
27
28 mockOpenaiVideoService = {
29 createVideo: vi.fn(),
30 getVideo: vi.fn(),
31 createCharacter: vi.fn(),
32 getCharacter: vi.fn(),
33 } as unknown as vi.Mocked<OpenAIVideoService>
34
35 mockImageService = {
36 userGeminiGeneration: vi.fn(),
37 } as unknown as vi.Mocked<ImageService>
38
39 mockAiAvailability = {
40 execute: vi.fn().mockImplementation((_ctx: unknown, fn: () => unknown) => (fn as () => Promise<unknown>)()),
41 } as unknown as vi.Mocked<Pick<AiAvailabilityService, 'execute'>>
42
43 mockGrokVideoService = {} as vi.Mocked<GrokVideoService>
44
45 mediaMcp = new MediaMcp(
46 mockOpenaiVideoService,
47 mockImageService,
48 mockAiAvailability as unknown as AiAvailabilityService,
49 mockGrokVideoService,
50 )
51 // Override the logger for testing
52 Object.defineProperty(mediaMcp, 'logger', { value: mockLogger })
53 })
54
55 describe('createGenerateImageTool', () => {
56 it('should have correct tool name', () => {
57 const tool = mediaMcp.createGenerateImageTool(userId, userType)
58 expect(tool.name).toBe(MediaToolName.GenerateImage)
59 })
60
61 it('should call imageService.userGeminiGeneration with correct params', async () => {
62 mockImageService.userGeminiGeneration.mockResolvedValue({
63 usage: { input_tokens: 100, output_tokens: 200, total_tokens: 300 },
64 images: [{ url: 'https://example.com/image1.png', data: '', mimeType: 'image/png' }],
65 })
66
67 const tool = mediaMcp.createGenerateImageTool(userId, userType)
68 await tool.handler({
69 prompt: 'A cute cat',
70 imageUrls: ['https://example.com/ref.png'],
71 imageSize: '2K',
72 aspectRatio: '16:9',
73 } as never, {})
74
75 expect(mockImageService.userGeminiGeneration).toHaveBeenCalledWith({
76 userId,
77 userType,
78 prompt: 'A cute cat',
79 imageUrls: ['https://example.com/ref.png'],
80 imageSize: '2K',
81 aspectRatio: '16:9',
82 })
83 })
84
85 it('should pass selected gemini image model when provided', async () => {
86 mockImageService.userGeminiGeneration.mockResolvedValue({
87 usage: { input_tokens: 100, output_tokens: 200, total_tokens: 300 },
88 images: [{ url: 'https://example.com/image1.png', data: '', mimeType: 'image/png' }],
89 })
90
91 const tool = mediaMcp.createGenerateImageTool(userId, userType)
92 await tool.handler({
93 prompt: 'A cute cat',
94 model: 'gemini-3-pro-image-preview',
95 } as never, {})
96
97 expect(mockImageService.userGeminiGeneration).toHaveBeenCalledWith(
98 expect.objectContaining({
99 model: 'gemini-3-pro-image-preview',
100 }),
101 )
102 })
103
104 it('should return generated image URLs', async () => {
105 mockImageService.userGeminiGeneration.mockResolvedValue({
106 usage: { input_tokens: 100, output_tokens: 200, total_tokens: 300 },
107 images: [
108 { url: 'image1.png', data: '', mimeType: 'image/png' },
109 { url: 'image2.png', data: '', mimeType: 'image/png' },
110 ],
111 })
112
113 const tool = mediaMcp.createGenerateImageTool(userId, userType)
114 const result = await tool.handler({
115 prompt: 'A cute cat',
116 imageUrls: [],
117 imageSize: undefined,
118 aspectRatio: undefined,
119 } as never, {})
120
121 expect(result.isError).toBeUndefined()
122 expect(result.content).toBeDefined()
123 expect(result.content.length).toBeGreaterThan(0)
124
125 // Should contain resource_link entries
126 const resourceLinks = result.content.filter(c => c.type === 'resource_link')
127 expect(resourceLinks.length).toBe(2)
128 })
129
130 it('should use default empty array for imageUrls when not provided', async () => {
131 mockImageService.userGeminiGeneration.mockResolvedValue({
132 usage: { input_tokens: 100, output_tokens: 200, total_tokens: 300 },
133 images: [{ url: 'image.png', data: '', mimeType: 'image/png' }],
134 })
135
136 const tool = mediaMcp.createGenerateImageTool(userId, userType)
137 await tool.handler({
138 prompt: 'A cute cat',
139 imageUrls: [],
140 imageSize: undefined,
141 aspectRatio: undefined,
142 } as never, {})
143
144 expect(mockImageService.userGeminiGeneration).toHaveBeenCalledWith(
145 expect.objectContaining({
146 imageUrls: [],
147 }),
148 )
149 })
150 })
151
152 describe('createGenerateVideoTool', () => {
153 it('should have correct tool name', () => {
154 const tool = mediaMcp.createGenerateVideoTool(userId, userType)
155 expect(tool.name).toBe(MediaToolName.GenerateVideo)
156 })
157
158 it('should call openaiVideoService.createVideo with correct params', async () => {
159 mockOpenaiVideoService.createVideo.mockResolvedValue({
160 id: 'task-123',
161 status: 'in_progress',
162 } as never)
163
164 const tool = mediaMcp.createGenerateVideoTool(userId, userType)
165 await tool.handler({
166 prompt: 'A cat walking',
167 model: 'sora-2',
168 input_reference: undefined,
169 seconds: undefined,
170 size: undefined,
171 } as never, {})
172
173 expect(mockOpenaiVideoService.createVideo).toHaveBeenCalledWith({
174 userId,
175 userType,
176 prompt: 'A cat walking',
177 input_reference: undefined,
178 model: 'sora-2',
179 seconds: '10',
180 size: '720x1280',
181 })
182 })
183
184 it('should use sora-2-pro defaults when model is sora-2-pro', async () => {
185 mockOpenaiVideoService.createVideo.mockResolvedValue({
186 id: 'task-123',
187 status: 'in_progress',
188 } as never)
189
190 const tool = mediaMcp.createGenerateVideoTool(userId, userType)
191 await tool.handler({
192 prompt: 'A cat walking',
193 model: 'sora-2-pro',
194 input_reference: undefined,
195 seconds: undefined,
196 size: undefined,
197 } as never, {})
198
199 expect(mockOpenaiVideoService.createVideo).toHaveBeenCalledWith(
200 expect.objectContaining({
201 seconds: '25',
202 size: '1024x1792',
203 }),
204 )
205 })
206
207 it('should return success result with task id', async () => {
208 mockOpenaiVideoService.createVideo.mockResolvedValue({
209 id: 'task-123',
210 status: 'in_progress',
211 } as never)
212
213 const tool = mediaMcp.createGenerateVideoTool(userId, userType)
214 const result = await tool.handler({
215 prompt: 'A cat walking',
216 model: 'sora-2',
217 input_reference: undefined,
218 seconds: undefined,
219 size: undefined,
220 } as never, {})
221
222 expect(result.isError).toBeUndefined()
223 const textContent = result.content[0] as { type: 'text', text: string }
224 expect(textContent.text).toContain('task-123')
225 })
226
227 it('should return error result when video generation fails', async () => {
228 mockOpenaiVideoService.createVideo.mockResolvedValue({
229 id: 'task-123',
230 status: AiLogStatus.Failed,
231 error: { code: 'policy_violation', message: 'Content policy violation' },
232 } as never)
233
234 const tool = mediaMcp.createGenerateVideoTool(userId, userType)
235 const result = await tool.handler({
236 prompt: 'A cat walking',
237 model: 'sora-2',
238 input_reference: undefined,
239 seconds: undefined,
240 size: undefined,
241 } as never, {})
242
243 expect(result.isError).toBe(true)
244 const textContent = result.content[0] as { type: 'text', text: string }
245 expect(textContent.text).toContain('Failed')
246 })
247 })
248
249 describe('createGetVideoStatusTool', () => {
250 it('should have correct tool name', () => {
251 const tool = mediaMcp.createGetVideoStatusTool(userId, userType)
252 expect(tool.name).toBe(MediaToolName.GetVideoStatus)
253 })
254
255 it('should return completed status with video URL', async () => {
256 mockOpenaiVideoService.getVideo.mockResolvedValue({
257 id: 'task-123',
258 object: 'video',
259 model: 'sora-2',
260 prompt: 'test',
261 status: 'completed',
262 url: 'video.mp4',
263 progress: 100,
264 created_at: Math.floor(Date.now() / 1000) - 60,
265 completed_at: Math.floor(Date.now() / 1000),
266 expires_at: null,
267 error: null,
268 remixed_from_video_id: null,
269 seconds: '10',
270 size: '720x1280',
271 })
272
273 const tool = mediaMcp.createGetVideoStatusTool(userId, userType)
274 const result = await tool.handler({ taskId: 'task-123' }, {})
275
276 expect(result.isError).toBeUndefined()
277 const textContent = result.content[0] as { type: 'text', text: string }
278 expect(textContent.text).toContain('completed')
279 expect(textContent.text).toContain('video.mp4')
280 })
281
282 it('should return failed status with error message', async () => {
283 mockOpenaiVideoService.getVideo.mockResolvedValue({
284 id: 'task-123',
285 object: 'video',
286 model: 'sora-2',
287 prompt: 'test',
288 status: 'failed',
289 progress: 0,
290 error: { code: 'error', message: 'Processing error' },
291 created_at: Math.floor(Date.now() / 1000) - 60,
292 completed_at: null,
293 expires_at: null,
294 remixed_from_video_id: null,
295 seconds: '10',
296 size: '720x1280',
297 })
298
299 const tool = mediaMcp.createGetVideoStatusTool(userId, userType)
300 const result = await tool.handler({ taskId: 'task-123' }, {})
301
302 expect(result.isError).toBe(true)
303 const textContent = result.content[0] as { type: 'text', text: string }
304 expect(textContent.text).toContain('failed')
305 expect(textContent.text).toContain('Processing error')
306 })
307
308 it('should return progress status when still processing', async () => {
309 mockOpenaiVideoService.getVideo.mockResolvedValue({
310 id: 'task-123',
311 object: 'video',
312 model: 'sora-2',
313 prompt: 'test',
314 status: 'in_progress',
315 progress: 50,
316 created_at: Math.floor(Date.now() / 1000) - 30,
317 completed_at: null,
318 expires_at: null,
319 error: null,
320 remixed_from_video_id: null,
321 seconds: '10',
322 size: '720x1280',
323 })
324
325 const tool = mediaMcp.createGetVideoStatusTool(userId, userType)
326 const result = await tool.handler({ taskId: 'task-123' }, {})
327
328 expect(result.isError).toBeUndefined()
329 const textContent = result.content[0] as { type: 'text', text: string }
330 expect(textContent.text).toContain('in_progress')
331 expect(textContent.text).toContain('50%')
332 })
333 })
334
335 describe('createSoraCharacterTool', () => {
336 it('should have correct tool name', () => {
337 const tool = mediaMcp.createSoraCharacterTool(userId, userType)
338 expect(tool.name).toBe(MediaToolName.CreateSoraCharacter)
339 })
340
341 it('should call openaiVideoService.createCharacter with correct params', async () => {
342 mockOpenaiVideoService.createCharacter.mockResolvedValue({
343 id: 'char-123',
344 object: 'character',
345 model: 'sora-2-character',
346 username: 'testchar',
347 status: 'processing',
348 created_at: Math.floor(Date.now() / 1000),
349 })
350
351 const tool = mediaMcp.createSoraCharacterTool(userId, userType)
352 await tool.handler({
353 prompt: 'A young woman',
354 videoUrl: 'https://example.com/video.mp4',
355 taskId: undefined,
356 timestamps: '1,3',
357 } as never, {})
358
359 expect(mockOpenaiVideoService.createCharacter).toHaveBeenCalledWith({
360 userId,
361 userType,
362 prompt: 'A young woman',
363 videoUrl: 'https://example.com/video.mp4',
364 taskId: undefined,
365 timestamps: '1,3',
366 })
367 })
368
369 it('should return success result with character id and username', async () => {
370 mockOpenaiVideoService.createCharacter.mockResolvedValue({
371 id: 'char-123',
372 object: 'character',
373 model: 'sora-2-character',
374 username: 'testchar',
375 status: 'processing',
376 created_at: Math.floor(Date.now() / 1000),
377 })
378
379 const tool = mediaMcp.createSoraCharacterTool(userId, userType)
380 const result = await tool.handler({
381 prompt: 'A young woman',
382 videoUrl: undefined,
383 taskId: undefined,
384 timestamps: '1,3',
385 } as never, {})
386
387 expect(result.isError).toBeUndefined()
388 const textContent = result.content[0] as { type: 'text', text: string }
389 expect(textContent.text).toContain('char-123')
390 expect(textContent.text).toContain('@testchar')
391 })
392
393 it('should return error when character creation fails', async () => {
394 mockOpenaiVideoService.createCharacter.mockResolvedValue({
395 id: 'char-123',
396 object: 'character',
397 model: 'sora-2-character',
398 username: 'testchar',
399 status: 'failed',
400 created_at: Math.floor(Date.now() / 1000),
401 error: { code: 400, message: 'Invalid video' },
402 })
403
404 const tool = mediaMcp.createSoraCharacterTool(userId, userType)
405 const result = await tool.handler({
406 prompt: 'A young woman',
407 videoUrl: undefined,
408 taskId: undefined,
409 timestamps: '1,3',
410 } as never, {})
411
412 expect(result.isError).toBe(true)
413 const textContent = result.content[0] as { type: 'text', text: string }
414 expect(textContent.text).toContain('Failed')
415 })
416 })
417
418 describe('createGetSoraCharacterTool', () => {
419 it('should have correct tool name', () => {
420 const tool = mediaMcp.createGetSoraCharacterTool(userId, userType)
421 expect(tool.name).toBe(MediaToolName.GetSoraCharacter)
422 })
423
424 it('should return completed status with username', async () => {
425 mockOpenaiVideoService.getCharacter.mockResolvedValue({
426 id: 'char-123',
427 object: 'character',
428 model: 'sora-2-character',
429 username: 'testchar',
430 status: 'completed',
431 created_at: Math.floor(Date.now() / 1000),
432 })
433
434 const tool = mediaMcp.createGetSoraCharacterTool(userId, userType)
435 const result = await tool.handler({ characterId: 'char-123' }, {})
436
437 expect(result.isError).toBeUndefined()
438 const textContent = result.content[0] as { type: 'text', text: string }
439 expect(textContent.text).toContain('ready')
440 expect(textContent.text).toContain('@testchar')
441 })
442
443 it('should return failed status with error', async () => {
444 mockOpenaiVideoService.getCharacter.mockResolvedValue({
445 id: 'char-123',
446 object: 'character',
447 model: 'sora-2-character',
448 username: 'testchar',
449 status: 'failed',
450 created_at: Math.floor(Date.now() / 1000),
451 error: { code: 500, message: 'Processing failed' },
452 })
453
454 const tool = mediaMcp.createGetSoraCharacterTool(userId, userType)
455 const result = await tool.handler({ characterId: 'char-123' }, {})
456
457 expect(result.isError).toBe(true)
458 const textContent = result.content[0] as { type: 'text', text: string }
459 expect(textContent.text).toContain('failed')
460 })
461
462 it('should return processing status', async () => {
463 mockOpenaiVideoService.getCharacter.mockResolvedValue({
464 id: 'char-123',
465 object: 'character',
466 model: 'sora-2-character',
467 username: 'testchar',
468 status: 'processing',
469 created_at: Math.floor(Date.now() / 1000),
470 })
471
472 const tool = mediaMcp.createGetSoraCharacterTool(userId, userType)
473 const result = await tool.handler({ characterId: 'char-123' }, {})
474
475 expect(result.isError).toBeUndefined()
476 const textContent = result.content[0] as { type: 'text', text: string }
477 expect(textContent.text).toContain('processing')
478 })
479 })
480
481 describe('createServer', () => {
482 it('should create server with correct name', () => {
483 const server = mediaMcp.createServer(userId, userType)
484 expect(server.name).toBe('mediaGeneration')
485 })
486
487 it('should include expected tools', () => {
488 const server = mediaMcp.createServer(userId, userType) as { tools?: Array<{ name: string }> }
489 const toolNames = server.tools?.map(t => t.name)
490
491 expect(toolNames).toContain(MediaToolName.GenerateImage)
492 expect(toolNames).toContain(MediaToolName.GenerateVideoWithGrok)
493 expect(toolNames).toContain(MediaToolName.GetGrokVideoStatus)
494 })
495 })
496 })
497
497 lines TYPESCRIPT