cangku/backend/test/pddDraftAttributePreparation.test.js

206 lines
6.9 KiB
JavaScript

'use strict';
const assert = require('node:assert/strict');
const test = require('node:test');
const {
findCategoryMapping,
mergePropertyRecommendations,
resolveExcelMappedProperties,
} = require('../src/services/pdd1688PropertyMappingService');
const {
collectPddAttributeImages,
preparePddDraftAttributes,
recognizeImageText,
} = require('../src/services/pddDraftAttributePreparationService');
const {
listPddPublishingShops,
resolvePddPublishingShop,
} = require('../src/services/pddPublishingShopService');
const {
DEFAULT_PDD_PROPERTY_MODEL,
normalizePddPropertyRecommendations,
} = require('../src/services/selectionFissionAiSettings');
function createItem() {
return {
selectedProductTitle: '现代简约实木餐桌',
metadata: {
sourceRow: {
product: {
mainImages: ['https://img.example/shared.jpg', 'https://img.example/main-2.jpg'],
detailImages: ['https://img.example/shared.jpg', 'https://img.example/detail-1.jpg'],
attributes: [{ name: '主材质', value: '实木' }],
},
},
},
mainImageGeneration: { rows: [] },
skuSceneImages: {
selectedSkuCodes: ['SKU-1', 'SKU-2'],
rows: [
{ skuCode: 'SKU-1', generatedImageUrl: 'https://img.example/sku-shared.jpg' },
{ skuCode: 'SKU-2', generatedImageUrl: 'https://img.example/sku-shared.jpg' },
],
},
skuSnapshot: {
rows: [
{ skuCode: 'SKU-1', skuImageUrl: 'https://img.example/source-1.jpg' },
{ skuCode: 'SKU-2', skuImageUrl: 'https://img.example/source-2.jpg' },
],
},
pricingRows: [],
};
}
test('Excel contains the configured category mapping and locks mapped values', () => {
assert.equal(findCategoryMapping({ categoryName: '普通餐桌' }).categoryName, '餐桌');
const categoryRule = {
properties: [{
refPid: 373,
name: '主材质',
values: [{ vid: 12859, value: '木' }, { vid: 12860, value: '金属' }],
}],
};
const excel = resolveExcelMappedProperties({
categoryName: '普通餐桌',
sourceAttributes: [{ name: '主材质', value: '进口橡胶木实木' }],
categoryRule,
});
assert.deepEqual(excel.properties.map((row) => ({
refPid: row.refPid,
vid: row.vid,
value: row.value,
source: row.source,
locked: row.locked,
})), [{ refPid: 373, vid: 12859, value: '木', source: 'excel', locked: true }]);
const merged = mergePropertyRecommendations(categoryRule, excel, [{
refPid: 373,
vid: 12860,
value: '金属',
evidence: 'AI错误结果',
}]);
assert.equal(merged[0].value, '木');
assert.equal(merged[0].source, 'excel');
});
test('AI recommendations are limited to live PDD property IDs and enum values', () => {
const categoryRule = {
properties: [{
refPid: 1628,
name: '环保等级',
values: [{ vid: 368832, value: 'E0级' }],
}],
};
assert.equal(DEFAULT_PDD_PROPERTY_MODEL, 'qwen3.8-max');
assert.deepEqual(normalizePddPropertyRecommendations({
properties: [
{ refPid: 1628, vid: 368832, value: 'E0级', confidence: 0.9 },
{ refPid: 1628, vid: 999, value: '虚构等级' },
{ refPid: 9999, value: '虚构属性' },
],
}, categoryRule), [{
refPid: 1628,
vid: 368832,
value: 'E0级',
propertyName: '环保等级',
evidence: '',
confidence: 0.9,
}]);
});
test('main, detail and SKU OCR images are globally deduplicated', () => {
const rows = collectPddAttributeImages(createItem());
assert.deepEqual(rows.map((row) => [row.kind, row.index, row.url]), [
['main', 1, 'https://img.example/shared.jpg'],
['main', 2, 'https://img.example/main-2.jpg'],
['detail', 1, 'https://img.example/detail-1.jpg'],
['sku', 1, 'https://img.example/sku-shared.jpg'],
]);
});
test('OCR request enables X-Image-OCR and extracts data.ocr', async () => {
let request;
const result = await recognizeImageText({
kind: 'detail',
label: '详情图',
index: 1,
url: 'https://img.example/detail.jpg',
}, {
config: { endpointUrl: 'https://ocr.example/query', token: 'secret', timeoutMs: 1000 },
httpClient: {
post: async (url, body, config) => {
request = { url, body, config };
return {
status: 200,
data: {
success: true,
request_id: 'request-1',
data: { ocr: { requested: true, success: true, fullText: '实木', textCount: 1 } },
},
};
},
},
});
assert.equal(request.config.headers['X-Image-OCR'], 'true');
assert.equal(request.body.image_url, 'https://img.example/detail.jpg');
assert.equal(result.fullText, '实木');
assert.equal(result.requestId, 'request-1');
});
test('attribute workflow reports every image before AI confirmation', async () => {
const progress = [];
const result = await preparePddDraftAttributes({
item: createItem(),
categoryName: '普通餐桌',
categoryPathNames: ['住宅家具', '桌子', '普通餐桌'],
productTitle: '现代简约实木餐桌',
sourceCategory: '餐桌',
sourceAttributes: [{ name: '主材质', value: '实木' }],
categoryRule: {
properties: [{ refPid: 373, name: '主材质', values: [{ vid: 12859, value: '木' }] }],
},
}, {
config: { endpointUrl: 'https://ocr.example/query', token: 'secret', timeoutMs: 1000 },
httpClient: {
post: async () => ({
status: 200,
data: { success: true, data: { ocr: { requested: true, success: true, fullText: '实木' } } },
}),
},
updateProgress: async (row) => progress.push(row),
generateRecommendations: async () => ({ model: 'qwen3.8-max', properties: [] }),
});
assert.equal(progress.filter((row) => row.stage === 'ocr' && row.currentImage).length, 4);
assert.equal(progress.at(-1).stage, 'confirmation');
assert.equal(result.properties[0].source, 'excel');
});
test('publishing shop list exposes only safe fields and resolves credentials server-side', async () => {
const row = {
shopName: '拼多多-HX旗舰店107',
nick: 'pdd86127727389',
tokenExpiredTime: new Date('2027-01-01T00:00:00Z'),
mallId: '861277273',
accessToken: 'access-token',
refreshToken: 'refresh-token',
clientId: 'client-id',
clientSecret: 'client-secret',
serviceId: 639,
};
const calls = [];
const query = async (sql, params) => {
calls.push({ sql, params });
return [row];
};
const list = await listPddPublishingShops({ query });
assert.equal(list.rows[0].shopName, row.shopName);
assert.equal(list.rows[0].accessToken, undefined);
assert.match(calls[0].sql, /shop_status = N'正常'/);
assert.match(calls[0].sql, /NOT LIKE N'%已关闭%'/);
const resolved = await resolvePddPublishingShop({ shopId: row.nick }, { query });
assert.equal(resolved.shop.shopId, row.nick);
assert.equal(resolved.credential.accessToken, 'access-token');
assert.equal(resolved.credential.clientSecret, 'client-secret');
});