feat(ai-image): AI生图开发
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { ProductImageTaskService } from '../src/media/product-image-task.service.js';
|
||||
|
||||
const workspaceId = '30cf54b7-06a3-4e5f-a5fc-d74165115094';
|
||||
const productId = '733d0d73-3d39-4a98-ac0a-5c0e521d8998';
|
||||
|
||||
describe('商品图片任务列表', () => {
|
||||
it('按工作空间和商品读取任务,结果只包含可公开的任务字段', async () => {
|
||||
const query = vi.fn(async (sql: string, values: unknown[]) => {
|
||||
expect(values[0]).toBe(productId);
|
||||
if (sql.includes('atlas.products')) return { rows: [{ id: productId }] };
|
||||
expect(values).toEqual([productId, 3]);
|
||||
return { rows: [{
|
||||
id: 'edit-id', status: 'running', provider_task_id: 'private-provider-id',
|
||||
output_asset_id: null, error_code: null,
|
||||
created_at: new Date('2026-09-26T03:00:00Z'), completed_at: null,
|
||||
}, {
|
||||
id: 'completed-id', status: 'succeeded', provider_task_id: 'private-provider-id',
|
||||
output_asset_id: 'asset-id', error_code: null,
|
||||
created_at: new Date('2026-09-26T02:00:00Z'), completed_at: new Date('2026-09-26T02:05:00Z'),
|
||||
}] };
|
||||
});
|
||||
const workspaceTransaction = vi.fn(async (_workspace: string, work: (tx: { query: typeof query }) => Promise<unknown>) => work({ query }));
|
||||
const service = new ProductImageTaskService(
|
||||
{ workspaceTransaction } as unknown as ConstructorParameters<typeof ProductImageTaskService>[0],
|
||||
);
|
||||
const result = await service.list(workspaceId, productId, { limit: 2 });
|
||||
expect(workspaceTransaction).toHaveBeenCalledWith(workspaceId, expect.any(Function));
|
||||
expect(result.meta).toEqual({ has_more: false, next_cursor: null });
|
||||
expect(result.data).toEqual([{
|
||||
id: 'edit-id', kind: 'single_ai_edit', status: 'running', phase: 'waiting_provider',
|
||||
total: 1, completed: 0, output_asset_ids: [], error_code: null, provider_error_raw: null,
|
||||
failure_source: null, failure_stage: null, failure_detail: null,
|
||||
created_at: '2026-09-26T03:00:00.000Z', completed_at: null,
|
||||
}, {
|
||||
id: 'completed-id', kind: 'single_ai_edit', status: 'succeeded', phase: null,
|
||||
total: 1, completed: 1, output_asset_ids: ['asset-id'], error_code: null, provider_error_raw: null,
|
||||
failure_source: null, failure_stage: null, failure_detail: null,
|
||||
created_at: '2026-09-26T02:00:00.000Z', completed_at: '2026-09-26T02:05:00.000Z',
|
||||
}]);
|
||||
expect(JSON.stringify(result)).not.toContain('private-provider-id');
|
||||
});
|
||||
|
||||
it('失败任务返回 Pcore 原始错误,不暴露供应商任务 ID', async () => {
|
||||
const raw = '{"message":"请求无法用于生成图像","type":"image_task_error","code":"failed"}';
|
||||
const query = vi.fn(async (sql: string) => {
|
||||
if (sql.includes('atlas.products')) return { rows: [{ id: productId }] };
|
||||
expect(sql).toContain('o.workspace_id = e.workspace_id');
|
||||
return { rows: [{
|
||||
id: 'failed-id', status: 'failed', provider_task_id: 'private-provider-id',
|
||||
output_asset_id: null, error_code: 'AI_IMAGE_PROVIDER_FAILED', provider_error_raw: raw,
|
||||
failure_source: 'pcore', failure_stage: 'provider_polling', failure_detail: 'Pcore 图片任务失败',
|
||||
created_at: new Date('2026-09-27T03:00:00Z'), completed_at: new Date('2026-09-27T03:01:00Z'),
|
||||
}] };
|
||||
});
|
||||
const workspaceTransaction = vi.fn(async (_workspace: string, work: (tx: { query: typeof query }) => Promise<unknown>) => work({ query }));
|
||||
const service = new ProductImageTaskService(
|
||||
{ workspaceTransaction } as unknown as ConstructorParameters<typeof ProductImageTaskService>[0],
|
||||
);
|
||||
const result = await service.list(workspaceId, productId, { limit: 20 });
|
||||
expect(result.data[0].provider_error_raw).toBe(raw);
|
||||
expect(result.data[0]).toMatchObject({
|
||||
failure_source: 'pcore', failure_stage: 'provider_polling', failure_detail: 'Pcore 图片任务失败',
|
||||
});
|
||||
expect(JSON.stringify(result)).not.toContain('private-provider-id');
|
||||
});
|
||||
|
||||
it('商品不属于当前工作空间时返回 404,不查询任务', async () => {
|
||||
const query = vi.fn(async () => ({ rows: [] }));
|
||||
const workspaceTransaction = vi.fn(async (_workspace: string, work: (tx: { query: typeof query }) => Promise<unknown>) => work({ query }));
|
||||
const service = new ProductImageTaskService(
|
||||
{ workspaceTransaction } as unknown as ConstructorParameters<typeof ProductImageTaskService>[0],
|
||||
);
|
||||
await expect(service.list(workspaceId, productId, { limit: 20 })).rejects.toMatchObject({ status: 404 });
|
||||
expect(query).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('超过页大小时返回游标,并将游标作为下一页的稳定排序条件', async () => {
|
||||
const id = '68db226f-3fcb-4579-ab9e-4932b1d84067';
|
||||
const rows = [0, 1, 2].map((index) => ({
|
||||
id, status: 'succeeded', provider_task_id: null, output_asset_id: null,
|
||||
error_code: null, created_at: new Date(`2026-09-26T0${3 - index}:00:00Z`),
|
||||
completed_at: null,
|
||||
}));
|
||||
const query = vi.fn(async (sql: string, _values?: unknown[]) => ({ rows: sql.includes('atlas.products') ? [{ id: productId }] : rows }));
|
||||
const workspaceTransaction = vi.fn(async (_workspace: string, work: (tx: { query: typeof query }) => Promise<unknown>) => work({ query }));
|
||||
const service = new ProductImageTaskService(
|
||||
{ workspaceTransaction } as unknown as ConstructorParameters<typeof ProductImageTaskService>[0],
|
||||
);
|
||||
const first = await service.list(workspaceId, productId, { limit: 2 });
|
||||
expect(first.meta.has_more).toBe(true);
|
||||
expect(first.meta.next_cursor).toEqual(expect.any(String));
|
||||
await service.list(workspaceId, productId, { limit: 2, cursor: first.meta.next_cursor! });
|
||||
const nextQuery = query.mock.calls.at(-1)!;
|
||||
expect(nextQuery[0]).toContain('e.created_at, e.id)');
|
||||
expect(nextQuery[1]).toEqual([productId, 0, '2026-09-26T02:00:00.000Z', id, 3]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user