102 lines
6.3 KiB
TypeScript
102 lines
6.3 KiB
TypeScript
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<unknown>) => work({ query }));
|
|
const service = new MediaAiEditService(
|
|
{} as ConstructorParameters<typeof MediaAiEditService>[0],
|
|
{ run } as unknown as ConstructorParameters<typeof MediaAiEditService>[1],
|
|
{} as ConstructorParameters<typeof MediaAiEditService>[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<unknown>) => work({ query }));
|
|
const service = new MediaAiEditService(
|
|
{} as ConstructorParameters<typeof MediaAiEditService>[0],
|
|
{ run } as unknown as ConstructorParameters<typeof MediaAiEditService>[1],
|
|
{} as ConstructorParameters<typeof MediaAiEditService>[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<unknown>) => work({ query })) };
|
|
const service = new MediaAiEditService(
|
|
database as unknown as ConstructorParameters<typeof MediaAiEditService>[0],
|
|
{} as ConstructorParameters<typeof MediaAiEditService>[1],
|
|
{} as ConstructorParameters<typeof MediaAiEditService>[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<unknown>) => 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<typeof MediaAiEditService>[0],
|
|
{} as ConstructorParameters<typeof MediaAiEditService>[1],
|
|
{ accessUrl } as unknown as ConstructorParameters<typeof MediaAiEditService>[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);
|
|
});
|
|
});
|