import { describe, expect, it, vi } from 'vitest'; import { createMediaAiEditRequestSchema } from '@atlas/contracts'; import { MediaAiEditService } from '../src/media/media-ai-edit.service.js'; const workspaceId = '30cf54b7-06a3-4e5f-a5fc-d74165115094'; const sourceId = '664d0d73-3d39-4a98-ac0a-5c0e521d8998'; const referenceId = '50ea00b7-6675-4cc7-b7a2-d45f05c07100'; const editId = '68db226f-3fcb-4579-ab9e-4932b1d84067'; const productId = '733d0d73-3d39-4a98-ac0a-5c0e521d8998'; describe('单张 AI 编辑任务', () => { it('验证主图和辅图都属于当前工作空间后,原子写入任务、进度和 Outbox', async () => { const statements: string[] = []; const query = vi.fn(async (sql: string) => { statements.push(sql); if (sql.includes('SELECT id FROM atlas.products')) return { rows: [{ id: productId }] }; if (sql.includes('SELECT id FROM atlas.media_assets')) return { rows: [{ id: sourceId }, { id: referenceId }] }; if (sql.includes('RETURNING id')) return { rows: [{ id: editId }] }; return { rows: [] }; }); const run = vi.fn(async (_input: unknown, work: (tx: { query: typeof query }) => Promise) => work({ query })); const service = new MediaAiEditService( {} as ConstructorParameters[0], { run } as unknown as ConstructorParameters[1], {} as ConstructorParameters[2], ); const input = createMediaAiEditRequestSchema.parse({ product_id: productId, reference_asset_ids: [referenceId], background: 'reference' }); const result = await service.create(workspaceId, 'user-id', sourceId, 'key', input); expect(result).toMatchObject({ status: 202, body: { data: { id: editId, status: 'queued' } } }); expect(run).toHaveBeenCalledOnce(); expect(statements.some((sql) => sql.includes('INSERT INTO atlas.media_ai_edits'))).toBe(true); expect(statements.some((sql) => sql.includes('INSERT INTO atlas.operations'))).toBe(true); expect(statements.some((sql) => sql.includes('INSERT INTO atlas.outbox_events'))).toBe(true); }); it('任一辅助图不在当前工作空间时,不创建任务', async () => { const query = vi.fn(async (sql: string) => ({ rows: [{ id: sql.includes('atlas.products') ? productId : sourceId }] })); const run = vi.fn(async (_input: unknown, work: (tx: { query: typeof query }) => Promise) => work({ query })); const service = new MediaAiEditService( {} as ConstructorParameters[0], { run } as unknown as ConstructorParameters[1], {} as ConstructorParameters[2], ); const input = createMediaAiEditRequestSchema.parse({ product_id: productId, reference_asset_ids: [referenceId] }); await expect(service.create(workspaceId, 'user-id', sourceId, 'key', input)) .rejects.toMatchObject({ status: 404 }); expect(query).toHaveBeenCalledTimes(2); }); it('仅扩图不能带辅图或尺寸标注,参考背景必须带辅图', () => { expect(createMediaAiEditRequestSchema.safeParse({ product_id: productId, mode: 'outpaint', reference_asset_ids: [referenceId] }).success).toBe(false); expect(createMediaAiEditRequestSchema.safeParse({ product_id: productId, background: 'reference' }).success).toBe(false); expect(createMediaAiEditRequestSchema.safeParse({ product_id: productId, generate_size_chart: true }).success).toBe(false); }); it('读取失败任务时返回记录的失败来源、步骤和诊断', async () => { const query = vi.fn(async (sql: string) => { expect(sql).toContain('o.workspace_id = e.workspace_id'); return { rows: [{ id: editId, status: 'failed', source_asset_id: sourceId, output_asset_id: null, error_code: 'AI_IMAGE_GENERATION_FAILED', failure_source: 'atlas', failure_stage: 'result_processing', failure_detail: 'Pcore 结果图片无效', }] }; }); const database = { workspaceTransaction: vi.fn(async (_workspace: string, work: (tx: { query: typeof query }) => Promise) => work({ query })) }; const service = new MediaAiEditService( database as unknown as ConstructorParameters[0], {} as ConstructorParameters[1], {} as ConstructorParameters[2], ); const result = await service.read(workspaceId, editId); expect(result).toMatchObject({ failure_source: 'atlas', failure_stage: 'result_processing', failure_detail: 'Pcore 结果图片无效', }); }); it('AI 生图记录按当前商品筛选并重新签发短时预览地址', async () => { const query = vi.fn(async (_sql: string, values: unknown[]) => { expect(values).toEqual([productId]); return { rows: [{ id: editId, status: 'succeeded', source_asset_id: sourceId, output_asset_id: referenceId, error_code: null, content_type: 'image/jpeg', byte_size: '1234', width_px: 1152, height_px: 1536, asset_created_at: new Date('2026-09-26T00:00:00Z'), }] }; }); const database = { workspaceTransaction: vi.fn(async (_workspace: string, work: (tx: { query: typeof query }) => Promise) => work({ query })) }; const accessUrl = vi.fn(async () => ({ url: 'https://example.com/short-lived', expires_at: '2026-09-26T01:00:00Z' })); const service = new MediaAiEditService( database as unknown as ConstructorParameters[0], {} as ConstructorParameters[1], { accessUrl } as unknown as ConstructorParameters[2], ); const items = await service.list(workspaceId, productId); expect(items).toMatchObject([{ id: editId, output_asset: { id: referenceId, preview_url: 'https://example.com/short-lived' } }]); expect(accessUrl).toHaveBeenCalledWith(workspaceId, referenceId); }); });