| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061 |
- import assert from 'node:assert/strict';
- import test from 'node:test';
- import type { ListingSourceSnapshot } from '../src/modules/listing-ai/domain.js';
- import { canonicalHash, scoreListing } from '../src/modules/listing-ai/scoring/rule-engine.js';
- function source(overrides: Partial<ListingSourceSnapshot> = {}): 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'], itemStatus: 'on_shelf',
- price: { jd: 1049, cost: 800 }, descriptions: { desktopHtml: `<p>${'完整商品详情与使用场景。'.repeat(30)}</p>`, mobileHtml: `<p>${'移动端商品详情。'.repeat(30)}</p>` },
- 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 },
- 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, 100);
- assert.equal(first.coverage.status, 'eligible');
- assert.equal(first.dimensions.length, 5);
- });
- 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.unique');
- assert.equal(result.rubricVersion, 'listing-jd-v2');
- assert.equal(duplicateRule?.outcome, 'unknown');
- 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 }));
- });
|