import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import test from 'node:test'; import type { ListingSourceSnapshot } from '../src/modules/listing-ai/domain.js'; import { canonicalHash, listingTitleVisibleCharacters, LISTING_DIMENSION_MAX, scoreListing } from '../src/modules/listing-ai/scoring/rule-engine.js'; import { normalizeJdListing } from '../src/modules/listing-ai/normalization/jd-listing.normalizer.js'; import { buildListingContextIndex, LISTING_CONTEXT_VERSION } from '../src/modules/listing-ai/normalization/listing-context.enricher.js'; import { listingProductScoreStatus, selectCurrentListingScore } from '../src/modules/listing-ai/scoring/score-status.js'; function source(overrides: Partial = {}): ListingSourceSnapshot { return { id: 'source-1', workspaceId: 'demashi', platform: 'jd', shopId: 'shop-1', productId: '1001', sourceHash: 'a'.repeat(64), title: '星星 商用冷藏展示柜 299L 一级能效风冷无霜便利店商超适用', titleBrandName: '星星', brand: { id: '1', name: '星星' }, categoryIds: ['10', '20'], categoryContext: { names: ['商用冷藏展示柜'], coreTerms: ['商用冷藏展示柜'], requiredSpecificationNames: ['容量'], qualificationNames: [], ruleVersion: 'test-category-v1' }, itemStatus: 'on_shelf', price: { jd: 1049, cost: 800 }, descriptions: { desktopHtml: `

${'完整商品详情与使用场景。'.repeat(30)}

`, mobileHtml: `

${'移动端商品详情。'.repeat(30)}

` }, descriptionStructure: { observed: true, imageCount: 8, videoCount: 1, headingCount: 4, faqCandidateCount: 5 }, features: [ { key: 'capacity', value: '299L 大容量' }, { key: 'efficiency', value: '一级能效' }, { key: 'cooling', value: '风冷无霜' }, ], attributes: [ { id: '1', name: '容量', values: ['299L'] }, { id: '2', name: '制冷方式', values: ['风冷'] }, { id: '3', name: '能效', values: ['一级'] }, ], images: Array.from({ length: 5 }, (_, index) => ({ url: `https://img.test/${index}.jpg`, order: index + 1, isPrimary: index === 0, gptFlag: false })), skus: [{ skuId: 'sku-1', name: '299L', price: 1049, stock: 20, status: '1', attributes: [{ id: '1', name: '容量', values: ['299L'] }] }], dimensions: { length: 600, width: 620, height: 1900, weight: 60 }, logistics: { delivery: '京东物流' }, afterService: { return7Days: true }, marketing: { adword: '299L 大容量 一级能效 风冷无霜', skuShortTitles: [{ skuId: 'sku-1', value: '299L 高效冷藏' }], sellingPoints: [{ value: '299L 大容量 一级能效 风冷无霜', source: 'product_adword', fieldPath: 'productInfo.adword', skuId: null }, { value: '299L 高效冷藏', source: 'sku_short_title', fieldPath: 'skuList[].features[key=shortTitle]', skuId: 'sku-1' }] }, vocEvidence: [{ id: 'voc-1', text: '容量不足和结霜是主要顾虑', sourceVersion: 'voc-v1', collectedAt: '2026-08-20T00:00:00.000Z' }], sourceModifiedAt: '2026-08-21T00:00:00.000Z', syncedAt: '2026-08-21T00:00:00.000Z', detailStatus: 'available', ...overrides, }; } test('rule engine is deterministic for the same source and rubric', () => { const first = scoreListing(source(), { id: 'same', now: '2026-08-21T00:00:00.000Z' }); const second = scoreListing(source(), { id: 'same', now: '2026-08-21T00:00:00.000Z' }); assert.deepEqual(first, second); assert.equal(first.overallScore, null); assert.equal(first.knownOverallScore, 64); assert.equal(first.knownOverallMaxScore, 64); assert.equal(first.coverage.status, 'eligible'); assert.equal(first.dimensions.length, 5); assert.deepEqual(Object.fromEntries(first.dimensions.map((item) => [item.dimension, item.maxScore])), LISTING_DIMENSION_MAX); }); test('V7 title length counts Unicode visible characters and missing observable category is a numeric failure', () => { assert.equal(listingTitleVisibleCharacters('京东ABC😀'), 6); assert.equal(scoreListing(source({ title: '德玛仕(DEMASHI)商用电饼铛大型双面加热全自动电热大号烤饼机' })).dimensions.find((item) => item.dimension === 'title')?.evidence.find((item) => item.ruleId === 'title.length')?.outcome, 'pass'); const withoutCategory = source(); delete withoutCategory.categoryContext; const result = scoreListing(withoutCategory); const title = result.dimensions.find((item) => item.dimension === 'title'); assert.equal(title?.evidence.find((item) => item.ruleId === 'title.category_in_first_15')?.outcome, 'fail'); assert.equal(title?.status, 'partial'); }); test('compliance is independent from quality points and blocks critical redirects', () => { const result = scoreListing(source({ title: '星星 商用冷藏展示柜 联系电话:13800138000' })); assert.equal(result.compliance?.status, 'blocked'); assert.equal(result.compliance?.findings.some((item) => item.ruleId === 'compliance.external_redirect'), true); }); test('missing detail is blocked instead of receiving a zero quality score', () => { const result = scoreListing(source({ title: null, features: [], images: [], attributes: [], skus: [], descriptions: { desktopHtml: null, mobileHtml: null }, detailStatus: 'empty', })); assert.equal(result.coverage.status, 'blocked'); assert.equal(result.overallScore, null); assert.ok(result.dimensions.every((dimension) => dimension.score === null)); }); test('JD transport flags are not misclassified as duplicate selling points', () => { const result = scoreListing(source({ features: [ { key: 'isPayFirst', value: '1' }, { key: 'is7ToReturn', value: '1' }, { key: 'popsfkc', value: '0' }, { key: 'nameWithoutBrand', value: '299L 一级能效风冷无霜展示柜' }, { key: 'model', value: 'BC-299' }, ], })); const sellingPoints = result.dimensions.find((item) => item.dimension === 'selling_points'); const duplicateRule = sellingPoints?.evidence.find((item) => item.ruleId === 'selling_points.exact_dedup'); assert.equal(result.rubricVersion, 'listing-jd-v7'); assert.equal(duplicateRule?.outcome, 'pass'); assert.equal(duplicateRule?.delta, 0); }); test('canonical source hash ignores object key insertion order', () => { assert.equal(canonicalHash({ a: 1, b: { c: 2 } }), canonicalHash({ b: { c: 2 }, a: 1 })); }); test('JD adword and SKU shortTitle are normalized as attributed marketing points', () => { const normalized = normalizeJdListing({ workspaceId: 'demashi', shopId: 'shop', row: { productId: '2001' }, detail: { productInfo: { productId: '2001', adword: '商用大容量', features: [] }, skuList: [ { skuId: 'sku-1', features: [{ key: 'shortTitle', value: '70L 高效供水' }] }, { skuId: 'sku-2', features: [{ key: 'shortTitle', value: '70L 高效供水' }] }, ] }, syncedAt: '2026-08-24T00:00:00.000Z', }); assert.equal(normalized.normalizerVersion, 'jd-listing-v5'); assert.deepEqual(normalized.marketing?.sellingPoints.map((item) => [item.source, item.value]), [ ['product_adword', '商用大容量'], ['sku_short_title', '70L 高效供水'], ]); }); test('composite JD category labels publish independently matchable core terms', () => { const result = scoreListing(source({ title: '星星 商用电饼铛 大号双面加热', categoryContext: { names: ['商用电饼铛/煎包锅贴机/烤饼机'], coreTerms: ['商用电饼铛', '煎包锅贴机', '烤饼机'], aliases: ['商用电饼铛', '煎包锅贴机', '烤饼机'], requiredSpecificationNames: ['品牌'], qualificationNames: [], ruleVersion: 'jd-category-rules-2026-08-v2' }, })); assert.equal(result.dimensions.find((item) => item.dimension === 'title')?.evidence.find((item) => item.ruleId === 'title.category_in_first_15')?.outcome, 'pass'); }); test('V6 category position starts after a confirmed bilingual leading brand', () => { const result = scoreListing(source({ title: '德玛仕(DEMASHI)商用电饼铛大型双面加热全自动电热大号烤饼机', titleBrandName: '德玛仕', brand: { id: '1', name: '德玛仕' }, categoryContext: { names: ['商用电饼铛/煎包锅贴机/烤饼机'], coreTerms: ['商用电饼铛'], aliases: [], requiredSpecificationNames: [], qualificationNames: [], ruleVersion: 'legacy' }, })); assert.equal(result.dimensions.find((item) => item.dimension === 'title')?.evidence.find((item) => item.ruleId === 'title.category_in_first_15')?.outcome, 'pass'); }); test('normalizer preserves image groups and uses only the default group primary as product primary', () => { const normalized = normalizeJdListing({ workspaceId: 'demashi', shopId: 'shop', row: { productId: 'image-groups' }, detail: { productInfo: { productId: 'image-groups' }, material: { mainImages: [ { uuid: '0000000000', imageInfoList: [{ imgUrl: 'default-1.jpg', orderSort: 1, primaryFlag: true }, { imgUrl: 'default-2.jpg', orderSort: 2, primaryFlag: false }] }, { uuid: 'sku-1', imageInfoList: [{ imgUrl: 'sku-1.jpg', orderSort: 1, primaryFlag: true }] }, ] } }, }); assert.equal(normalized.images.filter((item) => item.isPrimary === true).length, 1); assert.equal(normalized.images[0]?.groupId, '0000000000'); assert.equal(normalized.images[0]?.primarySource, 'authoritative_field'); assert.equal(scoreListing(normalized).dimensions.find((item) => item.dimension === 'images')?.evidence.find((item) => item.ruleId === 'images.primary')?.outcome, 'pass'); }); test('ambiguous image groups produce a numeric primary failure instead of a dynamic denominator', () => { const normalized = normalizeJdListing({ workspaceId: 'demashi', shopId: 'shop', row: { productId: 'ambiguous-groups' }, detail: { productInfo: { productId: 'ambiguous-groups' }, material: { mainImages: [ { uuid: 'sku-1', imageInfoList: [{ imgUrl: 'sku-1.jpg', orderSort: 1, primaryFlag: true }] }, { uuid: 'sku-2', imageInfoList: [{ imgUrl: 'sku-2.jpg', orderSort: 1, primaryFlag: true }] }, ] } }, }); const result = scoreListing(normalized); assert.equal(normalized.images.length, 0); assert.equal(result.dimensions.find((item) => item.dimension === 'images')?.evidence.find((item) => item.ruleId === 'images.primary')?.outcome, 'fail'); assert.deepEqual(result.unknownCriteria, []); }); test('a result without an overall score is classified as partial instead of disappearing between filters', () => { const input = source(); const result = scoreListing(input); const partial = { ...result, overallScore: null }; assert.equal(listingProductScoreStatus(input, partial), 'partial'); }); test('the presentation gate never falls back to a legacy score version', () => { const input=source(); const current=scoreListing(input); const legacy={...current,rubricVersion:'listing-jd-v4',overallScore:88,knownOverallScore:88,knownOverallMaxScore:100}; assert.equal(selectCurrentListingScore([legacy],input.sourceHash).displayScore,null); assert.equal(selectCurrentListingScore([current],input.sourceHash).displayScore?.rubricVersion,'listing-jd-v7'); }); test('V7 migration creates current slots and physically drops score history', async () => { const migration = await readFile(new URL('../migrations/012_listing_remove_score_history.sql', import.meta.url), 'utf8'); assert.match(migration, /CREATE TABLE IF NOT EXISTS voc\.listing_current_score/); assert.match(migration, /UNIQUE \(workspace_id, product_id, slot\)/); assert.match(migration, /DROP TABLE voc\.listing_score_result/); assert.match(migration, /DROP COLUMN IF EXISTS score_result_public_id/); assert.match(migration, /DROP COLUMN IF EXISTS base_score_result_public_id/); }); test('listing context joins SKU categories and versioned category VOC evidence', () => { const index = buildListingContextIndex( [{ productId: 'sku-1', category3: '商用冰箱', title: 'SKU 商品' }], [{ competitorProductId: 'competitor-1', category: '商用冰箱' }], [{ productId: 'competitor-1', reviewKey: 'review-1', rating: 2, content: '容易结霜,容量偏小', reviewDate: '2026-08-20T00:00:00.000Z' }], ); const enriched = index.enrich(source(), '2026-08-24T00:00:00.000Z'); assert.equal(enriched.contextVersion, LISTING_CONTEXT_VERSION); assert.deepEqual(enriched.categoryContext?.coreTerms, ['商用冰箱']); assert.equal(enriched.categoryContext?.ruleVersion, 'jd-category-rules-2026-08-v3'); assert.equal(enriched.vocEvidence?.[0]?.text, '容易结霜,容量偏小'); assert.notEqual(enriched.sourceHash, source().sourceHash); });