import assert from 'node:assert/strict'; import test from 'node:test'; import type { ListingSourceSnapshot } from '../src/modules/listing-ai/domain.js'; import { reviewImageAssets } from '../src/modules/listing-ai/image-review/image-review.service.js'; import { FmodeGeminiImageReviewProvider } from '../src/modules/listing-ai/image-review/gemini-image-review.provider.js'; import { InMemoryListingAiRepository } from '../src/modules/listing-ai/repositories/in-memory-listing-ai.repository.js'; import { ListingAiService } from '../src/modules/listing-ai/listing-ai.service.js'; const source = (images: number): ListingSourceSnapshot => ({ id: 'image-source', workspaceId: 'demashi', platform: 'jd', shopId: 'shop', productId: 'image-1', sourceHash: 'c'.repeat(64), title: '测试商品', titleBrandName: null, brand: { id: null, name: null }, categoryIds: [], itemStatus: null, price: { jd: null, cost: null }, descriptions: { desktopHtml: null, mobileHtml: null }, features: [], attributes: [], images: Array.from({ length: images }, (_, index) => ({ url: `https://private.example/${index}.jpg`, order: index + 1, isPrimary: index === 0, gptFlag: null })), imageAssets: { defaultImages: [], skuImages: [], whiteBackgroundImages: [] }, skus: [], dimensions: { length: null, width: null, height: null, weight: null }, logistics: {}, afterService: {}, sourceModifiedAt: null, syncedAt: '2026-09-04T00:00:00.000Z', detailStatus: 'available', }); test('image review is closed by default and never exposes image URLs', () => { assert.deepEqual(reviewImageAssets(source(2)), { status: 'not_requested', model: null, evidence: [], suggestions: [] }); const result = reviewImageAssets(source(2), { enabled: true }); assert.equal(result.status, 'shadow_completed'); assert.equal(JSON.stringify(result).includes('private.example'), false); assert.equal(result.evidence.every((item) => !item.message.includes('http')), true); }); test('shadow benchmark classifies empty and non-empty asset structure without changing score inputs', () => { const cases = Array.from({ length: 10 }, (_, index) => ({ source: source(index % 2), expected: index % 2 ? 'pass' : 'fail' })); const correct = cases.filter((item) => reviewImageAssets(item.source, { enabled: true }).evidence[0]?.outcome === item.expected).length; assert.equal(correct / cases.length, 1); }); test('Gemini provider sends bounded image inputs with server authorization and normalizes JSON', async () => { let requestBody = ''; let authorization = ''; const provider = new FmodeGeminiImageReviewProvider({ baseUrl: 'https://vision.example.test', token: 'server-token', fetchImpl: (async (_input, init) => { requestBody = String(init?.body ?? ''); authorization = new Headers(init?.headers).get('authorization') ?? ''; return new Response(JSON.stringify({ choices: [{ message: { content: JSON.stringify({ visible_facts: [{ field: 'category', value: '削皮机', confidence: 0.9 }], visual_inferences: [], unknowns: [] }) } }] }), { status: 200 }); }) as typeof fetch, }); const result = await provider.analyze(['https://img.test/a.jpg', 'https://img.test/b.jpg', 'https://img.test/c.jpg', 'https://img.test/d.jpg'], source(2)); assert.equal(result.visibleFacts[0]?.value, '削皮机'); assert.equal(authorization, 'Bearer server-token'); assert.equal((JSON.parse(requestBody) as { messages: Array<{ content: unknown[] }> }).messages[0]?.content.length, 4); }); test('service persists shadow evidence without changing any JD-VOC main score', async () => { const input = source(2); const repository = new InMemoryListingAiRepository([input]); const provider = { analyze: async () => ({ visibleFacts: [{ field: 'category', value: '测试商品', confidence: 0.9 }], visualInferences: [], unknowns: [], model: 'gemini-test', latencyMs: 1 }) }; const service = new ListingAiService(repository, undefined, () => new Date('2026-09-05T00:00:00.000Z'), 1, 10, undefined, provider); const before = await service.scoreJdVocRules({ workspaceId: input.workspaceId, platform: input.platform, productId: input.productId }); const after = await service.reviewJdVocImages({ workspaceId: input.workspaceId, platform: input.platform, productId: input.productId }); assert.equal(after.imageReview?.status, 'shadow_completed'); assert.equal(after.overallScore, before.overallScore); assert.deepEqual(after.dimensions, before.dimensions); });