import { describe, expect, it, vi } from 'vitest'; import { PRODUCT_AI_HINT_MAX_LENGTH, requestProductAiTaskSchema } from '@atlas/contracts'; import type { ProductEditorSnapshot } from '@atlas/contracts'; import { ProductAiService } from '../src/product-editor/product-ai.service.js'; describe('SKU 文案用户要求', () => { it('将用户要求与 SKU 输入原子固化,并纳入幂等请求,不改写商品字段', async () => { const hint = requestProductAiTaskSchema.parse({ hint: ' 改为红色款,25cm;标题不要写包邮。 ' }).hint; let stored: Record | undefined; let outbox: unknown; const statements: string[] = []; const query = vi.fn(async (sql: string, values: unknown[] = []) => { statements.push(sql); if (sql.includes('SELECT product_id')) return { rows: [{ product_id: 'product' }] }; if (sql.includes('SELECT product.revision')) return { rows: [{ product_revision: 3, profile_revision: 4, source_snapshot_revision: 5 }] }; if (sql.includes('INSERT INTO atlas.product_ai_tasks')) { stored = JSON.parse(values[4] as string); return { rows: [{ id: 'task' }] }; } if (sql.includes('INSERT INTO atlas.outbox_events')) outbox = JSON.parse(values[1] as string); return { rows: [] }; }); const transaction = { query }; const workspaceTransaction = vi.fn(async (_workspace: string, work: (tx: typeof transaction) => Promise) => work(transaction)); const run = vi.fn(async (_input: unknown, work: (tx: typeof transaction) => Promise) => work(transaction)); const snapshot = { product: { id: 'product', revision: 3, common: { model: 'model' } }, profile: { id: 'profile', revision: 4, platform_id: 'platform', target_store: { id: 'store' }, category: { id: null }, title: null, tags: [], description: null }, source_material: { source_snapshot_revision: 5, title: '采集蓝色 20cm', description: null }, variants: [{ id: 'sku', display_name: '蓝色款', specification_values: [], listing: { status: 'not_listed', offer_id: 'model-blue' }, tags: [] }], } as unknown as ProductEditorSnapshot; const service = new ProductAiService( { workspaceTransaction } as unknown as ConstructorParameters[0], { run } as unknown as ConstructorParameters[1], { read: vi.fn(async () => snapshot) } as unknown as ConstructorParameters[2], ); const result = await service.create('workspace', 'user', 'profile', 'platform', 'sku', 'key', hint); expect(result).toMatchObject({ status: 202, body: { data: { ai_task_id: 'task', status: 'queued' } } }); expect(stored).toMatchObject({ hint, variant_id: 'sku', revision: { product: 3, profile: 4, source_snapshot: 5 } }); expect(stored?.source_material).toEqual(snapshot.source_material); expect(run.mock.calls[0][0]).toMatchObject({ request: { hint, platform_id: 'platform' } }); expect(outbox).toEqual({ task_id: 'task' }); expect(statements.some((sql) => /UPDATE atlas\.(products|product_variants|collection_items)/.test(sql))).toBe(false); expect(workspaceTransaction.mock.calls.every(([workspace]) => workspace === 'workspace')).toBe(true); }); it('兼容不传要求,接受 1000 字符并拒绝超限和未知字段', () => { expect(requestProductAiTaskSchema.parse({})).toEqual({}); expect(requestProductAiTaskSchema.parse({ hint: ' ' })).toEqual({ hint: '' }); expect(requestProductAiTaskSchema.safeParse({ hint: '要'.repeat(PRODUCT_AI_HINT_MAX_LENGTH) }).success).toBe(true); expect(requestProductAiTaskSchema.safeParse({ hint: '要'.repeat(PRODUCT_AI_HINT_MAX_LENGTH + 1) }).success).toBe(false); expect(requestProductAiTaskSchema.safeParse({ hint: '红色款', source_override: {} }).success).toBe(false); }); });