Files
atlas-seller-kit/apps/api/test/image-suite.unit.test.ts
T

292 lines
10 KiB
TypeScript

import { randomUUID } from 'node:crypto';
import { describe, expect, it, vi } from 'vitest';
import {
createImageSuiteSchema,
imageSuiteInputSchema,
imageSuitePlanInputSchema,
} from '@atlas/contracts';
import { ImageSuiteService } from '../src/media/image-suite.service.js';
const input = () =>
imageSuiteInputSchema.parse({
variant_id: randomUUID(),
reference_asset_ids: [randomUUID()],
product_name: '台灯',
});
const shot = () => ({
id: randomUUID(),
category: 'white',
type: 'white_background',
theme: '白底主图',
description: '展示商品完整外观',
});
describe('套图共享契约与资源边界', () => {
it('规划默认四张,接受 1–20 张预算,生成接口不接收规划预算字段', () => {
const valid = input();
expect(imageSuitePlanInputSchema.parse(valid).image_count).toBe(4);
expect(
imageSuitePlanInputSchema.parse({ ...valid, image_count: 6 })
.image_count,
).toBe(6);
for (const image_count of [0, 21, 1.2])
expect(
imageSuitePlanInputSchema.safeParse({ ...valid, image_count })
.success,
).toBe(false);
expect(
createImageSuiteSchema.safeParse({
...valid,
image_count: 4,
shots: [shot()],
}).success,
).toBe(false);
});
it('规划保存预算并只创建规划 Outbox,不创建生图子任务', async () => {
const request = imageSuitePlanInputSchema.parse({
...input(),
image_count: 6,
});
const suiteId = randomUUID(),
productId = randomUUID();
const query = vi.fn(async (sql: string, values: unknown[] = []) => {
if (sql.startsWith('SELECT id FROM atlas.product_variants'))
return { rows: [{ id: request.variant_id }] };
if (sql.startsWith('SELECT id FROM atlas.media_assets'))
return {
rows: request.reference_asset_ids.map((id) => ({ id })),
};
if (sql.startsWith('INSERT INTO atlas.product_image_suites'))
return {
rows: [
{
id: suiteId,
product_id: productId,
variant_id: request.variant_id,
purpose: 'plan',
status: 'queued',
input: JSON.parse(values[3] as string),
shots: [],
created_at: new Date(),
completed_at: null,
error_code: null,
},
],
};
return { rows: [] };
});
const service = new ImageSuiteService(
{} as never,
{
run: async (
_context: unknown,
work: (tx: { query: typeof query }) => Promise<unknown>,
) => work({ query }),
} as never,
{} as never,
);
const result = await service.create(
randomUUID(),
randomUUID(),
productId,
randomUUID(),
request,
'plan',
);
expect(result).toMatchObject({
status: 202,
body: {
data: { purpose: 'plan', input: { image_count: 6 }, shots: [] },
},
});
expect(
query.mock.calls.some(([sql]) =>
sql.includes('INSERT INTO atlas.media_ai_edits'),
),
).toBe(false);
expect(
query.mock.calls.find(([sql]) =>
sql.includes('INSERT INTO atlas.outbox_events'),
)?.[1],
).toContain('image_suite.plan.requested');
});
it('拒绝重复底图、超过 8 张底图、超过分类数量和无实物尺寸的尺寸图', () => {
const valid = input();
expect(
imageSuiteInputSchema.safeParse({
...valid,
reference_asset_ids: Array.from({ length: 9 }, randomUUID),
}).success,
).toBe(false);
expect(
imageSuiteInputSchema.safeParse({
...valid,
reference_asset_ids: [
...valid.reference_asset_ids,
...valid.reference_asset_ids,
],
}).success,
).toBe(false);
expect(
createImageSuiteSchema.safeParse({
...valid,
shots: Array.from({ length: 6 }, shot),
}).success,
).toBe(false);
expect(
createImageSuiteSchema.safeParse({
...valid,
shots: [{ ...shot(), category: 'other', type: 'dimensions' }],
}).success,
).toBe(false);
});
it('逐张底图必须来自已选图库,并保留主体图,类型必须匹配分类', () => {
const valid = input();
expect(
createImageSuiteSchema.safeParse({
...valid,
shots: [{ ...shot(), reference_asset_ids: [randomUUID()] }],
}).success,
).toBe(false);
expect(
createImageSuiteSchema.safeParse({
...valid,
shots: [{ ...shot(), category: 'scene' }],
}).success,
).toBe(false);
expect(
createImageSuiteSchema.safeParse({
...valid,
shots: [shot()],
ratio: '4:3',
language: 'zh',
}).success,
).toBe(true);
});
it('目标 SKU 不属于当前商品时,在产生任何任务前拒绝请求', async () => {
const query = vi.fn(async () => ({ rows: [] }));
const run = vi.fn(
async (
_input: unknown,
work: (tx: { query: typeof query }) => Promise<unknown>,
) => work({ query }),
);
const service = new ImageSuiteService(
{} as never,
{ run } as never,
{} as never,
);
await expect(
service.create(
randomUUID(),
randomUUID(),
randomUUID(),
randomUUID(),
input(),
'plan',
),
).rejects.toMatchObject({ status: 404 });
expect(query).toHaveBeenCalledOnce();
});
it('任一底图不可见时拒绝,不产生套图及 Outbox', async () => {
const query = vi.fn(async (sql: string) => ({
rows: sql.includes('atlas.product_variants')
? [{ id: 'variant' }]
: [],
}));
const run = vi.fn(
async (
_input: unknown,
work: (tx: { query: typeof query }) => Promise<unknown>,
) => work({ query }),
);
const service = new ImageSuiteService(
{} as never,
{ run } as never,
{} as never,
);
await expect(
service.create(
randomUUID(),
randomUUID(),
randomUUID(),
randomUUID(),
input(),
'plan',
),
).rejects.toMatchObject({ status: 404 });
expect(query).toHaveBeenCalledTimes(2);
});
it('默认逐张用图保留主体,每张最多 3 张,整套轮换覆盖已选 8 张底图', async () => {
const request = createImageSuiteSchema.parse({
...input(),
reference_asset_ids: Array.from({ length: 8 }, randomUUID),
shots: Array.from({ length: 8 }, (_, index) => ({
...shot(),
category: ['white', 'scene', 'selling', 'other'][index % 4],
type: ['white_background', 'lifestyle', 'detail', 'custom'][
index % 4
],
})),
});
const used: string[][] = [];
const query = vi.fn(async (sql: string, values: unknown[] = []) => {
if (sql.startsWith('SELECT id FROM atlas.product_variants'))
return { rows: [{ id: request.variant_id }] };
if (sql.startsWith('SELECT id FROM atlas.media_assets'))
return {
rows: request.reference_asset_ids.map((id) => ({ id })),
};
if (sql.includes('INSERT INTO atlas.product_image_suites'))
return {
rows: [
{
id: randomUUID(),
product_id: randomUUID(),
variant_id: request.variant_id,
purpose: 'generate',
status: 'queued',
input: request,
shots: request.shots,
created_at: new Date(),
completed_at: null,
error_code: null,
},
],
};
if (sql.includes('INSERT INTO atlas.media_ai_edits'))
used.push([values[5] as string, ...(values[6] as string[])]);
return { rows: [] };
});
const service = new ImageSuiteService(
{} as never,
{
run: async (
_request: unknown,
work: (tx: { query: typeof query }) => Promise<unknown>,
) => work({ query }),
} as never,
{} as never,
);
await service.create(
randomUUID(),
randomUUID(),
randomUUID(),
randomUUID(),
request,
'generate',
);
expect(used).toHaveLength(8);
expect(
used.every(
(refs) =>
refs.length <= 3 &&
refs[0] === request.reference_asset_ids[0],
),
).toBe(true);
expect(new Set(used.flat())).toEqual(
new Set(request.reference_asset_ids),
);
});
});