import assert from 'node:assert/strict'; import { test } from 'node:test'; import sharp from 'sharp'; import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3'; import { createMediaAiEditRequestSchema } from '@atlas/contracts'; import { classifyAiEditFailure, downloadProviderTaskImage, normalizeAiEditOutput, pinnedLookup, providerImageResult, runMediaAiEdit } from '../src/media-ai-edit.ts'; import { buildMediaAiPrompt } from '../src/media-ai-prompt.ts'; const productId = '733d0d73-3d39-4a98-ac0a-5c0e521d8998'; test('仅扩图在供应商改写中心后仍把原图区域覆盖回结果', async () => { const source = await sharp({ create: { width: 600, height: 600, channels: 3, background: '#e1252a' } }).png().toBuffer(); const generated = await sharp({ create: { width: 1152, height: 1536, channels: 3, background: '#2055d8' } }).png().toBuffer(); const settings = createMediaAiEditRequestSchema.parse({ product_id: productId, mode: 'outpaint' }); const output = await normalizeAiEditOutput(generated, settings, source); const { data, info } = await sharp(output).raw().toBuffer({ resolveWithObject: true }); assert.equal(info.width, 1152); assert.equal(info.height, 1536); const pixel = (x, y) => [...data.subarray((y * info.width + x) * 3, (y * info.width + x) * 3 + 3)]; assert.ok(pixel(576, 768)[0] > 200, '原图中心仍应为红色'); assert.ok(pixel(576, 20)[2] > 170, '扩充边缘仍应为蓝色'); }); test('提示词明确区分主图、辅助图和尺寸数值', () => { const settings = createMediaAiEditRequestSchema.parse({ product_id: productId, reference_asset_ids: ['50ea00b7-6675-4cc7-b7a2-d45f05c07100'], dimensions: { length: 12, unit: 'cm' }, generate_size_chart: true, }); const prompt = buildMediaAiPrompt(settings); assert.match(prompt, /Image 1 is the fixed main product image/); assert.match(prompt, /Images 2 through 2 are auxiliary references only/); assert.match(prompt, /length: 12 cm/); assert.match(prompt, /Do not infer other measurements/); }); test('Pcore 完成结果的空 b64_json 回退到可下载 URL', () => { const url = 'https://pcore.ai/v1/images/content/task-example/0'; assert.equal(providerImageResult({ data: [{ b64_json: '', url }] }), url); assert.equal(providerImageResult({ data: [{ b64_json: ' ', url }] }), url); assert.deepEqual(providerImageResult({ data: [{ b64_json: '', b64_image: Buffer.from('image').toString('base64'), url }] }), Buffer.from('image')); assert.deepEqual(providerImageResult({ data: [{ b64_json: Buffer.from('image').toString('base64'), url }] }), Buffer.from('image')); }); test('固定 DNS 回调按 Node 的 all 选项返回地址列表或单个地址', () => { const lookup = pinnedLookup('8.8.8.8'); lookup('pcore.ai', { all: true }, (error, addresses) => { assert.equal(error, null); assert.deepEqual(addresses, [{ address: '8.8.8.8', family: 4 }]); }); lookup('pcore.ai', { all: false }, (error, address, family) => { assert.equal(error, null); assert.equal(address, '8.8.8.8'); assert.equal(family, 4); }); }); test('同源 Pcore 任务取图使用配置地址,不发送密钥且禁止重定向', async () => { const oldFetch = globalThis.fetch; const oldBase = process.env.PCORE_BASE_URL; const oldKey = process.env.PCORE_API_KEY; process.env.PCORE_BASE_URL = 'https://pcore.ai'; process.env.PCORE_API_KEY = 'test-key'; let requests = 0; globalThis.fetch = async (url, init) => { requests += 1; assert.equal(url, 'https://pcore.ai/v1/images/content/provider-123/0'); assert.equal(init.redirect, 'error'); assert.equal(init.headers, undefined); assert.ok(init.signal); return new Response('image'); }; try { assert.deepEqual(await downloadProviderTaskImage('https://pcore.ai/v1/images/content/provider-123/0', 'provider-123'), Buffer.from('image')); for (const url of [ 'https://pcore.ai/v1/images/content/another-task/0', 'https://pcore.ai/v1/images/content/provider-123/1', 'https://pcore.ai/v1/images/content/provider-123%2F0/0', 'https://pcore.ai/v1/images/content/provider-123/0?url=http://localhost', 'https://pcore.ai/v1/images/content/provider-123/0#fragment', 'https://user:password@pcore.ai/v1/images/content/provider-123/0', 'https://pcore.ai/v1/images/edits/provider-123', 'https://127.0.0.1/v1/images/content/provider-123/0', 'http://pcore.ai/v1/images/content/provider-123/0', ]) await assert.rejects(downloadProviderTaskImage(url, 'provider-123'), /结果地址不安全/); assert.equal(requests, 1, '非当前任务或非安全地址不能进入供应商内容请求'); await assert.rejects(downloadProviderTaskImage('https://pcore.ai/v1/images/content/../0', '..'), /结果地址不安全/); globalThis.fetch = async () => new Response(null, { status: 302, headers: { Location: 'https://127.0.0.1/' } }); await assert.rejects(downloadProviderTaskImage('https://pcore.ai/v1/images/content/provider-123/0', 'provider-123'), /HTTP 302/); globalThis.fetch = async () => new Response('oversize', { headers: { 'Content-Length': String(26 * 1024 * 1024) } }); await assert.rejects(downloadProviderTaskImage('https://pcore.ai/v1/images/content/provider-123/0', 'provider-123'), /超过处理上限/); globalThis.fetch = async () => new Response(new ReadableStream({ start(controller) { controller.enqueue(new Uint8Array(25 * 1024 * 1024)); controller.enqueue(new Uint8Array(1)); controller.close(); } })); await assert.rejects(downloadProviderTaskImage('https://pcore.ai/v1/images/content/provider-123/0', 'provider-123'), /超过处理上限/); globalThis.fetch = async () => { throw new TypeError('fetch failed'); }; await assert.rejects(downloadProviderTaskImage('https://pcore.ai/v1/images/content/provider-123/0', 'provider-123'), (error) => { assert.equal(classifyAiEditFailure(error).source, 'pcore'); assert.equal(error.message, 'Pcore 结果下载连接失败'); return true; }); } finally { globalThis.fetch = oldFetch; if (oldBase === undefined) delete process.env.PCORE_BASE_URL; else process.env.PCORE_BASE_URL = oldBase; if (oldKey === undefined) delete process.env.PCORE_API_KEY; else process.env.PCORE_API_KEY = oldKey; } }); test('异步供应商任务登记独立素材,完成结果兼容 base64 和同源图片接口', async () => { const sourceId = '664d0d73-3d39-4a98-ac0a-5c0e521d8998'; const referenceId = '6ebfe347-bc77-4270-8213-b2021271c6b2'; const outputId = '113d0d73-3d39-4a98-ac0a-5c0e521d8998'; const editId = '68db226f-3fcb-4579-ab9e-4932b1d84067'; const source = await sharp({ create: { width: 600, height: 800, channels: 3, background: '#ad3222' } }).jpeg().toBuffer(); const reference = await sharp({ create: { width: 400, height: 400, channels: 4, background: '#2452af80' } }).png().toBuffer(); const generated = await sharp({ create: { width: 1152, height: 1536, channels: 3, background: '#2f72b7' } }).png().toBuffer(); const settings = createMediaAiEditRequestSchema.parse({ product_id: productId, reference_asset_ids: [referenceId], prompt: '保留商品,使用明亮背景' }); const task = { status: 'queued', source_asset_id: sourceId, reference_asset_ids: [referenceId], settings, provider_task_id: null, created_at: new Date() }; const outbox = []; const stored = []; const database = { workspaceTransaction: async (_workspaceId, work) => work({ query: async (sql, params = []) => { if (sql.includes('SELECT status, source_asset_id, reference_asset_ids')) return { rows: [{ ...task }] }; if (sql.includes('SELECT id, storage_provider, bucket, object_key')) return { rows: [ { id: sourceId, storage_provider: 'local_minio', bucket: 'atlas-assets', object_key: 'main.jpg' }, { id: referenceId, storage_provider: 'local_minio', bucket: 'atlas-assets', object_key: 'reference.jpg' }, ] }; if (sql.includes('SELECT status, generation_attempt FROM atlas.media_ai_edits')) return { rows: [{ status: task.status, generation_attempt: 1 }] }; if (sql.includes('SELECT id FROM atlas.media_assets WHERE checksum_sha256')) return { rows: [] }; if (sql.includes('INSERT INTO atlas.media_assets')) return { rows: [{ id: outputId }] }; if (sql.includes('INSERT INTO atlas.outbox_events')) outbox.push(sql); if (sql.includes("SET status = 'running'")) task.status = 'running'; if (sql.includes('SET provider_task_id =')) task.provider_task_id = params[1]; if (sql.includes("SET status = 'succeeded'")) task.status = 'succeeded'; return { rows: [] }; }, }) }; const storage = { send: async (command) => { if (command instanceof GetObjectCommand) return { Body: (async function* () { yield command.input.Key === 'main.jpg' ? source : reference; })() }; if (command instanceof PutObjectCommand) { stored.push(command.input); return {}; } throw new Error('unexpected object storage command'); } }; const oldFetch = globalThis.fetch; const oldBase = process.env.PCORE_BASE_URL; const oldKey = process.env.PCORE_API_KEY; process.env.PCORE_BASE_URL = 'https://pcore.ai/v1'; process.env.PCORE_API_KEY = 'test-key'; let requests = 0; let useContentUrl = false; globalThis.fetch = async (url, init) => { requests += 1; if (init.method === 'POST') { assert.equal(url, 'https://pcore.ai/v1/images/edits'); assert.equal(init.headers['Content-Type'], 'application/json'); const body = JSON.parse(init.body); assert.equal(body.model, 'gpt-image-2'); assert.equal(body.size, '1152x1536'); assert.equal(body.n, 1); assert.equal(body.async, true); assert.equal(body.images.length, 2); assert.deepEqual(body.images, [ { image_url: `data:image/jpeg;base64,${source.toString('base64')}` }, { image_url: `data:image/png;base64,${reference.toString('base64')}` }, ]); assert.equal(body.mask, undefined); return Response.json({ task_id: 'provider-123' }); } if (url === 'https://pcore.ai/v1/images/content/provider-123/0') { assert.equal(init.headers, undefined); assert.equal(init.redirect, 'error'); return new Response(generated); } assert.equal(url, 'https://pcore.ai/v1/images/edits/provider-123'); return Response.json({ status: 'completed', data: [useContentUrl ? { b64_json: '', url: 'https://pcore.ai/v1/images/content/provider-123/0' } : { b64_json: generated.toString('base64') }] }); }; try { const job = { workspaceId: '30cf54b7-06a3-4e5f-a5fc-d74165115094', payload: { edit_id: editId } }; await runMediaAiEdit(database, storage, job); assert.equal(task.status, 'running'); assert.equal(task.provider_task_id, 'provider-123'); assert.equal(outbox.length, 1); await runMediaAiEdit(database, storage, job); assert.equal(task.status, 'succeeded'); assert.equal(stored.length, 1); assert.equal(stored[0].ContentType, 'image/jpeg'); assert.equal(requests, 2); task.status = 'running'; useContentUrl = true; await runMediaAiEdit(database, storage, job); assert.equal(task.status, 'succeeded'); assert.equal(stored.length, 2); assert.equal(requests, 4, '已保存任务只查询状态和取图,不重新提交生成'); } finally { globalThis.fetch = oldFetch; if (oldBase === undefined) delete process.env.PCORE_BASE_URL; else process.env.PCORE_BASE_URL = oldBase; if (oldKey === undefined) delete process.env.PCORE_API_KEY; else process.env.PCORE_API_KEY = oldKey; } }); test('仅扩图以 JSON 图片和蒙版对象提交 Pcore', async () => { const sourceId = '664d0d73-3d39-4a98-ac0a-5c0e521d8998'; const editId = '68db226f-3fcb-4579-ab9e-4932b1d84067'; const source = await sharp({ create: { width: 600, height: 600, channels: 3, background: '#ad3222' } }).jpeg().toBuffer(); const settings = createMediaAiEditRequestSchema.parse({ product_id: productId, mode: 'outpaint' }); const database = { workspaceTransaction: async (_workspaceId, work) => work({ query: async (sql) => { if (sql.includes('SELECT status, source_asset_id, reference_asset_ids')) return { rows: [{ status: 'queued', source_asset_id: sourceId, reference_asset_ids: [], settings, provider_task_id: null, created_at: new Date(), }] }; if (sql.includes('SELECT id, storage_provider, bucket, object_key')) return { rows: [{ id: sourceId, storage_provider: 'local_minio', bucket: 'atlas-assets', object_key: 'main.jpg', }] }; return { rows: [] }; }, }) }; const storage = { send: async () => ({ Body: (async function* () { yield source; })() }) }; const oldFetch = globalThis.fetch; const oldBase = process.env.PCORE_BASE_URL; const oldKey = process.env.PCORE_API_KEY; process.env.PCORE_BASE_URL = 'https://pcore.ai'; process.env.PCORE_API_KEY = 'test-key'; globalThis.fetch = async (_url, init) => { const body = JSON.parse(init.body); assert.equal(init.headers['Content-Type'], 'application/json'); assert.equal(body.images.length, 1); assert.match(body.images[0].image_url, /^data:image\/png;base64,/); assert.match(body.mask.image_url, /^data:image\/png;base64,/); const image = Buffer.from(body.images[0].image_url.split(',')[1], 'base64'); const mask = Buffer.from(body.mask.image_url.split(',')[1], 'base64'); assert.equal((await sharp(image).metadata()).width, 1152); assert.equal((await sharp(mask).metadata()).height, 1536); return Response.json({ task_id: 'provider-outpaint' }); }; try { await runMediaAiEdit(database, storage, { workspaceId: '30cf54b7-06a3-4e5f-a5fc-d74165115094', payload: { edit_id: editId }, }); } finally { globalThis.fetch = oldFetch; if (oldBase === undefined) delete process.env.PCORE_BASE_URL; else process.env.PCORE_BASE_URL = oldBase; if (oldKey === undefined) delete process.env.PCORE_API_KEY; else process.env.PCORE_API_KEY = oldKey; } }); test('Pcore 终态失败不重复轮询,泛化错误不会误判成内容拒绝', async () => { const sourceId = '664d0d73-3d39-4a98-ac0a-5c0e521d8998'; const editId = '68db226f-3fcb-4579-ab9e-4932b1d84067'; const settings = createMediaAiEditRequestSchema.parse({ product_id: productId }); let status = 'running'; let failureCode = null; let providerErrorRaw = null; let polls = 0; let providerError = { code: 'failed', message: '您的请求无法用于生成图像。该请求可能因安全政策被拦截。', }; const database = { workspaceTransaction: async (_workspaceId, work) => work({ query: async (sql, params = []) => { if (sql.includes('SELECT status, source_asset_id, reference_asset_ids')) return { rows: [{ status, source_asset_id: sourceId, reference_asset_ids: [], settings, provider_task_id: 'provider-123', created_at: new Date(), }] }; if (sql.includes('SELECT status, generation_attempt FROM atlas.media_ai_edits')) return { rows: [{ status, generation_attempt: 1 }] }; if (sql.includes('SELECT id, storage_provider, bucket, object_key')) return { rows: [{ id: sourceId, storage_provider: 'local_minio', bucket: 'atlas-assets', object_key: 'main.jpg', }] }; if (sql.includes("SET status = 'failed'")) { status = 'failed'; failureCode = params[1]; if (sql.includes('atlas.operations')) { providerErrorRaw = params[6]; assert.equal(params[3], 'pcore'); assert.equal(params[4], 'provider_polling'); } } return { rows: [] }; }, }) }; const oldFetch = globalThis.fetch; const oldBase = process.env.PCORE_BASE_URL; const oldKey = process.env.PCORE_API_KEY; process.env.PCORE_BASE_URL = 'https://pcore.ai'; process.env.PCORE_API_KEY = 'test-key'; globalThis.fetch = async (url) => { polls += 1; assert.equal(url, 'https://pcore.ai/v1/images/edits/provider-123'); return Response.json({ status: 'failed', error: providerError }); }; try { const job = { workspaceId: '30cf54b7-06a3-4e5f-a5fc-d74165115094', payload: { edit_id: editId } }; await runMediaAiEdit(database, {}, job); assert.equal(status, 'failed'); assert.equal(failureCode, 'AI_IMAGE_PROVIDER_FAILED'); assert.equal(providerErrorRaw, JSON.stringify(providerError)); assert.equal(polls, 1); status = 'running'; providerError = { code: 'content_policy_violation', message: 'The image was blocked.' }; await runMediaAiEdit(database, {}, job); assert.equal(failureCode, 'AI_IMAGE_CONTENT_REJECTED'); assert.equal(providerErrorRaw, JSON.stringify(providerError)); assert.equal(polls, 2); } finally { globalThis.fetch = oldFetch; if (oldBase === undefined) delete process.env.PCORE_BASE_URL; else process.env.PCORE_BASE_URL = oldBase; if (oldKey === undefined) delete process.env.PCORE_API_KEY; else process.env.PCORE_API_KEY = oldKey; } }); test('Pcore 提交时返回错误体会保留原始内容', async () => { const sourceId = '664d0d73-3d39-4a98-ac0a-5c0e521d8998'; const editId = '68db226f-3fcb-4579-ab9e-4932b1d84067'; const source = await sharp({ create: { width: 600, height: 800, channels: 3, background: '#ad3222' } }).jpeg().toBuffer(); const settings = createMediaAiEditRequestSchema.parse({ product_id: productId }); const database = { workspaceTransaction: async (_workspaceId, work) => work({ query: async (sql) => { if (sql.includes('SELECT status, source_asset_id, reference_asset_ids')) return { rows: [{ status: 'queued', source_asset_id: sourceId, reference_asset_ids: [], settings, provider_task_id: null, created_at: new Date(), }] }; if (sql.includes('SELECT id, storage_provider, bucket, object_key')) return { rows: [{ id: sourceId, storage_provider: 'local_minio', bucket: 'atlas-assets', object_key: 'main.jpg', }] }; return { rows: [] }; }, }) }; const storage = { send: async () => ({ Body: (async function* () { yield source; })() }) }; const oldFetch = globalThis.fetch; const oldBase = process.env.PCORE_BASE_URL; const oldKey = process.env.PCORE_API_KEY; process.env.PCORE_BASE_URL = 'https://pcore.ai'; process.env.PCORE_API_KEY = 'test-key'; const providerError = { code: 'invalid_request_error', message: '图像文件无效' }; globalThis.fetch = async () => Response.json({ status: 'failed', error: providerError }); try { const job = { workspaceId: '30cf54b7-06a3-4e5f-a5fc-d74165115094', payload: { edit_id: editId } }; await assert.rejects(runMediaAiEdit(database, storage, job), (error) => { assert.deepEqual(classifyAiEditFailure(error), { code: 'AI_IMAGE_GENERATION_FAILED', source: 'pcore', stage: 'provider_submission', detail: 'Pcore 图片任务请求被拒绝(invalid_request_error 图像文件无效)', providerErrorRaw: JSON.stringify(providerError), }); return true; }); } finally { globalThis.fetch = oldFetch; if (oldBase === undefined) delete process.env.PCORE_BASE_URL; else process.env.PCORE_BASE_URL = oldBase; if (oldKey === undefined) delete process.env.PCORE_API_KEY; else process.env.PCORE_API_KEY = oldKey; } }); test('失败诊断区分 Pcore 查询中断、Atlas 图片处理与保存失败', async () => { const sourceId = '664d0d73-3d39-4a98-ac0a-5c0e521d8998'; const settings = createMediaAiEditRequestSchema.parse({ product_id: productId }); const database = { workspaceTransaction: async (_workspaceId, work) => work({ query: async (sql) => { if (sql.includes('SELECT status, source_asset_id, reference_asset_ids')) return { rows: [{ status: 'running', source_asset_id: sourceId, reference_asset_ids: [], settings, provider_task_id: 'provider-123', created_at: new Date(), }] }; if (sql.includes('SELECT id, storage_provider, bucket, object_key')) return { rows: [{ id: sourceId, storage_provider: 'local_minio', bucket: 'atlas-assets', object_key: 'main.jpg', }] }; return { rows: [] }; }, }) }; const job = { workspaceId: '30cf54b7-06a3-4e5f-a5fc-d74165115094', payload: { edit_id: '68db226f-3fcb-4579-ab9e-4932b1d84067' } }; const oldFetch = globalThis.fetch; const oldBase = process.env.PCORE_BASE_URL; const oldKey = process.env.PCORE_API_KEY; process.env.PCORE_BASE_URL = 'https://pcore.ai/v1'; process.env.PCORE_API_KEY = 'test-key'; try { globalThis.fetch = async () => { throw new TypeError('fetch failed'); }; await assert.rejects(runMediaAiEdit(database, {}, job), (error) => { assert.deepEqual(classifyAiEditFailure(error), { code: 'AI_IMAGE_GENERATION_FAILED', source: 'pcore', stage: 'provider_polling', detail: 'fetch failed', providerErrorRaw: null, }); return true; }); globalThis.fetch = async () => Response.json({ status: 'completed', data: [{ b64_json: Buffer.from('invalid image').toString('base64') }] }); await assert.rejects(runMediaAiEdit(database, {}, job), (error) => { const failure = classifyAiEditFailure(error); assert.equal(failure.source, 'atlas'); assert.equal(failure.stage, 'result_processing'); assert.match(failure.detail, /image|format|buffer/i); return true; }); const generated = await sharp({ create: { width: 100, height: 100, channels: 3, background: '#445566' } }).png().toBuffer(); globalThis.fetch = async () => Response.json({ status: 'completed', data: [{ b64_json: generated.toString('base64') }] }); const storage = { send: async () => { throw new Error('private/object-key.jpg'); } }; await assert.rejects(runMediaAiEdit(database, storage, job), (error) => { const failure = classifyAiEditFailure(error); assert.equal(failure.source, 'atlas'); assert.equal(failure.stage, 'result_storage'); assert.equal(failure.detail, 'Error'); return true; }); } finally { globalThis.fetch = oldFetch; if (oldBase === undefined) delete process.env.PCORE_BASE_URL; else process.env.PCORE_BASE_URL = oldBase; if (oldKey === undefined) delete process.env.PCORE_API_KEY; else process.env.PCORE_API_KEY = oldKey; } });