image-review-shadow.test.ts 4.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
  1. import assert from 'node:assert/strict';
  2. import test from 'node:test';
  3. import type { ListingSourceSnapshot } from '../src/modules/listing-ai/domain.js';
  4. import { reviewImageAssets } from '../src/modules/listing-ai/image-review/image-review.service.js';
  5. import { FmodeGeminiImageReviewProvider } from '../src/modules/listing-ai/image-review/gemini-image-review.provider.js';
  6. import { InMemoryListingAiRepository } from '../src/modules/listing-ai/repositories/in-memory-listing-ai.repository.js';
  7. import { ListingAiService } from '../src/modules/listing-ai/listing-ai.service.js';
  8. const source = (images: number): ListingSourceSnapshot => ({
  9. id: 'image-source', workspaceId: 'demashi', platform: 'jd', shopId: 'shop', productId: 'image-1', sourceHash: 'c'.repeat(64),
  10. title: '测试商品', titleBrandName: null, brand: { id: null, name: null }, categoryIds: [], itemStatus: null, price: { jd: null, cost: null },
  11. descriptions: { desktopHtml: null, mobileHtml: null }, features: [], attributes: [],
  12. images: Array.from({ length: images }, (_, index) => ({ url: `https://private.example/${index}.jpg`, order: index + 1, isPrimary: index === 0, gptFlag: null })),
  13. 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',
  14. });
  15. test('image review is closed by default and never exposes image URLs', () => {
  16. assert.deepEqual(reviewImageAssets(source(2)), { status: 'not_requested', model: null, evidence: [], suggestions: [] });
  17. const result = reviewImageAssets(source(2), { enabled: true });
  18. assert.equal(result.status, 'shadow_completed');
  19. assert.equal(JSON.stringify(result).includes('private.example'), false);
  20. assert.equal(result.evidence.every((item) => !item.message.includes('http')), true);
  21. });
  22. test('shadow benchmark classifies empty and non-empty asset structure without changing score inputs', () => {
  23. const cases = Array.from({ length: 10 }, (_, index) => ({ source: source(index % 2), expected: index % 2 ? 'pass' : 'fail' }));
  24. const correct = cases.filter((item) => reviewImageAssets(item.source, { enabled: true }).evidence[0]?.outcome === item.expected).length;
  25. assert.equal(correct / cases.length, 1);
  26. });
  27. test('Gemini provider sends bounded image inputs with server authorization and normalizes JSON', async () => {
  28. let requestBody = '';
  29. let authorization = '';
  30. const provider = new FmodeGeminiImageReviewProvider({
  31. baseUrl: 'https://vision.example.test', token: 'server-token', fetchImpl: (async (_input, init) => {
  32. requestBody = String(init?.body ?? ''); authorization = new Headers(init?.headers).get('authorization') ?? '';
  33. return new Response(JSON.stringify({ choices: [{ message: { content: JSON.stringify({ visible_facts: [{ field: 'category', value: '削皮机', confidence: 0.9 }], visual_inferences: [], unknowns: [] }) } }] }), { status: 200 });
  34. }) as typeof fetch,
  35. });
  36. 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));
  37. assert.equal(result.visibleFacts[0]?.value, '削皮机');
  38. assert.equal(authorization, 'Bearer server-token');
  39. assert.equal((JSON.parse(requestBody) as { messages: Array<{ content: unknown[] }> }).messages[0]?.content.length, 4);
  40. });
  41. test('service persists shadow evidence without changing any JD-VOC main score', async () => {
  42. const input = source(2);
  43. const repository = new InMemoryListingAiRepository([input]);
  44. const provider = { analyze: async () => ({ visibleFacts: [{ field: 'category', value: '测试商品', confidence: 0.9 }], visualInferences: [], unknowns: [], model: 'gemini-test', latencyMs: 1 }) };
  45. const service = new ListingAiService(repository, undefined, () => new Date('2026-09-05T00:00:00.000Z'), 1, 10, undefined, provider);
  46. const before = await service.scoreJdVocRules({ workspaceId: input.workspaceId, platform: input.platform, productId: input.productId });
  47. const after = await service.reviewJdVocImages({ workspaceId: input.workspaceId, platform: input.platform, productId: input.productId });
  48. assert.equal(after.imageReview?.status, 'shadow_completed');
  49. assert.equal(after.overallScore, before.overallScore);
  50. assert.deepEqual(after.dimensions, before.dimensions);
  51. });