forked from lunanting/cangku
3460 lines
122 KiB
JavaScript
3460 lines
122 KiB
JavaScript
'use strict';
|
||
|
||
const test = require('node:test');
|
||
const assert = require('node:assert/strict');
|
||
const axios = require('axios');
|
||
const database = require('../src/config/database');
|
||
const planningDatabase = require('../src/services/planningDatabase');
|
||
const { normalizeImageGenerationConfig } = require('../src/services/selectionFissionAiSettings');
|
||
const {
|
||
requestQwenImage,
|
||
resolveQwenImageDimensions,
|
||
} = require('../src/services/selectionImageGenerationService');
|
||
const {
|
||
_test: {
|
||
advanceWorkflowProgress,
|
||
mapPlan,
|
||
mapPricingRow,
|
||
mapActivityPricingRow,
|
||
buildDeliveryPayload,
|
||
buildDeliveryReadiness,
|
||
buildDeliveryTaskPayload,
|
||
buildDeliveryVideoPayload,
|
||
buildEmptyPddActivityPrices,
|
||
buildMarketProductDetailUrl,
|
||
buildMarketReferencePriceBand,
|
||
buildAlibaba1688SkuSnapshot,
|
||
resolveAlibaba1688GlobalEncoding,
|
||
applyAlibaba1688GlobalEncodingToSnapshot,
|
||
buildAlibaba1688GlobalEncodingState,
|
||
buildAlibaba1688SourceReadiness,
|
||
buildAlibaba1688PricingEvidence,
|
||
buildAlibaba1688ListingCompliance,
|
||
getPddDraftItemConfig,
|
||
assertAlibaba1688PddDraftPlan,
|
||
buildSkuSelectionAssetsFromSnapshot,
|
||
formatImageAspectRatio,
|
||
hydrateDeliveryVideoAssets,
|
||
buildDeliveryMaterials,
|
||
getSkuSelectionImage,
|
||
getMainGenerationSourceImage,
|
||
getSimilarProductsConfig,
|
||
getSimilarProductsRequestHeaders,
|
||
createSimilarProductsRequestError,
|
||
isRetryableSimilarProductsError,
|
||
isSimilarProductsFallbackEligible,
|
||
getWorkflowSelectedSkuCodes,
|
||
getWorkflowSelectedSkuRows,
|
||
buildBulkSkuNameSelections,
|
||
getConfiguredMainImageCount,
|
||
isSelectionFissionSpuCurrent,
|
||
filterWorkflowRowsBySelectedSkus,
|
||
getWorkflowProductTitleOptions,
|
||
getCurrentTitleKeywordStrategy,
|
||
getTaskForbiddenBrandTerms,
|
||
findForbiddenTitleBrandTerm,
|
||
extractReferenceBrandTerms,
|
||
validateAdditionalProductTitleCandidates,
|
||
prepareSkuSceneImageRows,
|
||
groupSkuSceneRowsBySourceImage,
|
||
splitIntoBatches,
|
||
summarizeSkuSceneImageRows,
|
||
finalizeSourceSkuImageRows,
|
||
resetInterruptedSkuSceneImageState,
|
||
mergeSelectedSkuRowsWithLegacySnapshot,
|
||
batchUpdateSelectionPricingRows,
|
||
insertRowsInBatches,
|
||
getSelectionFissionItemNodeState,
|
||
getRelistableSelectionTasks,
|
||
hashClaimToken,
|
||
normalizeClaimToken,
|
||
normalizeSqlBindValues,
|
||
assertValidClaimToken,
|
||
fetchCompanySkusFromJstApi,
|
||
hydrateCompanySpuCandidateDetails,
|
||
mapJstCompanySkuRow,
|
||
normalizeAutoListingProductId,
|
||
normalizeProfitPricingRows,
|
||
pruneSelectionSkuPayload,
|
||
normalizeListingShop,
|
||
buildSelectionFissionExecutionOptions,
|
||
canDelegateListingExecution,
|
||
resolveMinimumPrice,
|
||
resolvePreservedSkuNameSelection,
|
||
normalizeMarketItemLookupId,
|
||
normalizeMarketReferenceCandidate,
|
||
normalizePriceBandList,
|
||
sortPriceBandValues,
|
||
inferMarketChannelName,
|
||
normalizeSelectionFissionPlanItems,
|
||
normalizeSimilarCandidate,
|
||
normalizeSimilarResponsePayload,
|
||
normalizeWhiteBackgroundSkuAssetDetails,
|
||
collectMarketReferenceCandidates,
|
||
fillMonthlySalesTrend6m,
|
||
normalizeSpuSelections,
|
||
parseCompanySkuMarkdown,
|
||
parseMarketReferenceMarkdown,
|
||
filterStrictMarketReferenceCandidates,
|
||
hasObviousMarketCategoryConflict,
|
||
supplementMarketReferenceCandidates,
|
||
rankSpuCandidatesByCategory,
|
||
validateCompanySpuCandidates,
|
||
getCompanyCategoryTerms,
|
||
parseImageDimensions,
|
||
requestSelectionGeneratedImage,
|
||
generateMainReplacementImage,
|
||
generateSkuSceneImage,
|
||
detectImageContent,
|
||
getExternalRequestErrorMessage,
|
||
hasStandardImageExtension,
|
||
shouldRehostImageGenerationReference,
|
||
normalizeImageGenerationReferenceUrl,
|
||
buildMainImageReferenceImages,
|
||
normalizeMainImageRetryIndexes,
|
||
isSelectionWorkflowTaskStepComplete,
|
||
collectTitleStrategyKeywords,
|
||
matchTitleKeywordEvidence,
|
||
buildWorkflowSummary,
|
||
toMysqlDateTime,
|
||
},
|
||
} = require('../src/services/planningSelectionFissionService');
|
||
|
||
test('listing execution options prefer self and only expose same-shop same-department delegates', () => {
|
||
const shops = [
|
||
{
|
||
id: 1, dingtalk_user_name: '当前用户', shop_id: 'shop-a', shop_name: '旗舰店',
|
||
platform: 'PDD', status: 'active', client_id: 'self-client', login_status: 'login_required',
|
||
},
|
||
{
|
||
id: 2, dingtalk_user_name: '同事甲', shop_id: 'shop-a', shop_name: '旗舰店',
|
||
platform: 'PDD', status: 'active', client_id: 'team-client', login_status: 'authenticated',
|
||
},
|
||
{
|
||
id: 3, dingtalk_user_name: '同事甲', shop_id: 'shop-b', shop_name: '其他店铺',
|
||
platform: 'PDD', status: 'active', client_id: 'team-client', login_status: 'authenticated',
|
||
},
|
||
{
|
||
id: 4, dingtalk_user_name: '其他部门', shop_id: 'shop-a', shop_name: '旗舰店',
|
||
platform: 'PDD', status: 'active', client_id: 'other-client', login_status: 'authenticated',
|
||
},
|
||
];
|
||
const presence = {
|
||
users: [
|
||
{
|
||
userId: 10, name: '当前用户', username: 'self', departmentName: '运营中心B', isOnline: true,
|
||
devices: [{ clientId: 'self-client', deviceName: '我的 Mac', isOnline: true }],
|
||
},
|
||
{
|
||
userId: 11, name: '同事甲', username: 'peer', departmentName: '运营中心B', isOnline: true,
|
||
devices: [{ clientId: 'team-client', deviceName: '同事电脑', isOnline: true }],
|
||
},
|
||
{
|
||
userId: 12, name: '其他部门', username: 'outsider', departmentName: '企划中心', isOnline: true,
|
||
devices: [{ clientId: 'other-client', deviceName: '其他电脑', isOnline: true }],
|
||
},
|
||
],
|
||
};
|
||
const result = buildSelectionFissionExecutionOptions(shops, presence, {
|
||
id: 10, name: '当前用户', username: 'self', departmentName: '运营中心B', role: 'user',
|
||
});
|
||
|
||
assert.equal(result.shops.length, 1);
|
||
assert.equal(result.shops[0].shopId, 'shop-a');
|
||
assert.equal(result.shops[0].availableSessionCount, 1);
|
||
assert.equal(result.shops[0].recommendedSessionId, '2');
|
||
assert.equal(result.sessions.find((session) => session.sessionId === '2').delegated, true);
|
||
assert.equal(result.recommended, null);
|
||
assert.equal(canDelegateListingExecution(
|
||
{ name: '当前用户', departmentName: '运营中心B' },
|
||
{ name: '同事甲', departmentName: '运营中心B' }
|
||
), true);
|
||
assert.equal(canDelegateListingExecution(
|
||
{ name: '当前用户', departmentName: '运营中心B' },
|
||
{ name: '其他部门', departmentName: '企划中心' }
|
||
), false);
|
||
});
|
||
|
||
test('listing execution options recommend the requester authenticated session first', () => {
|
||
const result = buildSelectionFissionExecutionOptions([
|
||
{
|
||
id: 8, dingtalk_user_name: '当前用户', shop_id: 'shop-a', shop_name: '旗舰店',
|
||
platform: 'PDD', status: 'active', client_id: 'self-client', login_status: 'authenticated',
|
||
},
|
||
{
|
||
id: 9, dingtalk_user_name: '同事甲', shop_id: 'shop-a', shop_name: '旗舰店',
|
||
platform: 'PDD', status: 'active', client_id: 'team-client', login_status: 'authenticated',
|
||
},
|
||
], {
|
||
users: [
|
||
{
|
||
userId: 10, name: '当前用户', username: 'self', departmentName: '运营中心B', isOnline: true,
|
||
devices: [{ clientId: 'self-client', deviceName: '我的 Mac', isOnline: true }],
|
||
},
|
||
{
|
||
userId: 11, name: '同事甲', username: 'peer', departmentName: '运营中心B', isOnline: true,
|
||
devices: [{ clientId: 'team-client', deviceName: '同事电脑', isOnline: true }],
|
||
},
|
||
],
|
||
}, {
|
||
id: 10, name: '当前用户', username: 'self', departmentName: '运营中心B', role: 'user',
|
||
});
|
||
|
||
assert.deepEqual(result.recommended, { listingShopId: 'shop-a', executorSessionId: '8' });
|
||
assert.equal(result.shops[0].recommendedSessionId, '8');
|
||
});
|
||
|
||
function createVerifiedSkuImageRow({
|
||
skuCode,
|
||
skuName = skuCode,
|
||
sourceImageUrl = '',
|
||
generatedImageUrl,
|
||
mode = 'source_original',
|
||
marketReferenceImageUrl = '',
|
||
status = 'completed',
|
||
error = '',
|
||
}) {
|
||
return {
|
||
skuCode,
|
||
skuName,
|
||
originalImageUrl: sourceImageUrl,
|
||
generatedImageUrl,
|
||
generationMode: mode,
|
||
marketReferenceImageUrl: mode === 'market_composite' ? marketReferenceImageUrl : '',
|
||
generatedForSkuCode: String(skuCode).trim().toUpperCase(),
|
||
generatedForSkuName: skuName,
|
||
generatedFromImageUrl: sourceImageUrl,
|
||
status,
|
||
progress: 100,
|
||
error,
|
||
};
|
||
}
|
||
|
||
test('interrupted SKU image recovery preserves completed images and resets only unfinished work', () => {
|
||
const recovered = resetInterruptedSkuSceneImageState({
|
||
currentBatch: 3,
|
||
batchTotal: 3,
|
||
retrySkuCodes: ['SKU-11'],
|
||
rows: [
|
||
{ skuCode: 'SKU-10', status: 'completed', progress: 100, generatedImageUrl: 'https://img.example/10.jpg' },
|
||
{ skuCode: 'SKU-11', status: 'processing', progress: 20, generatedImageUrl: '', error: '' },
|
||
{ skuCode: 'SKU-12', status: 'waiting', progress: 0, generatedImageUrl: '' },
|
||
],
|
||
});
|
||
|
||
assert.equal(recovered.currentBatch, 0);
|
||
assert.deepEqual(recovered.retrySkuCodes, []);
|
||
assert.equal(recovered.completedCount, 1);
|
||
assert.equal(recovered.rows[0].generatedImageUrl, 'https://img.example/10.jpg');
|
||
assert.equal(recovered.rows[0].status, 'completed');
|
||
assert.equal(recovered.rows[1].status, 'waiting');
|
||
assert.equal(recovered.rows[1].progress, 0);
|
||
assert.equal(recovered.rows[2].status, 'waiting');
|
||
});
|
||
const {
|
||
buildAlibaba1688SkuKey,
|
||
buildAlibaba1688SkuFingerprint,
|
||
} = require('../src/services/alibaba1688SkuIdentity');
|
||
|
||
test('1688 draft config is selected per plan item and ordinary plans are rejected', () => {
|
||
const input = {
|
||
pddDraftItems: [
|
||
{ planItemId: 101, catId: 10607 },
|
||
{ planItemId: 102, catId: 10553 },
|
||
],
|
||
};
|
||
assert.equal(getPddDraftItemConfig(input, 102).catId, 10553);
|
||
assert.equal(getPddDraftItemConfig(input, 999), null);
|
||
assert.doesNotThrow(() => assertAlibaba1688PddDraftPlan({
|
||
workflowProfile: 'alibaba_1688_pending',
|
||
workflowStatus: 'completed',
|
||
workflowProgress: 100,
|
||
}));
|
||
assert.throws(
|
||
() => assertAlibaba1688PddDraftPlan({ workflowProfile: 'ordinary' }),
|
||
(error) => error.code === 'PDD_DRAFT_WORKFLOW_UNSUPPORTED'
|
||
);
|
||
});
|
||
|
||
test('company SPU candidates always hydrate display images independently from operating metrics', async () => {
|
||
const calls = [];
|
||
const candidates = [{
|
||
spuCode: 'CZ0009',
|
||
spuName: '',
|
||
imageUrl: '',
|
||
skuCount: 0,
|
||
operatingMetrics: { minCost: null, maxCost: null, material: '' },
|
||
}];
|
||
|
||
const rows = await hydrateCompanySpuCandidateDetails(candidates, async (spuCode, options) => {
|
||
calls.push({ spuCode, options });
|
||
return {
|
||
rows: [{
|
||
skuCode: 'CZ0009N01V01',
|
||
skuName: '胡桃色床头柜',
|
||
productName: '实木床头柜',
|
||
skuImageUrl: 'https://img.alicdn.com/example.jpg',
|
||
material: '橡木',
|
||
cost: 128,
|
||
}],
|
||
};
|
||
});
|
||
|
||
assert.deepEqual(calls, [{
|
||
spuCode: 'CZ0009',
|
||
options: { limit: 300, includeInactive: true },
|
||
}]);
|
||
assert.equal(rows[0].imageUrl, 'https://img.alicdn.com/example.jpg');
|
||
assert.equal(rows[0].spuName, '实木床头柜');
|
||
assert.equal(rows[0].skuCount, 1);
|
||
assert.equal(rows[0].operatingMetrics.material, '橡木');
|
||
assert.equal(rows[0].operatingMetrics.minCost, 128);
|
||
assert.equal(rows[0].operatingMetrics.maxCost, 128);
|
||
});
|
||
|
||
test('company SPU candidate realtime cost excludes discontinued SKUs but keeps retired SKU evidence', async () => {
|
||
const candidates = [{
|
||
spuCode: 'SZ0097',
|
||
spuName: '电脑桌',
|
||
imageUrl: 'https://example.com/existing.jpg',
|
||
skuCount: 14,
|
||
operatingMetrics: { minCost: null, maxCost: null, material: '板材' },
|
||
}];
|
||
|
||
const rows = await hydrateCompanySpuCandidateDetails(candidates, async () => ({
|
||
source: 'getJstSkusBySpu',
|
||
rows: [
|
||
{ skuCode: 'SZ0097-A', saleStatus: '销售', cost: 65.5 },
|
||
{ skuCode: 'SZ0097-B', saleStatus: '销售', cost: 110 },
|
||
{ skuCode: 'SZ0097-C', saleStatus: '退市', cost: 58 },
|
||
{ skuCode: 'SZ0097-D', saleStatus: '停产', cost: 145.5 },
|
||
],
|
||
}));
|
||
|
||
assert.equal(rows[0].operatingMetrics.minCost, 58);
|
||
assert.equal(rows[0].operatingMetrics.maxCost, 110);
|
||
assert.equal(rows[0].operatingMetrics.realtimeSkuCount, 4);
|
||
assert.equal(rows[0].operatingMetrics.realtimeCostSkuCount, 3);
|
||
assert.equal(rows[0].operatingMetrics.costSource, 'getJstSkusBySpu');
|
||
assert.equal(
|
||
rows[0].operatingMetrics.costMetricLabel,
|
||
'聚水潭实时接口成本区间(销售/退市 SKU,停产 SKU 不计;3 条有成本)'
|
||
);
|
||
});
|
||
|
||
test('company SPU candidate prefers realtime JST cost over the ERP mirror cost', async () => {
|
||
const candidates = [{
|
||
spuCode: 'SZ0097',
|
||
spuName: '电脑桌',
|
||
imageUrl: 'https://example.com/existing.jpg',
|
||
skuCount: 31,
|
||
operatingMetrics: { minCost: 58, maxCost: 137, material: '板材' },
|
||
}];
|
||
|
||
const rows = await hydrateCompanySpuCandidateDetails(candidates, async () => ({
|
||
source: 'getJstSkusBySpu',
|
||
rows: [
|
||
{ skuCode: 'SZ0097-A', saleStatus: '销售', cost: 65.5 },
|
||
{ skuCode: 'SZ0097-B', saleStatus: '销售', cost: 110 },
|
||
],
|
||
}));
|
||
|
||
assert.equal(rows[0].operatingMetrics.minCost, 65.5);
|
||
assert.equal(rows[0].operatingMetrics.maxCost, 110);
|
||
assert.equal(rows[0].operatingMetrics.costSource, 'getJstSkusBySpu');
|
||
});
|
||
|
||
test('company SPU candidate does not present fallback snapshot cost as realtime cost', async () => {
|
||
const candidates = [{
|
||
spuCode: 'SZ0097',
|
||
operatingMetrics: { minCost: null, maxCost: null, material: '' },
|
||
}];
|
||
|
||
const rows = await hydrateCompanySpuCandidateDetails(candidates, async () => ({
|
||
source: 'planning_may_product_snapshot_di',
|
||
rows: [{ skuCode: 'SZ0097-A', saleStatus: '销售', cost: 37.2 }],
|
||
}));
|
||
|
||
assert.equal(rows[0].operatingMetrics.minCost, null);
|
||
assert.equal(rows[0].operatingMetrics.maxCost, null);
|
||
});
|
||
|
||
test('similar-products config accepts a complete endpoint URL', () => {
|
||
const originalEndpoint = process.env.SELECTION_SIMILAR_PRODUCTS_ENDPOINT_URL;
|
||
process.env.SELECTION_SIMILAR_PRODUCTS_ENDPOINT_URL = 'https://qh.yydd.ai/open-api/image-search/similar-products';
|
||
try {
|
||
const config = getSimilarProductsConfig();
|
||
assert.equal(config.endpointUrl, 'https://qh.yydd.ai/open-api/image-search/similar-products');
|
||
} finally {
|
||
if (originalEndpoint === undefined) delete process.env.SELECTION_SIMILAR_PRODUCTS_ENDPOINT_URL;
|
||
else process.env.SELECTION_SIMILAR_PRODUCTS_ENDPOINT_URL = originalEndpoint;
|
||
}
|
||
});
|
||
|
||
test('similar-products request sends API key and bearer compatibility headers', () => {
|
||
assert.deepEqual(getSimilarProductsRequestHeaders('image-search-key'), {
|
||
Authorization: 'Bearer image-search-key',
|
||
'X-API-Key': 'image-search-key',
|
||
'Content-Type': 'application/json',
|
||
});
|
||
});
|
||
|
||
test('company image search treats the upstream 7701 refusal as retryable and fallback eligible', () => {
|
||
const error = {
|
||
response: {
|
||
status: 500,
|
||
data: {
|
||
message: 'request to http://127.0.0.1:7701/search failed, reason: connect ECONNREFUSED 127.0.0.1:7701',
|
||
},
|
||
},
|
||
};
|
||
|
||
const requestError = createSimilarProductsRequestError(error, '公司相似款查询失败');
|
||
|
||
assert.equal(requestError.code, 'SIMILAR_PRODUCTS_REQUEST_FAILED');
|
||
assert.equal(requestError.status, 500);
|
||
assert.equal(requestError.retryable, true);
|
||
assert.equal(isRetryableSimilarProductsError(requestError), true);
|
||
assert.equal(isSimilarProductsFallbackEligible(requestError), true);
|
||
});
|
||
|
||
test('company image search does not hide non-retryable authentication failures', () => {
|
||
const requestError = createSimilarProductsRequestError({
|
||
response: {
|
||
status: 401,
|
||
data: { message: 'invalid api key' },
|
||
},
|
||
}, '公司相似款查询失败');
|
||
|
||
assert.equal(requestError.retryable, false);
|
||
assert.equal(isSimilarProductsFallbackEligible(requestError), false);
|
||
});
|
||
|
||
test('1688 source products keep channel, category, image and source metadata in a new plan', () => {
|
||
const [item] = normalizeSelectionFissionPlanItems([{
|
||
marketItemId: '1688:620789798938',
|
||
channelName: '1688',
|
||
title: '翻斗鞋柜极窄多层家用门口收纳玄关柜',
|
||
shopName: '常州新佰厂家居有限公司',
|
||
categoryLevel1: '家装建材',
|
||
categoryLevel2: '柜类',
|
||
categoryLevel3: '鞋柜',
|
||
mainImageUrl: '//cbu01.alicdn.com/img/ibank/sample.jpg',
|
||
marketDetailUrl: 'https://detail.1688.com/offer/620789798938.html',
|
||
priceBandList: ['80-100', '100-120'],
|
||
sourceMetadata: { productId: '620789798938', source: 'alibaba-1688-products' },
|
||
}]);
|
||
|
||
assert.equal(item.marketItemId, '1688:620789798938');
|
||
assert.equal(item.channelName, '1688');
|
||
assert.equal(item.categoryLevel2, '柜类');
|
||
assert.equal(item.categoryLevel3, '鞋柜');
|
||
assert.equal(item.mainImageUrl, 'https://cbu01.alicdn.com/img/ibank/sample.jpg');
|
||
assert.deepEqual(item.priceBandList, ['80-100', '100-120']);
|
||
assert.equal(item.metadata.sourceMetadata.source, 'alibaba-1688-products');
|
||
});
|
||
|
||
test('1688 workflow keeps exact source SKU names and never invents a fallback SKU', () => {
|
||
const item = {
|
||
marketItemId: '1688:620789798938',
|
||
title: '翻斗鞋柜',
|
||
mainImageUrl: 'https://example.com/main.jpg',
|
||
metadata: {
|
||
sourceRow: {
|
||
source: 'alibaba_1688_products',
|
||
product: {
|
||
skus: [{
|
||
skuId: 'SKU-RED',
|
||
name: '红色 / 80cm',
|
||
image: 'https://example.com/red.jpg',
|
||
price: '88.50',
|
||
}],
|
||
},
|
||
},
|
||
},
|
||
};
|
||
|
||
const snapshot = buildAlibaba1688SkuSnapshot(item);
|
||
assert.equal(snapshot.rows.length, 1);
|
||
assert.equal(snapshot.rows[0].skuCode, 'SKU-RED');
|
||
assert.equal(snapshot.rows[0].skuName, '红色 / 80cm');
|
||
assert.equal(snapshot.rows[0].cost, 88.5);
|
||
|
||
const emptySnapshot = buildAlibaba1688SkuSnapshot({
|
||
...item,
|
||
metadata: { sourceRow: { source: 'alibaba_1688_products', product: { skus: [] } } },
|
||
});
|
||
assert.deepEqual(emptySnapshot.rows, []);
|
||
});
|
||
|
||
test('1688 workflow uses the SKU row price when the page total lags behind', () => {
|
||
const item = {
|
||
marketItemId: '1688:620789798938',
|
||
title: '实木电脑桌',
|
||
metadata: {
|
||
sourceRow: {
|
||
product: {
|
||
skus: [{
|
||
name: '原木色 / 140cm',
|
||
price: 427.18,
|
||
priceText: '商品价格:¥427.18',
|
||
rowPriceText: '¥603.64',
|
||
}],
|
||
},
|
||
},
|
||
},
|
||
};
|
||
|
||
const snapshot = buildAlibaba1688SkuSnapshot(item);
|
||
assert.equal(snapshot.rows[0].cost, 603.64);
|
||
assert.equal(snapshot.rows[0].purchasePrice, 603.64);
|
||
});
|
||
|
||
test('1688 workflow reuses the canonical global encoding by source SKU key', () => {
|
||
const sourceSkus = [
|
||
{
|
||
index: 1,
|
||
skuId: 'SOURCE-RED',
|
||
name: 'Red / 80cm',
|
||
specs: [{ name: 'Color', value: 'Red' }, { name: 'Size', value: '80cm' }],
|
||
price: 80,
|
||
},
|
||
{
|
||
index: 2,
|
||
skuId: 'SOURCE-BLUE',
|
||
name: 'Blue / 100cm',
|
||
specs: [{ name: 'Color', value: 'Blue' }, { name: 'Size', value: '100cm' }],
|
||
price: 100,
|
||
},
|
||
];
|
||
const item = {
|
||
workflowProfile: 'alibaba_1688_pending',
|
||
marketItemId: '1688:1073252781818',
|
||
title: 'Source table',
|
||
metadata: {
|
||
sourceRow: {
|
||
source: 'alibaba_1688_products',
|
||
product: { skus: sourceSkus },
|
||
},
|
||
},
|
||
};
|
||
const fingerprint = buildAlibaba1688SkuFingerprint(sourceSkus);
|
||
const encoding = {
|
||
productId: '1073252781818',
|
||
status: 'encoded',
|
||
styleCode: 'STYLE-001',
|
||
sourceSkuCount: 2,
|
||
sourceSkuFingerprint: fingerprint,
|
||
request: {
|
||
operationCenter: 'Center A',
|
||
categoryName: 'Furniture',
|
||
taxStatus: 'Taxed',
|
||
invoiceType: 'Normal',
|
||
installationService: 'None',
|
||
installationMode: 'Assembly',
|
||
},
|
||
response: { created: true },
|
||
encodedAt: '2026-08-19T10:00:00.000Z',
|
||
skuMappings: [
|
||
{
|
||
sourceSkuKey: buildAlibaba1688SkuKey(sourceSkus[1], 1),
|
||
sourceSkuIndex: 2,
|
||
productName: 'Blue table',
|
||
colorSpec: 'Blue;100cm',
|
||
productCode: 'GLOBAL-SKU-02',
|
||
},
|
||
{
|
||
sourceSkuKey: buildAlibaba1688SkuKey(sourceSkus[0], 0),
|
||
sourceSkuIndex: 1,
|
||
productName: 'Red table',
|
||
colorSpec: 'Red;80cm',
|
||
productCode: 'GLOBAL-SKU-01',
|
||
},
|
||
],
|
||
};
|
||
|
||
const resolved = resolveAlibaba1688GlobalEncoding(item, encoding);
|
||
assert.deepEqual(
|
||
resolved.skuMappings.map((mapping) => mapping.productCode),
|
||
['GLOBAL-SKU-01', 'GLOBAL-SKU-02']
|
||
);
|
||
|
||
const snapshot = applyAlibaba1688GlobalEncodingToSnapshot(
|
||
buildAlibaba1688SkuSnapshot(item),
|
||
resolved
|
||
);
|
||
assert.deepEqual(snapshot.rows[0].specs, [
|
||
{ name: 'Color', value: 'Red' },
|
||
{ name: 'Size', value: '80cm' },
|
||
]);
|
||
assert.deepEqual(snapshot.rows.map((row) => ({
|
||
skuCode: row.skuCode,
|
||
internalProductCode: row.internalProductCode,
|
||
})), [
|
||
{ skuCode: 'SOURCE-RED', internalProductCode: 'GLOBAL-SKU-01' },
|
||
{ skuCode: 'SOURCE-BLUE', internalProductCode: 'GLOBAL-SKU-02' },
|
||
]);
|
||
|
||
const state = buildAlibaba1688GlobalEncodingState(resolved);
|
||
assert.equal(state.status, 'completed');
|
||
assert.equal(state.styleCode, 'STYLE-001');
|
||
assert.equal(state.requestHash.length, 64);
|
||
assert.equal(state.config.operationCenter, 'Center A');
|
||
});
|
||
|
||
test('1688 workflow rejects stale or incomplete global encoding mappings', () => {
|
||
const sourceSkus = [{
|
||
index: 1,
|
||
name: 'Red / 80cm',
|
||
specs: [{ name: 'Size', value: '80cm' }],
|
||
}];
|
||
const item = {
|
||
workflowProfile: 'alibaba_1688_pending',
|
||
marketItemId: '1073252781818',
|
||
metadata: { sourceRow: { product: { skus: sourceSkus } } },
|
||
};
|
||
const baseEncoding = {
|
||
productId: '1073252781818',
|
||
status: 'encoded',
|
||
sourceSkuCount: 1,
|
||
sourceSkuFingerprint: buildAlibaba1688SkuFingerprint(sourceSkus),
|
||
skuMappings: [{
|
||
sourceSkuKey: buildAlibaba1688SkuKey(sourceSkus[0], 0),
|
||
productCode: 'GLOBAL-SKU-01',
|
||
}],
|
||
};
|
||
|
||
assert.throws(
|
||
() => resolveAlibaba1688GlobalEncoding(item, {
|
||
...baseEncoding,
|
||
sourceSkuFingerprint: 'changed-fingerprint',
|
||
}),
|
||
(error) => error.code === 'ALIBABA_1688_PRODUCT_ENCODING_STALE'
|
||
);
|
||
assert.throws(
|
||
() => resolveAlibaba1688GlobalEncoding(item, {
|
||
...baseEncoding,
|
||
skuMappings: [],
|
||
}),
|
||
(error) => error.code === 'ALIBABA_1688_PRODUCT_ENCODING_MAPPING_INCOMPLETE'
|
||
);
|
||
assert.throws(
|
||
() => resolveAlibaba1688GlobalEncoding(item, null),
|
||
(error) => error.code === 'ALIBABA_1688_PRODUCT_NOT_ENCODED'
|
||
);
|
||
});
|
||
|
||
test('1688 workflow exposes source names and titles without changing ordinary workflow rules', () => {
|
||
const aiTitles = ['AI标题1', 'AI标题2', 'AI标题3', 'AI标题4', 'AI标题5'];
|
||
const alibabaItem = {
|
||
title: '1688原始标题',
|
||
workflowProfile: 'alibaba_1688_pending',
|
||
optimizedTitleCandidates: aiTitles,
|
||
};
|
||
const ordinaryItem = {
|
||
title: '市场来源标题',
|
||
optimizedTitleCandidates: aiTitles,
|
||
};
|
||
|
||
assert.deepEqual(getWorkflowProductTitleOptions(alibabaItem), ['1688原始标题', ...aiTitles]);
|
||
assert.deepEqual(getWorkflowProductTitleOptions(ordinaryItem), aiTitles);
|
||
assert.equal(resolvePreservedSkuNameSelection({
|
||
originalSkuName: '1688原始SKU名',
|
||
names: ['AI SKU 1', 'AI SKU 2'],
|
||
}, '1688原始SKU名'), '1688原始SKU名');
|
||
assert.equal(resolvePreservedSkuNameSelection({
|
||
originalSkuName: '1688原始SKU名',
|
||
names: ['AI SKU 1', 'AI SKU 2'],
|
||
}, '旧的无效名称'), '');
|
||
});
|
||
|
||
test('1688 source readiness requires real title, images, SKUs and purchase prices', () => {
|
||
const completeItem = {
|
||
workflowProfile: 'alibaba_1688_pending',
|
||
title: '1688原始商品标题',
|
||
mainImageUrl: 'https://example.com/main.jpg',
|
||
metadata: {
|
||
sourceRow: {
|
||
product: {
|
||
sourceUrl: 'https://detail.1688.com/offer/123456.html',
|
||
storeName: '源头工厂',
|
||
mainImage: 'https://example.com/main.jpg',
|
||
detailImages: ['https://example.com/detail-1.jpg'],
|
||
skus: [{
|
||
skuId: 'SKU-A',
|
||
name: '白色 / 80cm',
|
||
image: 'https://example.com/sku-a.jpg',
|
||
price: '88.50',
|
||
}],
|
||
},
|
||
},
|
||
},
|
||
};
|
||
|
||
const complete = buildAlibaba1688SourceReadiness(completeItem);
|
||
assert.equal(complete.complete, true);
|
||
assert.equal(complete.facts.detailImageCount, 1);
|
||
assert.equal(complete.facts.pricedSkuCount, 1);
|
||
|
||
const incomplete = buildAlibaba1688SourceReadiness({
|
||
workflowProfile: 'alibaba_1688_pending',
|
||
metadata: { sourceRow: { product: { skus: [{ skuId: 'SKU-A', name: '白色' }] } } },
|
||
});
|
||
assert.equal(incomplete.complete, false);
|
||
assert.deepEqual(
|
||
incomplete.blockingIssues.map((row) => row.field),
|
||
['title', 'mainImages', 'detailImages', 'skuPrices']
|
||
);
|
||
});
|
||
|
||
test('1688 pricing evidence preserves missing freight and stock instead of inventing values', () => {
|
||
const evidence = buildAlibaba1688PricingEvidence({
|
||
workflowProfile: 'alibaba_1688_pending',
|
||
metadata: {
|
||
sourceRow: {
|
||
product: {
|
||
priceText: '88.50-99.00',
|
||
minPrice: 88.5,
|
||
maxPrice: 99,
|
||
skus: [{ skuId: 'SKU-A', name: '白色', price: '88.50' }],
|
||
},
|
||
},
|
||
},
|
||
});
|
||
|
||
assert.equal(evidence.freightEvidenceCount, 0);
|
||
assert.equal(evidence.stockEvidenceCount, 0);
|
||
assert.equal(evidence.rows[0].purchasePrice, 88.5);
|
||
assert.equal(evidence.rows[0].freight, null);
|
||
assert.equal(evidence.rows[0].stock, null);
|
||
});
|
||
|
||
test('1688 pricing SQL values preserve missing evidence as database NULL', () => {
|
||
assert.deepEqual(
|
||
normalizeSqlBindValues(['SKU-A', undefined, NaN, Infinity, 0, null]),
|
||
['SKU-A', null, null, null, 0, null]
|
||
);
|
||
assert.equal(resolveMinimumPrice({ skuCode: 'SKU-A' }, { minimumPricesBySku: {} }), null);
|
||
});
|
||
|
||
test('selection pricing updates all SKU rows in one database statement', async () => {
|
||
const calls = [];
|
||
const connection = {
|
||
execute: async (sql, values) => calls.push({ sql, values }),
|
||
};
|
||
await batchUpdateSelectionPricingRows(
|
||
connection,
|
||
'pricing_rows',
|
||
88,
|
||
[
|
||
{ skuCode: 'SKU-A', price: 99 },
|
||
{ skuCode: 'SKU-B', price: 109 },
|
||
],
|
||
[['group_price', (row) => row.price]],
|
||
['updated_at = NOW()']
|
||
);
|
||
|
||
assert.equal(calls.length, 1);
|
||
assert.match(calls[0].sql, /CASE UPPER\(sku_code\)/);
|
||
assert.match(calls[0].sql, /UPPER\(sku_code\) IN \(\?, \?\)/);
|
||
assert.deepEqual(calls[0].values, ['SKU-A', 99, 'SKU-B', 109, 88, 'SKU-A', 'SKU-B']);
|
||
});
|
||
|
||
test('pricing rows are inserted in bounded batches with normalized bind values', async () => {
|
||
const calls = [];
|
||
const connection = {
|
||
execute: async (sql, values) => calls.push({ sql, values }),
|
||
};
|
||
const rows = Array.from({ length: 53 }, (_, index) => [index + 1, undefined]);
|
||
|
||
await insertRowsInBatches(connection, 'pricing_rows', ['id', 'optional_value'], rows, 25);
|
||
|
||
assert.equal(calls.length, 3);
|
||
assert.equal(calls[0].values.length, 50);
|
||
assert.equal(calls[1].values.length, 50);
|
||
assert.equal(calls[2].values.length, 6);
|
||
assert.ok(calls.every((call) => !call.values.includes(undefined)));
|
||
assert.equal(calls[0].values[1], null);
|
||
});
|
||
|
||
test('1688 listing compliance blocks unconfirmed operational decisions', () => {
|
||
const compliance = buildAlibaba1688ListingCompliance({
|
||
workflowProfile: 'alibaba_1688_pending',
|
||
title: '1688原始标题',
|
||
mainImageUrl: 'https://example.com/main.jpg',
|
||
metadata: {
|
||
sourceRow: {
|
||
product: {
|
||
detailImages: ['https://example.com/detail.jpg'],
|
||
skus: [{ skuId: 'SKU-A', name: '白色', price: '88.50' }],
|
||
},
|
||
},
|
||
},
|
||
skuSnapshot: {
|
||
rows: [{ skuCode: 'SKU-A', skuName: '白色', purchasePrice: 88.5 }],
|
||
},
|
||
}, {});
|
||
|
||
assert.equal(compliance.complete, false);
|
||
assert.ok(compliance.blockers.some((row) => row.field === 'selectedProductTitle'));
|
||
assert.ok(compliance.blockers.some((row) => row.field === 'pricing'));
|
||
assert.ok(compliance.blockers.some((row) => row.field === 'mainImages'));
|
||
});
|
||
|
||
test('company candidate supplementation prefers level 3 and falls back to level 2', () => {
|
||
assert.deepEqual(getCompanyCategoryTerms({ level2: '柜类', level3: '餐边柜' }), ['餐边柜', '柜类', '柜']);
|
||
assert.deepEqual(getCompanyCategoryTerms({ level2: '柜类', level3: '柜类' }), ['柜类', '柜']);
|
||
});
|
||
|
||
test('workflow keeps only user-selected SKUs for naming and pricing nodes', () => {
|
||
const task = {
|
||
skuSnapshot: {
|
||
rows: [
|
||
{ skuCode: 'SKU-A', skuName: 'A' },
|
||
{ skuCode: 'SKU-B', skuName: 'B' },
|
||
{ skuCode: 'SKU-C', skuName: 'C' },
|
||
],
|
||
},
|
||
skuSceneImages: {
|
||
selectedSkuCodes: ['SKU-A', 'SKU-C'],
|
||
rows: [
|
||
{ skuCode: 'SKU-A', generatedImageUrl: 'https://example.com/a.jpg' },
|
||
{ skuCode: 'SKU-C', generatedImageUrl: 'https://example.com/c.jpg' },
|
||
],
|
||
},
|
||
skuNameCandidates: {
|
||
rows: [
|
||
{ skuCode: 'SKU-A', names: ['A1', 'A2', 'A3', 'A4', 'A5'], selectedName: 'A1' },
|
||
{ skuCode: 'SKU-C', names: ['C1', 'C2', 'C3', 'C4', 'C5'], selectedName: 'C1' },
|
||
],
|
||
},
|
||
pricingRows: [{ skuCode: 'SKU-A', groupPrice: 100 }, { skuCode: 'SKU-C', groupPrice: 120 }],
|
||
activityPricingRows: [{ skuCode: 'SKU-A' }, { skuCode: 'SKU-C' }],
|
||
pricingTableSavedAt: '2026-08-02 10:00:00',
|
||
};
|
||
|
||
assert.deepEqual(getWorkflowSelectedSkuCodes(task), ['SKU-A', 'SKU-C']);
|
||
assert.deepEqual(getWorkflowSelectedSkuRows(task).map((row) => row.skuCode), ['SKU-A', 'SKU-C']);
|
||
assert.equal(isSelectionWorkflowTaskStepComplete(task, 6), true);
|
||
assert.equal(isSelectionWorkflowTaskStepComplete(task, 8), true);
|
||
});
|
||
|
||
test('bulk SKU-name selection applies the same candidate position to every selected SKU', () => {
|
||
const task = {
|
||
skuSnapshot: {
|
||
rows: [
|
||
{ skuCode: 'SKU-A', skuName: '原始名称A' },
|
||
{ skuCode: 'SKU-B', skuName: '原始名称B' },
|
||
{ skuCode: 'SKU-C', skuName: '原始名称C' },
|
||
],
|
||
},
|
||
skuSceneImages: { selectedSkuCodes: ['SKU-A', 'SKU-C'] },
|
||
skuNameCandidates: {
|
||
rows: [
|
||
{ skuCode: 'SKU-A', originalSkuName: '原始名称A', names: ['推荐A1', '推荐A2'] },
|
||
{ skuCode: 'SKU-B', originalSkuName: '原始名称B', names: ['推荐B1', '推荐B2'] },
|
||
{ skuCode: 'SKU-C', originalSkuName: '原始名称C', names: ['推荐C1', '推荐C2'] },
|
||
],
|
||
},
|
||
};
|
||
|
||
assert.deepEqual(buildBulkSkuNameSelections(task, 'candidate_2_all'), [
|
||
{ skuCode: 'SKU-A', selectedName: '推荐A2' },
|
||
{ skuCode: 'SKU-C', selectedName: '推荐C2' },
|
||
]);
|
||
assert.deepEqual(buildBulkSkuNameSelections(task, 'original_all'), [
|
||
{ skuCode: 'SKU-A', selectedName: '原始名称A' },
|
||
{ skuCode: 'SKU-C', selectedName: '原始名称C' },
|
||
]);
|
||
});
|
||
|
||
test('company SKU snapshots are accepted only for the task current SPU', () => {
|
||
assert.equal(isSelectionFissionSpuCurrent({ selectedSpuCode: 'DNZ0324' }, 'dnz0324'), true);
|
||
assert.equal(isSelectionFissionSpuCurrent({ selectedSpuCode: 'DNZ0247' }, 'DNZ0324'), false);
|
||
assert.equal(isSelectionFissionSpuCurrent({ selectedSpuCode: '' }, 'DNZ0324'), false);
|
||
});
|
||
|
||
test('workflow SKU matching is case-insensitive after an upstream refresh', () => {
|
||
const task = {
|
||
skuSnapshot: {
|
||
rows: [
|
||
{ skuCode: 'DNZ015401N79V02', skuName: 'legacy selected' },
|
||
{ skuCode: 'DNZ015499N79V02', skuName: 'legacy unselected' },
|
||
],
|
||
},
|
||
skuSceneImages: {
|
||
selectedSkuCodes: ['dnz015401n79v02', 'DNZ0154100N79V01'],
|
||
},
|
||
};
|
||
const refreshed = {
|
||
spuCode: 'DNZ0154',
|
||
rows: [{ skuCode: 'dnz0154100n79v01', skuName: 'current' }],
|
||
source: 'getJstSkusBySpu',
|
||
warnings: [],
|
||
};
|
||
|
||
const merged = mergeSelectedSkuRowsWithLegacySnapshot(task, refreshed);
|
||
assert.deepEqual(merged.rows.map((row) => row.skuCode), [
|
||
'dnz0154100n79v01',
|
||
'DNZ015401N79V02',
|
||
]);
|
||
assert.equal(merged.source, 'getJstSkusBySpu+legacy-selected');
|
||
assert.equal(merged.total, 2);
|
||
assert.equal(getWorkflowSelectedSkuRows(task, merged.rows).length, 2);
|
||
assert.match(merged.warnings[0], /retained from the previous snapshot/);
|
||
});
|
||
|
||
test('title strategy keywords are deduplicated and matched to demand evidence', () => {
|
||
const keywords = collectTitleStrategyKeywords({
|
||
category: '餐边柜',
|
||
scenes: ['客厅', '客厅'],
|
||
hotKeywords: ['餐边柜'],
|
||
material: '实木',
|
||
});
|
||
assert.deepEqual(keywords.map((row) => row.keyword), ['餐边柜', '实木', '客厅']);
|
||
|
||
const matched = matchTitleKeywordEvidence('客厅', [
|
||
{ searchKeyword: '客厅餐边柜', searchIndex: 500 },
|
||
{ searchKeyword: '客厅', searchIndex: 200 },
|
||
]);
|
||
assert.equal(matched.candidate, '客厅');
|
||
assert.equal(matched.score, 3);
|
||
});
|
||
|
||
test('market visual references reject zero-score and broad-category matches', () => {
|
||
const candidates = filterStrictMarketReferenceCandidates([
|
||
{ marketItemId: 'sideboard', score: 0.83, categoryMatchLevel: 3 },
|
||
{ marketItemId: 'shoe-rack', score: 0.91, categoryMatchLevel: 2 },
|
||
{ marketItemId: 'side-table', score: 0, categoryMatchLevel: 3 },
|
||
], {
|
||
level2: '柜类',
|
||
level3: '餐边柜',
|
||
});
|
||
|
||
assert.deepEqual(candidates.map((candidate) => candidate.marketItemId), ['sideboard']);
|
||
});
|
||
|
||
test('market references reject obvious cross-category image-search results', () => {
|
||
const categoryConstraint = {
|
||
level2: '桌面收纳',
|
||
level3: '桌面扩展板',
|
||
};
|
||
assert.equal(hasObviousMarketCategoryConflict({
|
||
title: '多层入户鞋架家用收纳架',
|
||
category: '住宅家具 / 架类',
|
||
}, categoryConstraint), true);
|
||
assert.equal(hasObviousMarketCategoryConflict({
|
||
title: '电脑桌面扩展板显示器增高架',
|
||
category: '办公家具 / 桌面收纳 / 桌面扩展板',
|
||
}, categoryConstraint), false);
|
||
});
|
||
|
||
test('market reference supplementation matches a source level-1 category against stored level-3 rows', async () => {
|
||
const originalQuery = planningDatabase.query;
|
||
let supplementSql = '';
|
||
let supplementParams = [];
|
||
planningDatabase.query = async (sql, params) => {
|
||
if (String(sql).includes('WHERE main_image_url = ?')) {
|
||
return [{ embeddingVectorJson: JSON.stringify([1, 0]) }];
|
||
}
|
||
supplementSql = String(sql);
|
||
supplementParams = params;
|
||
return [{
|
||
marketItemId: 'market-电脑桌-1',
|
||
sourceChannel: 'taobao',
|
||
title: '电脑桌办公桌',
|
||
imageUrl: 'https://img.example.com/desk.jpg',
|
||
categoryLevel3: '电脑桌',
|
||
embeddingVectorJson: JSON.stringify([1, 0]),
|
||
}];
|
||
};
|
||
|
||
try {
|
||
const candidates = await supplementMarketReferenceCandidates(
|
||
[],
|
||
{ level1: '电脑桌' },
|
||
100,
|
||
'https://img.example.com/source.jpg'
|
||
);
|
||
assert.equal(candidates.length, 1);
|
||
assert.equal(candidates[0].marketItemId, 'market-电脑桌-1');
|
||
assert.match(supplementSql, /category_level1 = \? OR category_level2 = \? OR category_level3 = \?/);
|
||
assert.deepEqual(supplementParams.slice(0, 3), ['电脑桌', '电脑桌', '电脑桌']);
|
||
assert.ok(Number(supplementParams[3]) >= 100);
|
||
} finally {
|
||
planningDatabase.query = originalQuery;
|
||
}
|
||
});
|
||
|
||
test('selection task stays processing until a callback product ID exists', () => {
|
||
const plan = mapPlan({
|
||
id: 12,
|
||
scheme_code: 'FB202607231431A1B2C3',
|
||
status: 'listed',
|
||
auto_listed_product_id: null,
|
||
});
|
||
|
||
assert.equal(plan.status, 'pending');
|
||
assert.equal(plan.autoListingStatus, '自动上架中');
|
||
assert.equal(plan.autoListingStatusCode, 'processing');
|
||
assert.equal(plan.autoListedProductId, '');
|
||
});
|
||
|
||
test('SG0088 JST SKU payload maps code, image, cost and minimum price', () => {
|
||
const row = mapJstCompanySkuRow({
|
||
i_id: 'SG0088',
|
||
sku_id: 'SG008830N1049V01',
|
||
name: '款式一 100/30/180 北美原木纹色',
|
||
category: '住宅家具 - 书柜',
|
||
cost_price: 151,
|
||
pic: 'https://img.example.com/SG008830N1049V01.jpg',
|
||
status: '销售',
|
||
stock: 55,
|
||
stockWarehouses: [{
|
||
warehouseId: 15038347,
|
||
warehouseName: '简尚-新苏北仓',
|
||
warehouseType: '成品发货仓-真实库存',
|
||
stock: 55,
|
||
}],
|
||
properties_value: '北美原木纹色',
|
||
nonOwnerLimitPrice: 302.596,
|
||
limitPriceTable: 'sku_bulk_price_limt',
|
||
});
|
||
|
||
assert.equal(row.spuCode, 'SG0088');
|
||
assert.equal(row.skuCode, 'SG008830N1049V01');
|
||
assert.equal(row.skuImageUrl, 'https://img.example.com/SG008830N1049V01.jpg');
|
||
assert.equal(row.cost, 151);
|
||
assert.equal(row.minimumPrice, 302.596);
|
||
assert.equal(row.saleStatus, '销售');
|
||
assert.equal(row.stock, 55);
|
||
assert.deepEqual(row.stockWarehouses, [{
|
||
warehouseId: '15038347',
|
||
warehouseName: '简尚-新苏北仓',
|
||
warehouseType: '成品发货仓-真实库存',
|
||
stock: 55,
|
||
}]);
|
||
assert.equal(row.sourceSystem, 'getJstSkusBySpu');
|
||
});
|
||
|
||
test('SKU scene generation splits all selected rows into batches of five', () => {
|
||
const rows = Array.from({ length: 12 }, (_, index) => ({ skuCode: `SKU-${index + 1}` }));
|
||
const batches = splitIntoBatches(rows);
|
||
|
||
assert.deepEqual(batches.map((batch) => batch.length), [5, 5, 2]);
|
||
assert.deepEqual(batches.flat().map((row) => row.skuCode), rows.map((row) => row.skuCode));
|
||
});
|
||
|
||
test('SKU scene retry preserves completed rows and resets only failed targets', () => {
|
||
const selectedSkuRows = [
|
||
{ skuCode: 'SKU-A' },
|
||
{ skuCode: 'SKU-B' },
|
||
{ skuCode: 'SKU-C' },
|
||
];
|
||
const task = {
|
||
skuSnapshot: {
|
||
rows: selectedSkuRows.map((row) => ({ ...row, skuImageUrl: `https://example.com/${row.skuCode}.jpg` })),
|
||
},
|
||
skuSceneImages: {
|
||
mode: 'source_original',
|
||
rows: [
|
||
createVerifiedSkuImageRow({
|
||
skuCode: 'SKU-A',
|
||
sourceImageUrl: 'https://example.com/SKU-A.jpg',
|
||
generatedImageUrl: 'https://example.com/generated-a.jpg',
|
||
}),
|
||
createVerifiedSkuImageRow({
|
||
skuCode: 'SKU-B',
|
||
sourceImageUrl: 'https://example.com/SKU-B.jpg',
|
||
generatedImageUrl: '',
|
||
status: 'failed',
|
||
error: '接口超时',
|
||
}),
|
||
createVerifiedSkuImageRow({
|
||
skuCode: 'SKU-C',
|
||
sourceImageUrl: 'https://example.com/SKU-C.jpg',
|
||
generatedImageUrl: '',
|
||
status: 'failed',
|
||
error: '接口限流',
|
||
}),
|
||
],
|
||
},
|
||
};
|
||
|
||
const rows = prepareSkuSceneImageRows(task, selectedSkuRows, ['SKU-B']);
|
||
assert.equal(rows[0].generatedImageUrl, 'https://example.com/generated-a.jpg');
|
||
assert.equal(rows[0].status, 'completed');
|
||
assert.equal(rows[1].status, 'waiting');
|
||
assert.equal(rows[1].progress, 0);
|
||
assert.equal(rows[1].error, '');
|
||
assert.equal(rows[2].status, 'failed');
|
||
assert.equal(rows[2].error, '接口限流');
|
||
assert.deepEqual(summarizeSkuSceneImageRows(rows), {
|
||
total: 3,
|
||
completedCount: 1,
|
||
failedCount: 1,
|
||
processedCount: 2,
|
||
progress: 67,
|
||
});
|
||
});
|
||
|
||
test('SKU scene completion checks every selected SKU instead of the first five', () => {
|
||
const selectedSkuCodes = Array.from({ length: 12 }, (_, index) => `SKU-${index + 1}`);
|
||
const task = {
|
||
skuSnapshot: {
|
||
rows: selectedSkuCodes.map((skuCode) => ({
|
||
skuCode,
|
||
skuImageUrl: `https://example.com/source-${skuCode}.jpg`,
|
||
})),
|
||
},
|
||
skuSceneImages: {
|
||
mode: 'source_original',
|
||
selectedSkuCodes,
|
||
rows: selectedSkuCodes.map((skuCode) => createVerifiedSkuImageRow({
|
||
skuCode,
|
||
sourceImageUrl: `https://example.com/source-${skuCode}.jpg`,
|
||
generatedImageUrl: `https://example.com/${skuCode}.jpg`,
|
||
})),
|
||
},
|
||
};
|
||
|
||
assert.equal(isSelectionWorkflowTaskStepComplete(task, 4), true);
|
||
task.skuSceneImages.rows[10].generatedImageUrl = '';
|
||
assert.equal(isSelectionWorkflowTaskStepComplete(task, 4), false);
|
||
});
|
||
|
||
test('SKU scene completion respects the user selected subset', () => {
|
||
const task = {
|
||
skuSnapshot: {
|
||
rows: [
|
||
{ skuCode: 'SKU-A', skuImageUrl: 'https://example.com/a.jpg' },
|
||
{ skuCode: 'SKU-B', skuImageUrl: 'https://example.com/b.jpg' },
|
||
],
|
||
},
|
||
skuSceneImages: {
|
||
mode: 'source_original',
|
||
selectedSkuCodes: ['SKU-A'],
|
||
rows: [createVerifiedSkuImageRow({
|
||
skuCode: 'SKU-A',
|
||
sourceImageUrl: 'https://example.com/a.jpg',
|
||
generatedImageUrl: 'https://example.com/generated-a.jpg',
|
||
})],
|
||
},
|
||
};
|
||
|
||
assert.equal(isSelectionWorkflowTaskStepComplete(task, 4), true);
|
||
task.skuSceneImages.rows.push({
|
||
...createVerifiedSkuImageRow({
|
||
skuCode: 'SKU-B',
|
||
sourceImageUrl: 'https://example.com/b.jpg',
|
||
generatedImageUrl: 'https://example.com/generated-b.jpg',
|
||
}),
|
||
});
|
||
assert.equal(isSelectionWorkflowTaskStepComplete(task, 4), true);
|
||
});
|
||
|
||
test('reference titles expose copied brand-model terms for title filtering', () => {
|
||
const forbiddenTerms = extractReferenceBrandTerms([
|
||
{ title: '钛载S3电脑桌家用卧室台式书桌', brandName: '' },
|
||
{ title: '钛载S3电竞桌机械臂游戏桌子', brandName: '' },
|
||
{ title: '林氏木业电脑桌现代简约书桌', brandName: '林氏木业' },
|
||
], {
|
||
categoryTerms: ['电脑桌', '电竞桌'],
|
||
});
|
||
|
||
assert.ok(forbiddenTerms.includes('钛载S3'));
|
||
assert.ok(forbiddenTerms.includes('林氏木业'));
|
||
assert.equal(findForbiddenTitleBrandTerm('钛载S3电脑桌家用简约', forbiddenTerms), '钛载S3');
|
||
assert.equal(findForbiddenTitleBrandTerm('现代简约家用电脑桌', forbiddenTerms), '');
|
||
});
|
||
|
||
test('title filtering uses only an adopted, category-matched market reference', () => {
|
||
const selectedMarketReferences = [{
|
||
title: '钛载S3电竞桌家用书桌',
|
||
brandName: '钛载S3',
|
||
categoryMatchLevel: 3,
|
||
}];
|
||
const marketReferenceBatch = {
|
||
candidates: [{ title: '林氏木业扫地机器人柜', categoryMatchLevel: 0 }],
|
||
};
|
||
|
||
assert.deepEqual(getTaskForbiddenBrandTerms({
|
||
mainImageGeneration: { mode: 'source_original' },
|
||
titleKeywordStrategy: {
|
||
forbiddenBrandTerms: ['旧错类品牌'],
|
||
referenceMode: 'market_composite',
|
||
},
|
||
selectedMarketReferences,
|
||
marketReferenceBatch,
|
||
categoryLevel3: '电脑桌',
|
||
}), []);
|
||
|
||
const adoptedTerms = getTaskForbiddenBrandTerms({
|
||
mainImageGeneration: { mode: 'market_composite' },
|
||
titleKeywordStrategy: {
|
||
forbiddenBrandTerms: ['已确认参考品牌'],
|
||
referenceMode: 'market_composite',
|
||
},
|
||
selectedMarketReferences,
|
||
marketReferenceBatch,
|
||
categoryLevel3: '电脑桌',
|
||
});
|
||
assert.ok(adoptedTerms.includes('钛载S3'));
|
||
assert.ok(adoptedTerms.includes('已确认参考品牌'));
|
||
assert.equal(adoptedTerms.includes('林氏木业'), false);
|
||
});
|
||
|
||
test('legacy title strategy is hidden when it does not match the current main-image mode', () => {
|
||
const staleStrategy = {
|
||
category: '收纳柜',
|
||
forbiddenBrandTerms: ['中古风实木扫地机器人'],
|
||
referenceMode: 'market_composite',
|
||
};
|
||
assert.equal(getCurrentTitleKeywordStrategy({
|
||
mainImageGeneration: { mode: 'source_original' },
|
||
titleKeywordStrategy: staleStrategy,
|
||
}), null);
|
||
assert.deepEqual(getCurrentTitleKeywordStrategy({
|
||
mainImageGeneration: { mode: 'market_composite' },
|
||
titleKeywordStrategy: staleStrategy,
|
||
}), staleStrategy);
|
||
});
|
||
|
||
test('main image target count follows the current reference selection before historical output', () => {
|
||
const task = {
|
||
workflowProfile: 'alibaba_1688_pending',
|
||
selectedMarketReferences: [
|
||
{ id: 'reference-1', imageUrl: 'https://example.com/reference-1.jpg' },
|
||
],
|
||
mainImageGeneration: {
|
||
total: 10,
|
||
rows: Array.from({ length: 10 }, (_, index) => ({
|
||
imageUrl: `https://example.com/generated-${index + 1}.jpg`,
|
||
})),
|
||
},
|
||
};
|
||
|
||
assert.equal(getConfiguredMainImageCount(task, 10), 1);
|
||
assert.equal(isSelectionWorkflowTaskStepComplete(task, 3), false);
|
||
});
|
||
|
||
test('market-composite main images require explicit structure review before advancing', () => {
|
||
const task = {
|
||
selectedMarketReferences: [{ id: 'reference-1', imageUrl: 'https://example.com/reference.jpg' }],
|
||
mainImageGeneration: {
|
||
mode: 'market_composite',
|
||
total: 1,
|
||
rows: [{
|
||
imageUrl: 'https://example.com/generated.jpg',
|
||
qualityReviewRequired: true,
|
||
qualityReviewStatus: 'pending',
|
||
}],
|
||
},
|
||
};
|
||
|
||
assert.equal(isSelectionWorkflowTaskStepComplete(task, 3), false);
|
||
assert.equal(isSelectionWorkflowTaskStepComplete({
|
||
...task,
|
||
mainImageGeneration: {
|
||
...task.mainImageGeneration,
|
||
rows: [{
|
||
...task.mainImageGeneration.rows[0],
|
||
qualityReviewStatus: 'approved',
|
||
}],
|
||
},
|
||
}, 3), true);
|
||
|
||
assert.equal(isSelectionWorkflowTaskStepComplete({
|
||
...task,
|
||
mainImageGeneration: {
|
||
mode: 'clean_only',
|
||
total: 1,
|
||
rows: [{
|
||
imageUrl: 'https://example.com/cleaned.jpg',
|
||
qualityReviewRequired: true,
|
||
qualityReviewStatus: 'pending',
|
||
}],
|
||
},
|
||
}, 3), false);
|
||
});
|
||
|
||
test('AI-generated SKU images require explicit per-SKU structure review before naming', () => {
|
||
const sourceImageUrl = 'https://example.com/sku-source.jpg';
|
||
const marketReferenceImageUrl = 'https://example.com/reference.jpg';
|
||
const generatedRow = {
|
||
...createVerifiedSkuImageRow({
|
||
skuCode: 'SKU-30',
|
||
skuName: '30cm 三层置物架',
|
||
sourceImageUrl,
|
||
generatedImageUrl: 'https://example.com/sku-generated.jpg',
|
||
mode: 'market_composite',
|
||
marketReferenceImageUrl,
|
||
}),
|
||
qualityReviewRequired: true,
|
||
qualityReviewStatus: 'pending',
|
||
};
|
||
const task = {
|
||
selectedSpuCode: 'SPU-RACK',
|
||
skuSnapshot: {
|
||
rows: [{ skuCode: 'SKU-30', skuName: '30cm 三层置物架', skuImageUrl: sourceImageUrl }],
|
||
},
|
||
skuSceneImages: {
|
||
mode: 'market_composite',
|
||
marketReference: { imageUrl: marketReferenceImageUrl },
|
||
selectedSkuCodes: ['SKU-30'],
|
||
rows: [generatedRow],
|
||
},
|
||
};
|
||
|
||
assert.equal(isSelectionWorkflowTaskStepComplete(task, 4), false);
|
||
assert.equal(isSelectionWorkflowTaskStepComplete({
|
||
...task,
|
||
skuSceneImages: {
|
||
...task.skuSceneImages,
|
||
rows: [{ ...generatedRow, qualityReviewStatus: 'approved' }],
|
||
},
|
||
}, 4), true);
|
||
});
|
||
|
||
test('JST SKU query filters inactive barcodes and preserves upstream order when included', async () => {
|
||
const originalGet = axios.get;
|
||
axios.get = async (url, config) => {
|
||
assert.match(url, /\/JS\/oa\/getJstSkusBySpu$/);
|
||
assert.equal(config.params.spu, 'SG0088');
|
||
return {
|
||
data: {
|
||
code: 200000,
|
||
result: [
|
||
{ i_id: 'SG0088', sku_id: 'SKU-SALE', status: '销售', stock: 0 },
|
||
{ i_id: 'SG0088', sku_id: 'SKU-RETIRED', status: '退市', stock: 7 },
|
||
{ i_id: 'SG0088', sku_id: 'SKU-DISCONTINUED', status: '停产', stock: null },
|
||
],
|
||
},
|
||
};
|
||
};
|
||
|
||
try {
|
||
const listingRows = await fetchCompanySkusFromJstApi('SG0088', 300);
|
||
const calculatorRows = await fetchCompanySkusFromJstApi('SG0088', 300, {
|
||
includeInactive: true,
|
||
});
|
||
|
||
assert.deepEqual(listingRows.map((row) => row.skuCode), ['SKU-SALE']);
|
||
assert.deepEqual(calculatorRows.map((row) => row.skuCode), [
|
||
'SKU-SALE',
|
||
'SKU-RETIRED',
|
||
'SKU-DISCONTINUED',
|
||
]);
|
||
assert.equal(calculatorRows[0].stock, 0);
|
||
assert.equal(calculatorRows[2].stock, null);
|
||
} finally {
|
||
axios.get = originalGet;
|
||
}
|
||
});
|
||
|
||
test('JST SKU image remains selectable without the supplemental material API', () => {
|
||
const assets = buildSkuSelectionAssetsFromSnapshot({
|
||
spuCode: 'BJ0179',
|
||
source: 'getJstSkusBySpu',
|
||
warnings: [],
|
||
rows: [{
|
||
skuCode: 'BJ017902N160V01',
|
||
skuName: '款式一 70/30/(60-75)',
|
||
skuImageUrl: 'https://static.jiansun.vip/Resource/SkuImg/BJ017902N160V01.jpg',
|
||
cost: 193,
|
||
minimumPrice: 380.366,
|
||
sourceSystem: 'getJstSkusBySpu',
|
||
}],
|
||
});
|
||
|
||
assert.equal(assets.total, 1);
|
||
assert.equal(assets.rows[0].previewImage.previewUrl, 'https://static.jiansun.vip/Resource/SkuImg/BJ017902N160V01.jpg');
|
||
assert.equal(assets.rows[0].cost, 193);
|
||
assert.equal(assets.rows[0].minimumPrice, 380.366);
|
||
assert.equal(assets.source, 'getJstSkusBySpu');
|
||
});
|
||
|
||
test('material library fills only company SKUs whose original image is missing', () => {
|
||
const assets = buildSkuSelectionAssetsFromSnapshot({
|
||
spuCode: 'SG0088',
|
||
source: 'getJstSkusBySpu',
|
||
rows: [
|
||
{
|
||
skuCode: 'SKU-ORIGINAL',
|
||
skuName: '有公司图',
|
||
skuImageUrl: 'https://company.example.com/original.jpg',
|
||
},
|
||
{
|
||
skuCode: 'SKU-MISSING',
|
||
skuName: '公司图缺失',
|
||
skuImageUrl: '',
|
||
},
|
||
],
|
||
}, {
|
||
rows: [
|
||
{
|
||
skuCode: 'SKU-ORIGINAL',
|
||
previewImage: { previewUrl: 'https://material.example.com/original-fallback.webp' },
|
||
},
|
||
{
|
||
skuCode: 'SKU-MISSING',
|
||
previewImage: { previewUrl: 'https://material.example.com/missing-fallback.webp' },
|
||
},
|
||
],
|
||
});
|
||
|
||
assert.equal(assets.rows[0].previewImage.previewUrl, 'https://company.example.com/original.jpg');
|
||
assert.equal(assets.rows[0].selectedImageSource, 'company-sku');
|
||
assert.equal(assets.rows[1].previewImage.previewUrl, 'https://material.example.com/missing-fallback.webp');
|
||
assert.equal(assets.rows[1].selectedImageSource, 'material-library-auto');
|
||
});
|
||
|
||
test('SKU material detail response keeps fixed and other images with stable keys', () => {
|
||
const assets = normalizeWhiteBackgroundSkuAssetDetails({
|
||
success: true,
|
||
data: {
|
||
sku: 'sg008830n1049v01',
|
||
whiteBackground: {
|
||
fixed: [
|
||
{ key: 'front', label: '正面', previewUrl: 'https://material.example.com/front.webp' },
|
||
],
|
||
other: [
|
||
{ key: 'other-1', label: '细节', previewUrl: 'https://material.example.com/detail.webp' },
|
||
{ key: 'duplicate', label: '重复图', previewUrl: 'https://material.example.com/front.webp' },
|
||
],
|
||
},
|
||
},
|
||
});
|
||
|
||
assert.equal(assets.skuCode, 'SG008830N1049V01');
|
||
assert.deepEqual(assets.rows.map((row) => row.assetKey), ['fixed:front', 'other:other-1']);
|
||
assert.equal(assets.total, 2);
|
||
});
|
||
|
||
test('manually selected material image drives SKU scene generation source', () => {
|
||
const imageUrl = getSkuSelectionImage({
|
||
skuSnapshot: {
|
||
rows: [{ skuCode: 'SKU-1', skuImageUrl: 'https://company.example.com/sku.jpg' }],
|
||
},
|
||
whiteBackgroundAssets: {
|
||
rows: [{
|
||
skuCode: 'SKU-1',
|
||
selectedImageSource: 'material-library-manual',
|
||
previewImage: { previewUrl: 'https://material.example.com/selected.webp' },
|
||
}],
|
||
},
|
||
}, 'SKU-1');
|
||
|
||
assert.equal(imageUrl, 'https://material.example.com/selected.webp');
|
||
});
|
||
|
||
test('SKU image processing keeps identical source URLs isolated by SKU code', () => {
|
||
const selectedSkuRows = [
|
||
{ skuCode: 'SKU-A' },
|
||
{ skuCode: 'SKU-B' },
|
||
{ skuCode: 'SKU-C' },
|
||
];
|
||
const task = {
|
||
skuSnapshot: {
|
||
rows: [
|
||
{ skuCode: 'SKU-A', skuImageUrl: 'https://example.com/shared.jpg' },
|
||
{ skuCode: 'SKU-B', skuImageUrl: 'https://example.com/shared.jpg' },
|
||
{ skuCode: 'SKU-C', skuImageUrl: 'https://example.com/other.jpg' },
|
||
],
|
||
},
|
||
};
|
||
|
||
const rows = prepareSkuSceneImageRows(task, selectedSkuRows, []);
|
||
const groups = groupSkuSceneRowsBySourceImage(rows);
|
||
assert.equal(groups.length, 3);
|
||
assert.deepEqual(groups.map((group) => group.rows.map((row) => row.skuCode)), [
|
||
['SKU-A'],
|
||
['SKU-B'],
|
||
['SKU-C'],
|
||
]);
|
||
});
|
||
|
||
test('direct SKU image mode rebinds every result to its current SKU identity', () => {
|
||
const rows = finalizeSourceSkuImageRows([
|
||
{
|
||
skuCode: 'sku-a',
|
||
skuName: '黑色 120cm',
|
||
originalImageUrl: 'http://img.example.com/a.jpg',
|
||
generatedImageUrl: 'https://img.example.com/stale-b.jpg',
|
||
generatedForSkuCode: 'SKU-B',
|
||
status: 'processing',
|
||
progress: 20,
|
||
},
|
||
{
|
||
skuCode: 'SKU-B',
|
||
skuName: '白色 140cm',
|
||
originalImageUrl: 'https://img.example.com/b.jpg',
|
||
},
|
||
]);
|
||
|
||
assert.deepEqual(rows.map((row) => ({
|
||
skuCode: row.skuCode,
|
||
generatedForSkuCode: row.generatedForSkuCode,
|
||
generatedForSkuName: row.generatedForSkuName,
|
||
generatedImageUrl: row.generatedImageUrl,
|
||
status: row.status,
|
||
progress: row.progress,
|
||
})), [
|
||
{
|
||
skuCode: 'sku-a',
|
||
generatedForSkuCode: 'SKU-A',
|
||
generatedForSkuName: '黑色 120cm',
|
||
generatedImageUrl: 'http://img.example.com/a.jpg',
|
||
status: 'completed',
|
||
progress: 100,
|
||
},
|
||
{
|
||
skuCode: 'SKU-B',
|
||
generatedForSkuCode: 'SKU-B',
|
||
generatedForSkuName: '白色 140cm',
|
||
generatedImageUrl: 'https://img.example.com/b.jpg',
|
||
status: 'completed',
|
||
progress: 100,
|
||
},
|
||
]);
|
||
});
|
||
|
||
test('retrying one SKU does not reset another SKU sharing the same source URL', () => {
|
||
const selectedSkuRows = [{ skuCode: 'SKU-A' }, { skuCode: 'SKU-B' }, { skuCode: 'SKU-C' }];
|
||
const task = {
|
||
skuSnapshot: {
|
||
rows: [
|
||
{ skuCode: 'SKU-A', skuImageUrl: 'https://example.com/shared.jpg' },
|
||
{ skuCode: 'SKU-B', skuImageUrl: 'https://example.com/shared.jpg' },
|
||
{ skuCode: 'SKU-C', skuImageUrl: 'https://example.com/other.jpg' },
|
||
],
|
||
},
|
||
skuSceneImages: {
|
||
mode: 'source_original',
|
||
rows: selectedSkuRows.map((row) => ({
|
||
skuCode: row.skuCode,
|
||
skuName: row.skuCode,
|
||
generationMode: 'source_original',
|
||
generatedForSkuCode: row.skuCode,
|
||
generatedForSkuName: row.skuCode,
|
||
generatedFromImageUrl: row.skuCode === 'SKU-C'
|
||
? 'https://example.com/other.jpg'
|
||
: 'https://example.com/shared.jpg',
|
||
generatedImageUrl: `https://example.com/${row.skuCode}.generated.jpg`,
|
||
status: 'completed',
|
||
})),
|
||
},
|
||
};
|
||
|
||
const rows = prepareSkuSceneImageRows(task, selectedSkuRows, ['SKU-A']);
|
||
assert.equal(rows[0].status, 'waiting');
|
||
assert.equal(rows[1].status, 'completed');
|
||
assert.equal(rows[0].generatedImageUrl, '');
|
||
assert.equal(rows[1].generatedImageUrl, 'https://example.com/SKU-B.generated.jpg');
|
||
assert.equal(rows[2].status, 'completed');
|
||
});
|
||
|
||
test('changing a SKU source URL invalidates its previous generated image', () => {
|
||
const task = {
|
||
skuSnapshot: {
|
||
rows: [{ skuCode: 'SKU-A', skuImageUrl: 'https://example.com/new.jpg' }],
|
||
},
|
||
skuSceneImages: {
|
||
rows: [{
|
||
skuCode: 'SKU-A',
|
||
originalImageUrl: 'https://example.com/old.jpg',
|
||
generatedImageUrl: 'https://example.com/old-generated.jpg',
|
||
status: 'completed',
|
||
}],
|
||
},
|
||
};
|
||
|
||
const [row] = prepareSkuSceneImageRows(task, [{ skuCode: 'SKU-A' }], []);
|
||
assert.equal(row.generatedImageUrl, '');
|
||
assert.equal(row.status, 'waiting');
|
||
assert.equal(row.sourceImageKey, 'sku:SKU-A');
|
||
});
|
||
|
||
test('legacy SKU image rows without identity metadata are invalidated', () => {
|
||
const task = {
|
||
skuSnapshot: {
|
||
rows: [{ skuCode: 'SKU-A', skuName: '黑色款', skuImageUrl: 'https://example.com/a.jpg' }],
|
||
},
|
||
skuSceneImages: {
|
||
mode: 'clean_only',
|
||
rows: [{
|
||
skuCode: 'SKU-A',
|
||
generatedImageUrl: 'https://example.com/legacy-generated.jpg',
|
||
status: 'completed',
|
||
}],
|
||
},
|
||
};
|
||
|
||
const [row] = prepareSkuSceneImageRows(task, task.skuSnapshot.rows, []);
|
||
assert.equal(row.generatedImageUrl, '');
|
||
assert.equal(row.status, 'waiting');
|
||
});
|
||
|
||
test('changing the SKU image mode invalidates results from another mode', () => {
|
||
const task = {
|
||
skuSnapshot: {
|
||
rows: [{ skuCode: 'SKU-A', skuName: '黑色款', skuImageUrl: 'https://example.com/a.jpg' }],
|
||
},
|
||
skuSceneImages: {
|
||
mode: 'clean_only',
|
||
rows: [createVerifiedSkuImageRow({
|
||
skuCode: 'SKU-A',
|
||
skuName: '黑色款',
|
||
sourceImageUrl: 'https://example.com/a.jpg',
|
||
generatedImageUrl: 'https://example.com/composite.jpg',
|
||
mode: 'market_composite',
|
||
marketReferenceImageUrl: 'https://example.com/reference-a.jpg',
|
||
})],
|
||
},
|
||
};
|
||
|
||
const [row] = prepareSkuSceneImageRows(task, task.skuSnapshot.rows, []);
|
||
assert.equal(row.generatedImageUrl, '');
|
||
assert.equal(row.generationMode, 'clean_only');
|
||
assert.equal(row.status, 'waiting');
|
||
});
|
||
|
||
test('changing the shared market reference invalidates every old composite result', () => {
|
||
const task = {
|
||
skuSnapshot: {
|
||
rows: [{ skuCode: 'SKU-A', skuName: '黑色款', skuImageUrl: 'https://example.com/a.jpg' }],
|
||
},
|
||
skuSceneImages: {
|
||
mode: 'market_composite',
|
||
marketReference: { imageUrl: 'https://example.com/reference-b.jpg' },
|
||
rows: [createVerifiedSkuImageRow({
|
||
skuCode: 'SKU-A',
|
||
skuName: '黑色款',
|
||
sourceImageUrl: 'https://example.com/a.jpg',
|
||
generatedImageUrl: 'https://example.com/composite.jpg',
|
||
mode: 'market_composite',
|
||
marketReferenceImageUrl: 'https://example.com/reference-a.jpg',
|
||
})],
|
||
},
|
||
};
|
||
|
||
const [row] = prepareSkuSceneImageRows(task, task.skuSnapshot.rows, []);
|
||
assert.equal(row.generatedImageUrl, '');
|
||
assert.equal(row.marketReferenceImageUrl, 'https://example.com/reference-b.jpg');
|
||
assert.equal(row.status, 'waiting');
|
||
});
|
||
|
||
test('main-image generation prefers an available material-library image over a raw source image', () => {
|
||
const sourceImageUrl = getMainGenerationSourceImage({
|
||
selectedMainSkuCode: 'SKU-1',
|
||
selectedMainSkuImageUrl: 'https://1688.example.com/raw.jpg',
|
||
skuSnapshot: {
|
||
rows: [{ skuCode: 'SKU-1', skuImageUrl: 'https://1688.example.com/raw.jpg' }],
|
||
},
|
||
whiteBackgroundAssets: {
|
||
rows: [{
|
||
skuCode: 'SKU-1',
|
||
selectedAssetKey: 'original',
|
||
previewImage: { previewUrl: 'https://1688.example.com/raw.jpg' },
|
||
originalPreviewImage: { previewUrl: 'https://1688.example.com/raw.jpg' },
|
||
materialLibraryPreviewImage: { previewUrl: 'https://material.example.com/clean.webp' },
|
||
}],
|
||
},
|
||
});
|
||
|
||
assert.equal(sourceImageUrl, 'https://material.example.com/clean.webp');
|
||
});
|
||
|
||
test('market item lookup removes only the platform prefix', () => {
|
||
assert.equal(
|
||
normalizeMarketItemLookupId('douyin:3670639322496893253'),
|
||
'3670639322496893253'
|
||
);
|
||
assert.equal(normalizeMarketItemLookupId('944734849380'), '944734849380');
|
||
});
|
||
|
||
test('listing callback accepts claim tokens from supported client payload shapes', () => {
|
||
assert.equal(normalizeClaimToken({ claimToken: 'top-level' }), 'top-level');
|
||
assert.equal(normalizeClaimToken({ taskClaimToken: 'task-alias' }), 'task-alias');
|
||
assert.equal(normalizeClaimToken({ task: { claimToken: 'nested-task' } }), 'nested-task');
|
||
assert.equal(normalizeClaimToken({ claim: { claim_token: 'nested-claim' } }), 'nested-claim');
|
||
});
|
||
|
||
test('claim token validation distinguishes a missing token from a stale token', () => {
|
||
const taskRow = {
|
||
id: 88,
|
||
task_code: 'MOCK-SF-SG0088-V1-S01-01',
|
||
claim_token_hash: hashClaimToken('active-token'),
|
||
claim_expires_at: new Date(Date.now() + 60_000).toISOString(),
|
||
};
|
||
|
||
assert.throws(
|
||
() => assertValidClaimToken(taskRow, {}),
|
||
(error) => error.status === 409
|
||
&& error.data?.code === 'CLAIM_TOKEN_MISSING'
|
||
&& error.data?.requiresReclaim === false
|
||
);
|
||
assert.throws(
|
||
() => assertValidClaimToken(taskRow, { claimToken: 'stale-token' }),
|
||
(error) => error.status === 409
|
||
&& error.data?.code === 'CLAIM_TOKEN_MISMATCH'
|
||
&& error.data?.requiresReclaim === true
|
||
);
|
||
assert.doesNotThrow(
|
||
() => assertValidClaimToken(taskRow, { task: { claimToken: 'active-token' } })
|
||
);
|
||
assert.throws(
|
||
() => assertValidClaimToken({
|
||
...taskRow,
|
||
claim_expires_at: new Date(Date.now() - 60_000).toISOString(),
|
||
}, { claimToken: 'active-token' }),
|
||
(error) => error.status === 409
|
||
&& error.data?.code === 'CLAIM_TOKEN_EXPIRED'
|
||
&& error.data?.requiresReclaim === true
|
||
);
|
||
});
|
||
|
||
test('delivery payload exposes all generated main images in workflow order', () => {
|
||
const generatedMainImageUrls = Array.from(
|
||
{ length: 5 },
|
||
(_, index) => `https://assets.example.com/generated-main-${index + 1}.jpg`
|
||
);
|
||
const payload = buildDeliveryPayload({
|
||
id: 27,
|
||
schemeCode: 'FB202608041305F50DC6',
|
||
items: [],
|
||
sourceItems: [],
|
||
listingTasks: [{
|
||
id: 80,
|
||
taskCode: 'FB202608041305F50DC6-S01-01',
|
||
slotNo: 1,
|
||
variantNo: 1,
|
||
marketItemId: '979560294960',
|
||
mainImageUrl: 'https://assets.example.com/market.jpg',
|
||
replacedImageUrl: generatedMainImageUrls[0],
|
||
mainImageGeneration: {
|
||
rows: generatedMainImageUrls.map((imageUrl, index) => ({ index: index + 1, imageUrl })),
|
||
},
|
||
}],
|
||
});
|
||
|
||
assert.deepEqual(payload.marketImages[0].generatedMainImageUrls, generatedMainImageUrls);
|
||
assert.equal(payload.marketImages[0].imageUrl, generatedMainImageUrls[0]);
|
||
});
|
||
|
||
test('selection fission plan accepts any source count from one to five', () => {
|
||
const buildItems = (count) => Array.from({ length: count }, (_, index) => ({
|
||
channelName: '淘宝',
|
||
marketItemId: `ITEM-${index + 1}`,
|
||
title: `商品 ${index + 1}`,
|
||
}));
|
||
|
||
assert.equal(normalizeSelectionFissionPlanItems(buildItems(1)).length, 1);
|
||
assert.equal(normalizeSelectionFissionPlanItems(buildItems(2)).length, 2);
|
||
assert.equal(normalizeSelectionFissionPlanItems(buildItems(5)).length, 5);
|
||
assert.throws(() => normalizeSelectionFissionPlanItems([]), /1~5/);
|
||
assert.throws(() => normalizeSelectionFissionPlanItems(buildItems(6)), /1~5/);
|
||
|
||
const [item] = normalizeSelectionFissionPlanItems([{
|
||
channelName: '淘宝',
|
||
marketItemId: '10001',
|
||
priceBand: '100 - 200',
|
||
priceBandList: ['100 - 200', '200 - 300'],
|
||
}]);
|
||
assert.equal(item.priceBand, '100 - 200');
|
||
assert.deepEqual(item.priceBandList, ['100 - 200', '200 - 300']);
|
||
});
|
||
|
||
test('market image search candidates keep unique products with usable images', () => {
|
||
const rows = collectMarketReferenceCandidates({
|
||
data: {
|
||
products: [
|
||
{
|
||
platform: '淘宝',
|
||
product_id: 'A-1',
|
||
image_url: 'https://example.com/a.jpg',
|
||
title: '参考商品 A',
|
||
price_band: '200-300元',
|
||
analysis_sales: '1288',
|
||
},
|
||
{
|
||
platform: '淘宝',
|
||
product_id: 'A-1',
|
||
image_url: 'https://example.com/a-copy.jpg',
|
||
},
|
||
{
|
||
platform: '京东',
|
||
product_id: 'B-2',
|
||
image_url: '',
|
||
},
|
||
{
|
||
platform: '京东',
|
||
product_id: 'C-3',
|
||
image_url: 'https://example.com/c.jpg',
|
||
},
|
||
],
|
||
},
|
||
});
|
||
|
||
assert.deepEqual(rows.map((row) => row.id), ['淘宝:A-1', '京东:C-3']);
|
||
assert.equal(rows[0].priceBand, '200-300元');
|
||
assert.equal(rows[0].salesVolume, '1288');
|
||
assert.equal(buildMarketProductDetailUrl(rows[0]), 'https://item.taobao.com/item.htm?id=A-1');
|
||
});
|
||
|
||
test('market references keep stored rank price bands before page-price fallback', () => {
|
||
assert.equal(inferMarketChannelName('', 'jd:10083267349458'), '京东');
|
||
assert.equal(inferMarketChannelName('', '1688:1024108915351'), '1688');
|
||
assert.equal(buildMarketReferencePriceBand(2.68), '0-10');
|
||
assert.equal(buildMarketReferencePriceBand(120.84), '100-200');
|
||
assert.equal(buildMarketReferencePriceBand(268), '200-300');
|
||
assert.equal(buildMarketReferencePriceBand(3699), '2000+');
|
||
const candidate = normalizeMarketReferenceCandidate({
|
||
product_id: '10001',
|
||
image_url: 'https://example.com/10001.jpg',
|
||
price: 19.9,
|
||
price_band: '100 - 200',
|
||
priceBandList: ['100 - 200', '200 - 300'],
|
||
});
|
||
assert.equal(candidate.priceBand, '100 - 200');
|
||
assert.deepEqual(candidate.priceBandList, ['100 - 200', '200 - 300']);
|
||
assert.deepEqual(
|
||
normalizePriceBandList('["100 - 200", "200 - 300"]', '100 - 200'),
|
||
['100 - 200', '200 - 300']
|
||
);
|
||
assert.deepEqual(
|
||
sortPriceBandValues(['200 - 300', '不限', '100 - 200']),
|
||
['不限', '100 - 200', '200 - 300']
|
||
);
|
||
assert.equal(
|
||
buildMarketProductDetailUrl({ marketItemId: 'jd:10083267349458', channelName: '京东' }),
|
||
'https://item.jd.com/10083267349458.html'
|
||
);
|
||
});
|
||
|
||
test('Market markdown keeps market IDs, links and main images', () => {
|
||
const rows = parseMarketReferenceMarkdown(`
|
||
| 排名 | 市场ID | 商品标题 | 详情链接 | 主图 | 相似度 | 提示 |
|
||
| ---: | --- | --- | --- | --- | ---: | --- |
|
||
| 1 | 637702521867 | 黑胡桃木床头柜 | [打开](https://item.example.com/637702521867) | [主图](https://img.example.com/1.jpg) | 1.000 | - |
|
||
| 2 | jd:10066646075317 | 实木收纳柜 | [打开](https://item.example.com/2) | [主图](https://img.example.com/2.jpg) | 0.925 | - |
|
||
`);
|
||
|
||
assert.deepEqual(rows.map((row) => row.marketItemId), [
|
||
'637702521867',
|
||
'jd:10066646075317',
|
||
]);
|
||
assert.equal(rows[0].mainImageUrl, 'https://img.example.com/1.jpg');
|
||
assert.equal(rows[1].score, 0.925);
|
||
});
|
||
|
||
test('company SPU candidates prioritize matching categories then similarity', () => {
|
||
const rows = rankSpuCandidatesByCategory([
|
||
{ spuCode: 'A', category: '住宅家具 / 桌类 / 餐桌', bestScore: 0.82 },
|
||
{ spuCode: 'B', category: '住宅家具 / 柜类 / 鞋柜', bestScore: 0.99 },
|
||
{ spuCode: 'C', category: '', bestScore: 0.91 },
|
||
], {
|
||
level1: '住宅家具',
|
||
level2: '桌类',
|
||
level3: '餐桌',
|
||
});
|
||
|
||
assert.deepEqual(rows.map((row) => row.spuCode), ['A', 'C', 'B']);
|
||
assert.deepEqual(rows.map((row) => row.categoryMatchLevel), [3, 0, -1]);
|
||
});
|
||
|
||
test('company SPU normalization never promotes a SKU code to SPU', () => {
|
||
const skuOnly = normalizeSimilarCandidate({
|
||
skuCode: '973351010702',
|
||
skuName: '仅有条码的候选',
|
||
});
|
||
assert.equal(skuOnly.spuCode, '');
|
||
|
||
const response = normalizeSimilarResponsePayload({
|
||
ownProductMatches: [
|
||
{ skuCode: '973351010702', skuName: '仅有条码的候选' },
|
||
{ productCode: '976235605909', skuCode: 'SKU-2', skuName: '待验证商品编码' },
|
||
{ spuCode: 'dnz0154', skuCode: 'SKU-3', skuName: '明确SPU' },
|
||
],
|
||
});
|
||
assert.deepEqual(response.spuCandidates.map((row) => row.spuCode), [
|
||
'976235605909',
|
||
'DNZ0154',
|
||
]);
|
||
});
|
||
|
||
test('company SPU validation keeps only exact snapshot or JST matches', async () => {
|
||
const realtimeRequests = [];
|
||
const result = await validateCompanySpuCandidates([
|
||
{ spuCode: '973351010702' },
|
||
{ spuCode: 'DNZ0154', skuCount: 1 },
|
||
{ spuCode: 'dnz0289' },
|
||
], {
|
||
querySnapshot: async (spuCodes) => {
|
||
assert.deepEqual(spuCodes, ['973351010702', 'DNZ0154', 'DNZ0289']);
|
||
return [{ spuCode: 'DNZ0154', skuCount: 58 }];
|
||
},
|
||
fetchSpuSkus: async (spuCode) => {
|
||
realtimeRequests.push(spuCode);
|
||
if (spuCode === 'DNZ0289') {
|
||
return [{ spuCode: 'DNZ0289', skuCode: 'DNZ028901' }];
|
||
}
|
||
return [{ spuCode: 'OTHER-SPU', skuCode: '973351010702' }];
|
||
},
|
||
});
|
||
|
||
assert.deepEqual(result.validCandidates.map((row) => row.spuCode), ['DNZ0154', 'DNZ0289']);
|
||
assert.deepEqual(result.invalidCandidates.map((row) => row.spuCode), ['973351010702']);
|
||
assert.deepEqual(result.unverifiedCandidates, []);
|
||
assert.deepEqual(realtimeRequests, ['973351010702', 'DNZ0289']);
|
||
assert.equal(result.validCandidates[0].skuCount, 58);
|
||
assert.equal(result.validCandidates[1].spuVerificationSource, 'getJstSkusBySpu');
|
||
});
|
||
|
||
test('company SPU validation does not trust client verification flags', async () => {
|
||
const result = await validateCompanySpuCandidates([
|
||
{ spuCode: 'FAKE-SPU', spuVerified: true },
|
||
], {
|
||
querySnapshot: async () => [],
|
||
fetchSpuSkus: async () => [],
|
||
});
|
||
|
||
assert.deepEqual(result.validCandidates, []);
|
||
assert.deepEqual(result.invalidCandidates.map((row) => row.spuCode), ['FAKE-SPU']);
|
||
});
|
||
|
||
test('company SPU validation stops after a full failed realtime batch', async () => {
|
||
let requestCount = 0;
|
||
const result = await validateCompanySpuCandidates(
|
||
Array.from({ length: 12 }, (_, index) => ({ spuCode: `SPU-${index + 1}` })),
|
||
{
|
||
querySnapshot: async () => [],
|
||
fetchSpuSkus: async () => {
|
||
requestCount += 1;
|
||
throw new Error('JST timeout');
|
||
},
|
||
}
|
||
);
|
||
|
||
assert.equal(requestCount, 8);
|
||
assert.equal(result.unverifiedCandidates.length, 12);
|
||
assert.equal(result.warnings.length, 1);
|
||
assert.match(result.warnings[0], /JST timeout/);
|
||
});
|
||
|
||
test('company sales trend fills six continuous calendar months', () => {
|
||
assert.deepEqual(fillMonthlySalesTrend6m([
|
||
{ month: '2026-03', salesAmount: 30 },
|
||
{ month: '2026-05', salesAmount: 50 },
|
||
{ month: '2026-07', salesAmount: 70 },
|
||
]), [
|
||
{ month: '2026-02', salesAmount: 0 },
|
||
{ month: '2026-03', salesAmount: 30 },
|
||
{ month: '2026-04', salesAmount: 0 },
|
||
{ month: '2026-05', salesAmount: 50 },
|
||
{ month: '2026-06', salesAmount: 0 },
|
||
{ month: '2026-07', salesAmount: 70 },
|
||
]);
|
||
});
|
||
|
||
test('each selection fission ID advances through its own next node', () => {
|
||
assert.deepEqual(
|
||
getSelectionFissionItemNodeState({}),
|
||
{ step: 1, action: 'initialize-source' }
|
||
);
|
||
assert.deepEqual(
|
||
getSelectionFissionItemNodeState({ mainImageUrl: 'https://example.com/market.jpg' }),
|
||
{ step: 2, action: 'match-spu' }
|
||
);
|
||
assert.deepEqual(
|
||
getSelectionFissionItemNodeState({
|
||
mainImageUrl: 'https://example.com/market.jpg',
|
||
similarResponse: { spuCandidates: [{ spuCode: 'SPU-1' }] },
|
||
}),
|
||
{ step: 2, action: 'select-spu', requiresUser: true }
|
||
);
|
||
assert.deepEqual(
|
||
getSelectionFissionItemNodeState({
|
||
selectedSpuCode: 'SPU-1',
|
||
whiteBackgroundAssets: { rows: [] },
|
||
}),
|
||
{ step: 3, action: 'prepare-main-sku' }
|
||
);
|
||
assert.deepEqual(
|
||
getSelectionFissionItemNodeState({
|
||
selectedSpuCode: 'SPU-1',
|
||
whiteBackgroundAssets: {
|
||
rows: [{
|
||
skuCode: 'SKU-1',
|
||
previewImage: { previewUrl: 'https://example.com/sku.jpg' },
|
||
}],
|
||
},
|
||
}),
|
||
{ step: 3, action: 'select-main-sku', requiresUser: true }
|
||
);
|
||
assert.deepEqual(
|
||
getSelectionFissionItemNodeState({
|
||
selectedSpuCode: 'SPU-1',
|
||
selectedMainSkuImageUrl: 'https://example.com/sku.jpg',
|
||
whiteBackgroundAssets: {
|
||
rows: [{
|
||
skuCode: 'SKU-1',
|
||
previewImage: { previewUrl: 'https://example.com/sku.jpg' },
|
||
}],
|
||
},
|
||
}),
|
||
{ step: 8, action: 'pricing' }
|
||
);
|
||
});
|
||
|
||
test('1688 pending items reuse generation nodes with pricing as their entry', () => {
|
||
const baseItem = {
|
||
metadata: { workflowProfile: 'alibaba_1688_pending' },
|
||
selectedSpuCode: '1688:123456',
|
||
selectedSpuName: '1688 source product',
|
||
selectedMainSkuImageUrl: 'https://example.com/source-sku.jpg',
|
||
skuSnapshot: {
|
||
rows: [{
|
||
skuCode: 'SKU-1',
|
||
skuName: 'source sku',
|
||
skuImageUrl: 'https://example.com/source-sku.jpg',
|
||
}],
|
||
},
|
||
whiteBackgroundAssets: {
|
||
rows: [{
|
||
skuCode: 'SKU-1',
|
||
previewImage: { previewUrl: 'https://example.com/source-sku.jpg' },
|
||
}],
|
||
},
|
||
};
|
||
|
||
assert.deepEqual(
|
||
getSelectionFissionItemNodeState(baseItem),
|
||
{ step: 8, action: 'pricing' }
|
||
);
|
||
|
||
const pricedItem = {
|
||
...baseItem,
|
||
productCodeStatus: 'completed',
|
||
productCodeStyleCode: 'STYLE-1688-1',
|
||
pricingRows: [{
|
||
skuCode: 'SKU-1',
|
||
selectedSkuName: 'source sku',
|
||
internalProductCode: 'STYLE-1688-1-01',
|
||
groupPrice: 100,
|
||
groupPrice: 99,
|
||
}],
|
||
activityPricingRows: [],
|
||
pricingTableSavedAt: '2026-08-15 12:00:00',
|
||
};
|
||
assert.deepEqual(
|
||
getSelectionFissionItemNodeState(pricedItem),
|
||
{ step: 3, action: 'main-image' }
|
||
);
|
||
|
||
const mainImagesReadyItem = {
|
||
...pricedItem,
|
||
mainImageGeneration: {
|
||
rows: Array.from({ length: 10 }, (_, index) => ({
|
||
imageUrl: `https://example.com/main-${index + 1}.jpg`,
|
||
})),
|
||
},
|
||
};
|
||
assert.deepEqual(
|
||
getSelectionFissionItemNodeState(mainImagesReadyItem),
|
||
{ step: 4, action: 'sku-images' }
|
||
);
|
||
|
||
const skuImagesReadyItem = {
|
||
...mainImagesReadyItem,
|
||
skuSceneImages: {
|
||
mode: 'source_original',
|
||
selectedSkuCodes: ['SKU-1'],
|
||
rows: [createVerifiedSkuImageRow({
|
||
skuCode: 'SKU-1',
|
||
skuName: 'source sku',
|
||
sourceImageUrl: 'https://example.com/source-sku.jpg',
|
||
generatedImageUrl: 'https://example.com/generated-sku.jpg',
|
||
})],
|
||
},
|
||
};
|
||
assert.deepEqual(
|
||
getSelectionFissionItemNodeState(skuImagesReadyItem),
|
||
{ step: 6, action: 'sku-names' }
|
||
);
|
||
|
||
const skuNamesReadyItem = {
|
||
...skuImagesReadyItem,
|
||
skuNameCandidates: {
|
||
rows: [{
|
||
skuCode: 'SKU-1',
|
||
names: ['name 1', 'name 2', 'name 3', 'name 4', 'name 5'],
|
||
selectedName: 'name 1',
|
||
}],
|
||
},
|
||
};
|
||
assert.deepEqual(
|
||
getSelectionFissionItemNodeState(skuNamesReadyItem),
|
||
{ step: 7, action: 'product-title' }
|
||
);
|
||
|
||
assert.deepEqual(
|
||
getSelectionFissionItemNodeState({
|
||
...skuNamesReadyItem,
|
||
optimizedTitleCandidates: ['title 1', 'title 2', 'title 3', 'title 4', 'title 5'],
|
||
selectedProductTitle: 'title 1',
|
||
}),
|
||
{ step: 7, action: 'completed', completed: true }
|
||
);
|
||
});
|
||
|
||
test('Company SKU markdown becomes selectable SPU candidates', () => {
|
||
const rows = parseCompanySkuMarkdown([
|
||
'| 排名 | SKU | 商品/规格 | SPU/货号 | 相似度 | 链接 |',
|
||
'| ---: | --- | --- | --- | ---: | --- |',
|
||
'| 1 | SG008832N1049V01 | 款式一 80/30/180 炭黑色 | SG0088 | 0.667 | [图片](https://example.com/sku.jpg) |',
|
||
].join('\n'));
|
||
|
||
assert.deepEqual(rows, [{
|
||
rank: 1,
|
||
maySkuCode: 'SG008832N1049V01',
|
||
skuCode: 'SG008832N1049V01',
|
||
skuName: '款式一 80/30/180 炭黑色',
|
||
productCode: 'SG0088',
|
||
spuCode: 'SG0088',
|
||
score: 0.667,
|
||
skuImageUrl: 'https://example.com/sku.jpg',
|
||
source: 'image-search',
|
||
}]);
|
||
});
|
||
|
||
test('image generation keeps reference images available while parsing output images', async () => {
|
||
const originalPost = axios.post;
|
||
const originalSystemQuery = database.local.query;
|
||
const originalApiKey = process.env.SELECTION_IMAGE_GEN_API_KEY;
|
||
const originalImageSize = process.env.SELECTION_IMAGE_GEN_IMAGE_SIZE;
|
||
const originalImageTimeout = process.env.SELECTION_IMAGE_GEN_TIMEOUT_MS;
|
||
process.env.SELECTION_IMAGE_GEN_API_KEY = 'test-image-api-key';
|
||
process.env.SELECTION_IMAGE_GEN_IMAGE_SIZE = '1K';
|
||
process.env.SELECTION_IMAGE_GEN_TIMEOUT_MS = '900000';
|
||
database.local.query = async () => [];
|
||
axios.post = async (_url, payload, requestConfig) => {
|
||
assert.equal(payload.imageSize, '1K');
|
||
assert.equal(payload.aspectRatio, '1:1');
|
||
assert.equal(payload.width, 800);
|
||
assert.equal(payload.height, 800);
|
||
assert.equal(requestConfig.timeout, 900000);
|
||
assert.deepEqual(payload.referenceImages, [
|
||
'https://example.com/market.jpg',
|
||
'https://example.com/product.jpg',
|
||
]);
|
||
return {
|
||
data: {
|
||
data: {
|
||
requestId: 'request-1',
|
||
images: [{ originalUrl: 'https://example.com/generated.jpg' }],
|
||
},
|
||
},
|
||
};
|
||
};
|
||
|
||
try {
|
||
const result = await requestSelectionGeneratedImage({
|
||
prompt: 'replace product',
|
||
referenceImages: [
|
||
'https://example.com/market.jpg',
|
||
'https://example.com/product.jpg',
|
||
],
|
||
aspectRatio: '4:5',
|
||
});
|
||
assert.equal(result.imageUrl, 'https://example.com/generated.jpg');
|
||
assert.equal(result.requestId, 'request-1');
|
||
} finally {
|
||
axios.post = originalPost;
|
||
if (originalApiKey === undefined) {
|
||
delete process.env.SELECTION_IMAGE_GEN_API_KEY;
|
||
} else {
|
||
process.env.SELECTION_IMAGE_GEN_API_KEY = originalApiKey;
|
||
}
|
||
if (originalImageSize === undefined) {
|
||
delete process.env.SELECTION_IMAGE_GEN_IMAGE_SIZE;
|
||
} else {
|
||
process.env.SELECTION_IMAGE_GEN_IMAGE_SIZE = originalImageSize;
|
||
}
|
||
if (originalImageTimeout === undefined) {
|
||
delete process.env.SELECTION_IMAGE_GEN_TIMEOUT_MS;
|
||
} else {
|
||
process.env.SELECTION_IMAGE_GEN_TIMEOUT_MS = originalImageTimeout;
|
||
}
|
||
}
|
||
});
|
||
|
||
test('image generation settings preserve independent provider keys and allow switching to Qwen', () => {
|
||
const config = normalizeImageGenerationConfig({
|
||
provider: 'qwen',
|
||
external: { apiKey: '' },
|
||
qwen: {
|
||
apiKey: 'new-qwen-key',
|
||
model: 'qwen-image-3.0-pro',
|
||
imageSize: '2K',
|
||
},
|
||
}, {
|
||
provider: 'external',
|
||
external: { apiKey: 'existing-external-key', baseUrl: 'https://image.example.com' },
|
||
qwen: { apiKey: 'old-qwen-key' },
|
||
});
|
||
|
||
assert.equal(config.provider, 'qwen');
|
||
assert.equal(config.external.apiKey, 'existing-external-key');
|
||
assert.equal(config.qwen.apiKey, 'new-qwen-key');
|
||
assert.equal(config.qwen.model, 'qwen-image-3.0-pro');
|
||
assert.equal(config.external.imageSize, '1K');
|
||
assert.equal(config.qwen.imageSize, '1K');
|
||
});
|
||
|
||
test('Qwen image generation keeps image order, fixed 1K dimensions and persists the temporary result', async () => {
|
||
let request = null;
|
||
let persistedUrl = '';
|
||
const result = await requestQwenImage({
|
||
baseUrl: 'https://dashscope.aliyuncs.com',
|
||
apiKey: 'qwen-test-key',
|
||
model: 'qwen-image-3.0-pro',
|
||
imageSize: '2K',
|
||
promptExtend: true,
|
||
watermark: false,
|
||
timeoutMs: 900000,
|
||
}, {
|
||
prompt: 'place the exact product in the market scene',
|
||
negativePrompt: 'changed product',
|
||
aspectRatio: '4:3',
|
||
referenceImages: ['https://example.com/product.jpg', 'https://example.com/scene.jpg'],
|
||
}, {
|
||
post: async (url, payload, config) => {
|
||
request = { url, payload, config };
|
||
return {
|
||
data: {
|
||
request_id: 'qwen-request-1',
|
||
output: {
|
||
choices: [{ message: { content: [{ image: 'https://temporary.example.com/result.png' }] } }],
|
||
},
|
||
usage: { image_count: 1, width: 2048, height: 1536 },
|
||
},
|
||
};
|
||
},
|
||
persistImage: async (url) => {
|
||
persistedUrl = url;
|
||
return 'https://cdn.example.com/permanent.png';
|
||
},
|
||
});
|
||
|
||
assert.match(request.url, /\/api\/v1\/services\/aigc\/multimodal-generation\/generation$/);
|
||
assert.deepEqual(request.payload.input.messages[0].content, [
|
||
{ image: 'https://example.com/product.jpg' },
|
||
{ image: 'https://example.com/scene.jpg' },
|
||
{ text: 'place the exact product in the market scene' },
|
||
]);
|
||
assert.equal(request.payload.parameters.size, '800*800');
|
||
assert.equal(request.payload.parameters.prompt_extend, true);
|
||
assert.equal(request.config.headers.Authorization, 'Bearer qwen-test-key');
|
||
assert.equal(persistedUrl, 'https://temporary.example.com/result.png');
|
||
assert.equal(result.imageUrl, 'https://cdn.example.com/permanent.png');
|
||
assert.equal(result.provider, 'qwen');
|
||
assert.equal(resolveQwenImageDimensions('9:16', '1K'), '800*800');
|
||
});
|
||
|
||
test('Qwen disables prompt expansion for identity-locked product compositing', async () => {
|
||
let request = null;
|
||
await requestQwenImage({
|
||
baseUrl: 'https://dashscope.aliyuncs.com',
|
||
apiKey: 'qwen-test-key',
|
||
model: 'qwen-image-3.0-pro',
|
||
imageSize: '1K',
|
||
promptExtend: true,
|
||
watermark: false,
|
||
timeoutMs: 900000,
|
||
}, {
|
||
prompt: 'keep the exact product unchanged',
|
||
negativePrompt: 'product redesign',
|
||
aspectRatio: '1:1',
|
||
referenceImages: ['https://example.com/product.jpg', 'https://example.com/scene.jpg'],
|
||
metadata: { preserveProductIdentity: true, disablePromptExtend: true },
|
||
}, {
|
||
post: async (_url, payload) => {
|
||
request = payload;
|
||
return {
|
||
data: {
|
||
request_id: 'qwen-identity-lock',
|
||
output: { choices: [{ message: { content: [{ image: 'https://temporary.example.com/result.png' }] } }] },
|
||
},
|
||
};
|
||
},
|
||
persistImage: async () => 'https://cdn.example.com/identity-locked.png',
|
||
});
|
||
|
||
assert.equal(request.parameters.prompt_extend, false);
|
||
});
|
||
|
||
test('1688 main image generation locks product identity while adopting the market camera', async () => {
|
||
const originalPost = axios.post;
|
||
const originalSystemQuery = database.local.query;
|
||
const originalApiKey = process.env.SELECTION_IMAGE_GEN_API_KEY;
|
||
process.env.SELECTION_IMAGE_GEN_API_KEY = 'test-image-api-key';
|
||
database.local.query = async () => [];
|
||
axios.post = async (_url, payload) => {
|
||
assert.deepEqual(payload.referenceImages, [
|
||
'https://example.com/target-product.jpg',
|
||
'https://example.com/market-scene.jpg',
|
||
]);
|
||
assert.match(payload.prompt, /exact product-compositing edit/);
|
||
assert.match(payload.prompt, /Transform the whole target product as one rigid 3D object/);
|
||
assert.match(payload.prompt, /exact part counts/);
|
||
assert.match(payload.prompt, /Target SKU specification: Color A \/ 1200W\*600D\*730H/);
|
||
assert.match(payload.prompt, /不可重新设计、不可拆分生成的刚性三维整体/);
|
||
assert.doesNotMatch(payload.prompt, /公司商品主体/);
|
||
assert.match(payload.negativePrompt, /missing components/);
|
||
assert.equal(payload.metadata.referenceOrder, 'target-product-first');
|
||
assert.equal(payload.metadata.preserveProductIdentity, true);
|
||
assert.equal(payload.metadata.disablePromptExtend, true);
|
||
return {
|
||
data: {
|
||
data: {
|
||
requestId: 'strict-request-1',
|
||
images: [{ originalUrl: 'https://example.com/strict-result.jpg' }],
|
||
},
|
||
},
|
||
};
|
||
};
|
||
|
||
try {
|
||
const result = await generateMainReplacementImage({
|
||
marketImageUrl: 'https://example.com/market-scene.jpg',
|
||
whiteBackgroundImageUrl: 'https://example.com/target-product.jpg',
|
||
marketItemId: 'MARKET-1',
|
||
spuCode: '1688-1',
|
||
skuCode: '1688-1-SKU-1',
|
||
skuName: 'Color A / 1200W*600D*730H',
|
||
aspectRatio: '1:1',
|
||
strictProductIdentity: true,
|
||
});
|
||
assert.equal(result.imageUrl, 'https://example.com/strict-result.jpg');
|
||
assert.equal(result.requestId, 'strict-request-1');
|
||
} finally {
|
||
axios.post = originalPost;
|
||
database.local.query = originalSystemQuery;
|
||
database.local.query = originalSystemQuery;
|
||
if (originalApiKey === undefined) {
|
||
delete process.env.SELECTION_IMAGE_GEN_API_KEY;
|
||
} else {
|
||
process.env.SELECTION_IMAGE_GEN_API_KEY = originalApiKey;
|
||
}
|
||
}
|
||
});
|
||
|
||
test('company main image generation uses the same strict product identity lock as 1688', async () => {
|
||
const originalPost = axios.post;
|
||
const originalSystemQuery = database.local.query;
|
||
const originalApiKey = process.env.SELECTION_IMAGE_GEN_API_KEY;
|
||
process.env.SELECTION_IMAGE_GEN_API_KEY = 'test-image-api-key';
|
||
database.local.query = async () => [];
|
||
axios.post = async (_url, payload) => {
|
||
assert.deepEqual(payload.referenceImages, [
|
||
'https://example.com/company-product.jpg',
|
||
'https://example.com/market-scene.jpg',
|
||
]);
|
||
assert.match(payload.prompt, /exact product-compositing edit/);
|
||
assert.match(payload.prompt, /Transform the whole target product as one rigid 3D object/);
|
||
assert.match(payload.prompt, /不可重新设计、不可拆分生成的刚性三维整体/);
|
||
assert.match(payload.prompt, /部件精确数量/);
|
||
assert.match(payload.prompt, /无条件遵守本规则/);
|
||
assert.doesNotMatch(payload.prompt, /参考图1是同品类市场商品图/);
|
||
assert.equal(payload.metadata.referenceOrder, 'target-product-first');
|
||
assert.equal(payload.metadata.identityLockVersion, 'rigid-product-v2');
|
||
return { data: { data: { images: [{ originalUrl: 'https://example.com/company-result.jpg' }] } } };
|
||
};
|
||
|
||
try {
|
||
const result = await generateMainReplacementImage({
|
||
marketImageUrl: 'https://example.com/market-scene.jpg',
|
||
whiteBackgroundImageUrl: 'https://example.com/company-product.jpg',
|
||
marketItemId: 'MARKET-2',
|
||
spuCode: 'SPU-2',
|
||
skuCode: 'SKU-2',
|
||
skuName: '双抽屉四腿原木色',
|
||
aspectRatio: '1:1',
|
||
marketCompositionFirst: true,
|
||
strictProductIdentity: true,
|
||
});
|
||
assert.equal(result.imageUrl, 'https://example.com/company-result.jpg');
|
||
} finally {
|
||
axios.post = originalPost;
|
||
database.local.query = originalSystemQuery;
|
||
if (originalApiKey === undefined) delete process.env.SELECTION_IMAGE_GEN_API_KEY;
|
||
else process.env.SELECTION_IMAGE_GEN_API_KEY = originalApiKey;
|
||
}
|
||
});
|
||
|
||
test('SKU scene generation preserves the exact SKU and uses the market image only as scene reference', async () => {
|
||
const originalPost = axios.post;
|
||
const originalSystemQuery = database.local.query;
|
||
const originalApiKey = process.env.SELECTION_IMAGE_GEN_API_KEY;
|
||
process.env.SELECTION_IMAGE_GEN_API_KEY = 'test-image-api-key';
|
||
database.local.query = async () => [];
|
||
axios.post = async (_url, payload) => {
|
||
assert.deepEqual(payload.referenceImages, [
|
||
'https://example.com/sku-blue.jpg',
|
||
'https://example.com/market-scene.jpg',
|
||
]);
|
||
assert.match(payload.prompt, /第一张图是当前SKU唯一商品身份图/);
|
||
assert.match(payload.prompt, /不可重新设计、不可拆分生成的刚性三维整体/);
|
||
assert.match(payload.prompt, /严禁凭常识补全.*脚轮/);
|
||
assert.match(payload.prompt, /尺寸、数量、颜色、款式和配件信息必须保持一致/);
|
||
assert.match(payload.prompt, /不得猜测、补画或改造商品/);
|
||
assert.match(payload.prompt, /内部SKU身份编码:SKU-BLUE/);
|
||
assert.match(payload.negativePrompt, /新增脚轮/);
|
||
assert.match(payload.negativePrompt, /尺寸变化/);
|
||
assert.equal(payload.metadata.referenceOrder, 'target-sku-first');
|
||
assert.equal(payload.metadata.targetSkuReferenceIndex, 1);
|
||
assert.equal(payload.metadata.marketCompositionReferenceIndex, 2);
|
||
assert.equal(payload.metadata.preserveProductIdentity, true);
|
||
assert.equal(payload.metadata.disablePromptExtend, true);
|
||
return { data: { data: { images: [{ originalUrl: 'https://example.com/sku-result.jpg' }] } } };
|
||
};
|
||
|
||
try {
|
||
const result = await generateSkuSceneImage({
|
||
marketReferenceImageUrl: 'https://example.com/market-scene.jpg',
|
||
skuImageUrl: 'https://example.com/sku-blue.jpg',
|
||
skuCode: 'SKU-BLUE',
|
||
skuName: '蓝色双门款',
|
||
title: '市场参考标题',
|
||
spuCode: 'SPU-3',
|
||
aspectRatio: '1:1',
|
||
});
|
||
assert.equal(result.imageUrl, 'https://example.com/sku-result.jpg');
|
||
} finally {
|
||
axios.post = originalPost;
|
||
database.local.query = originalSystemQuery;
|
||
if (originalApiKey === undefined) delete process.env.SELECTION_IMAGE_GEN_API_KEY;
|
||
else process.env.SELECTION_IMAGE_GEN_API_KEY = originalApiKey;
|
||
}
|
||
});
|
||
|
||
test('extensionless image references are rehosted with detected image metadata', async () => {
|
||
const sourceUrl = 'https://example.com/material/image_without_extension';
|
||
const jpeg = Buffer.alloc(16);
|
||
Buffer.from([0xff, 0xd8, 0xff, 0xe0]).copy(jpeg);
|
||
let uploaded = null;
|
||
|
||
const result = await normalizeImageGenerationReferenceUrl(sourceUrl, {
|
||
fetchImage: async (url) => {
|
||
assert.equal(url, sourceUrl);
|
||
return {
|
||
data: jpeg,
|
||
headers: { 'content-type': 'application/octet-stream' },
|
||
};
|
||
},
|
||
uploadImage: async (buffer, mimeType, folder, extension) => {
|
||
uploaded = { buffer, mimeType, folder, extension };
|
||
return { url: 'https://images.example.com/reference.jpg' };
|
||
},
|
||
});
|
||
|
||
assert.equal(result, 'https://images.example.com/reference.jpg');
|
||
assert.deepEqual(uploaded.buffer, jpeg);
|
||
assert.equal(uploaded.mimeType, 'image/jpeg');
|
||
assert.equal(uploaded.folder, 'selection-fission/reference-images');
|
||
assert.equal(uploaded.extension, 'jpg');
|
||
assert.deepEqual(detectImageContent(jpeg, 'application/octet-stream'), {
|
||
mimeType: 'image/jpeg',
|
||
extension: 'jpg',
|
||
});
|
||
assert.equal(hasStandardImageExtension('https://example.com/a.jpg?size=800'), true);
|
||
assert.equal(hasStandardImageExtension(sourceUrl), false);
|
||
assert.equal(shouldRehostImageGenerationReference('https://cbu01.alicdn.com/img/ibank/example.jpg'), true);
|
||
assert.equal(shouldRehostImageGenerationReference('https://images.example.com/example.jpg'), false);
|
||
});
|
||
|
||
test('image generation surfaces structured upstream errors', () => {
|
||
assert.equal(getExternalRequestErrorMessage({
|
||
response: { data: { error: 'reference image format is unsupported' } },
|
||
}, 'fallback'), 'reference image format is unsupported');
|
||
});
|
||
|
||
test('image dimensions become an exact reduced aspect ratio', () => {
|
||
const png = Buffer.alloc(24);
|
||
Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]).copy(png, 0);
|
||
png.writeUInt32BE(1200, 16);
|
||
png.writeUInt32BE(1500, 20);
|
||
assert.deepEqual(parseImageDimensions(png), { width: 1200, height: 1500 });
|
||
assert.equal(formatImageAspectRatio({ width: 1200, height: 1500 }), '4:5');
|
||
});
|
||
|
||
test('selection pricing timestamps are normalized for MySQL DATETIME', () => {
|
||
assert.equal(toMysqlDateTime('2026-07-23T15:44:10.162Z'), '2026-07-23 15:44:10');
|
||
assert.equal(toMysqlDateTime('2026-07-23 15:44:10'), '2026-07-23 15:44:10');
|
||
assert.equal(toMysqlDateTime('not-a-date'), null);
|
||
});
|
||
|
||
test('workflow resume skips only nodes whose required outputs are complete', () => {
|
||
const task = {
|
||
replacedImageUrl: 'https://example.com/main.jpg',
|
||
skuSnapshot: {
|
||
rows: Array.from({ length: 5 }, (_, index) => ({
|
||
skuCode: `SKU-${index + 1}`,
|
||
skuImageUrl: `https://example.com/source-sku-${index + 1}.jpg`,
|
||
})),
|
||
},
|
||
skuSceneImages: {
|
||
mode: 'source_original',
|
||
selectedSkuCodes: Array.from({ length: 5 }, (_, index) => `SKU-${index + 1}`),
|
||
rows: Array.from({ length: 5 }, (_, index) => createVerifiedSkuImageRow({
|
||
skuCode: `SKU-${index + 1}`,
|
||
sourceImageUrl: `https://example.com/source-sku-${index + 1}.jpg`,
|
||
generatedImageUrl: `https://example.com/sku-${index + 1}.jpg`,
|
||
})),
|
||
},
|
||
skuNameCandidates: {
|
||
rows: Array.from({ length: 5 }, (_, index) => ({
|
||
skuCode: `SKU-${index + 1}`,
|
||
names: ['名称1', '名称2', '名称3', '名称4', '名称5'],
|
||
selectedName: '名称1',
|
||
})),
|
||
},
|
||
optimizedTitleCandidates: ['标题1', '标题2', '标题3', '标题4', '标题5'],
|
||
selectedProductTitle: '标题1',
|
||
pricingRows: Array.from({ length: 5 }, (_, index) => ({
|
||
skuCode: `SKU-${index + 1}`,
|
||
groupPrice: 100,
|
||
})),
|
||
activityPricingRows: Array.from({ length: 5 }, () => ({})),
|
||
pricingTableSavedAt: '2026-08-01 15:00:00',
|
||
};
|
||
|
||
for (const step of [3, 4, 5, 6, 7, 8]) {
|
||
assert.equal(isSelectionWorkflowTaskStepComplete(task, step), true);
|
||
}
|
||
assert.equal(
|
||
isSelectionWorkflowTaskStepComplete({
|
||
...task,
|
||
skuSceneImages: { rows: task.skuSceneImages.rows.slice(0, 4) },
|
||
}, 4),
|
||
false
|
||
);
|
||
assert.equal(
|
||
isSelectionWorkflowTaskStepComplete({
|
||
...task,
|
||
skuNameCandidates: {
|
||
rows: task.skuNameCandidates.rows.map((row, index) => (
|
||
index === 0 ? { ...row, selectedName: '' } : row
|
||
)),
|
||
},
|
||
}, 6),
|
||
false
|
||
);
|
||
assert.equal(
|
||
isSelectionWorkflowTaskStepComplete({ ...task, selectedProductTitle: '' }, 7),
|
||
false
|
||
);
|
||
assert.equal(
|
||
isSelectionWorkflowTaskStepComplete({ ...task, pricingTableSavedAt: null }, 8),
|
||
false
|
||
);
|
||
});
|
||
|
||
test('remaining workflow progress never moves backwards on resume', () => {
|
||
let progress = 44;
|
||
const observed = [progress];
|
||
for (const candidate of [30, 31.6, 33.2, 34.8, 36.4, 38, 38, 40.8, 43.6, 46.4, 49.2, 52]) {
|
||
progress = advanceWorkflowProgress(progress, candidate);
|
||
observed.push(progress);
|
||
}
|
||
|
||
assert.equal(
|
||
observed.every((value, index) => index === 0 || value >= observed[index - 1]),
|
||
true
|
||
);
|
||
assert.equal(progress, 52);
|
||
});
|
||
|
||
test('selection task becomes completed after a callback product ID exists', () => {
|
||
const plan = mapPlan({
|
||
id: 12,
|
||
scheme_code: 'FB202607231431A1B2C3',
|
||
status: 'pending',
|
||
auto_listed_product_id: '839201746512',
|
||
auto_listing_completed_at: '2026-07-23 16:20:00',
|
||
});
|
||
|
||
assert.equal(plan.status, 'listed');
|
||
assert.equal(plan.autoListingStatus, '上架完成');
|
||
assert.equal(plan.autoListingStatusCode, 'completed');
|
||
assert.equal(plan.autoListedProductId, '839201746512');
|
||
assert.equal(plan.autoListingCompletedAt, '2026-07-23 16:20:00');
|
||
});
|
||
|
||
test('auto listing callback accepts common product ID field aliases', () => {
|
||
assert.equal(normalizeAutoListingProductId({ productId: 'P-1' }), 'P-1');
|
||
assert.equal(normalizeAutoListingProductId({ product_id: 'P-2' }), 'P-2');
|
||
assert.equal(normalizeAutoListingProductId({ listedItemId: 'P-3' }), 'P-3');
|
||
assert.equal(normalizeAutoListingProductId({ goodsId: 12345 }), '12345');
|
||
});
|
||
|
||
test('selection task exposes claimed and failed states without a product ID', () => {
|
||
const claimed = mapPlan({
|
||
id: 13,
|
||
status: 'claimed',
|
||
claimed_by: 'worker-01',
|
||
});
|
||
const failed = mapPlan({
|
||
id: 14,
|
||
status: 'failed',
|
||
auto_listing_error_code: 'PLATFORM_REJECTED',
|
||
auto_listing_error_message: '平台审核拒绝',
|
||
});
|
||
|
||
assert.equal(claimed.status, 'claimed');
|
||
assert.equal(claimed.autoListingStatusCode, 'processing');
|
||
assert.equal(claimed.claimedBy, 'worker-01');
|
||
assert.equal(failed.status, 'failed');
|
||
assert.equal(failed.autoListingStatus, '上架失败');
|
||
assert.equal(failed.autoListingErrorCode, 'PLATFORM_REJECTED');
|
||
});
|
||
|
||
test('selection shop sync accepts common shop aliases and validates required fields', () => {
|
||
assert.deepEqual(
|
||
normalizeListingShop({
|
||
shop_id: 'shop-100',
|
||
shop_name: '测试旗舰店',
|
||
channel_name: '拼多多',
|
||
}),
|
||
{
|
||
shopId: 'shop-100',
|
||
shopCode: '',
|
||
shopName: '测试旗舰店',
|
||
platform: '拼多多',
|
||
accountName: '',
|
||
status: 'active',
|
||
clientId: '',
|
||
loginStatus: 'unknown',
|
||
loginMessage: '',
|
||
lastLoginCheckedAt: null,
|
||
}
|
||
);
|
||
assert.throws(() => normalizeListingShop({ shopName: '无编号店铺' }), /缺少 shopId/);
|
||
assert.equal(hashClaimToken('claim-token'), hashClaimToken('claim-token'));
|
||
assert.notEqual(hashClaimToken('claim-token'), hashClaimToken('other-token'));
|
||
});
|
||
|
||
test('pricing row exposes original, five optimized and selected SKU names', () => {
|
||
const row = mapPricingRow({
|
||
id: 7,
|
||
plan_item_id: 3,
|
||
sku_code: 'SG008830N1049V01',
|
||
sku_name: '款式一 100/30/180 北美原木纹色',
|
||
ai_sku_name_candidates_json: JSON.stringify([
|
||
'北美原木纹100cm书柜',
|
||
'原木纹窄款收纳书柜',
|
||
'北美原木色五层书柜',
|
||
'100cm原木纹展示书柜',
|
||
'北美原木纹储物书柜',
|
||
]),
|
||
selected_sku_name: '北美原木色五层书柜',
|
||
ai_sku_name_model: 'qwen3.7-max-2026-06-08',
|
||
});
|
||
|
||
assert.equal(row.originalSkuName, '款式一 100/30/180 北美原木纹色');
|
||
assert.equal(row.skuName, '北美原木色五层书柜');
|
||
assert.equal(row.selectedSkuName, '北美原木色五层书柜');
|
||
assert.equal(row.optimizedSkuNames.length, 5);
|
||
});
|
||
|
||
test('profit pricing table normalizes group price as the final auto-listing SKU price', () => {
|
||
const [row] = normalizeProfitPricingRows([{
|
||
barcode: 'SG008830N1049V01',
|
||
groupPrice: 399.9,
|
||
price: 219.9,
|
||
installFee: 30,
|
||
coupon: 20,
|
||
activityDiscount: 0.6,
|
||
shipping: 20,
|
||
commission: 0.06,
|
||
targetMargin: 0.15,
|
||
}]);
|
||
|
||
assert.equal(row.skuCode, 'SG008830N1049V01');
|
||
assert.equal(row.groupPrice, 399.9);
|
||
assert.equal(row.finalPrice, 219.9);
|
||
assert.equal(row.installationFee, 30);
|
||
assert.equal(row.discountAmount, 20);
|
||
assert.equal(row.activityDiscount, 0.6);
|
||
assert.equal(row.freightNational, 20);
|
||
assert.throws(
|
||
() => normalizeProfitPricingRows([{ skuCode: 'SKU-1', groupPrice: 0 }]),
|
||
/拼单价必须大于0/
|
||
);
|
||
});
|
||
|
||
test('main image generation prioritizes one company product image over the market scene', () => {
|
||
assert.deepEqual(
|
||
buildMainImageReferenceImages(
|
||
'https://example.com/company-product.jpg',
|
||
'https://example.com/market-scene.jpg'
|
||
),
|
||
[
|
||
'https://example.com/company-product.jpg',
|
||
'https://example.com/market-scene.jpg',
|
||
]
|
||
);
|
||
assert.deepEqual(
|
||
buildMainImageReferenceImages(
|
||
'https://example.com/same.jpg',
|
||
'https://example.com/same.jpg'
|
||
),
|
||
['https://example.com/same.jpg']
|
||
);
|
||
assert.deepEqual(
|
||
buildMainImageReferenceImages(
|
||
'https://example.com/1688-product.jpg',
|
||
'https://example.com/market-composition.jpg',
|
||
true
|
||
),
|
||
[
|
||
'https://example.com/1688-product.jpg',
|
||
'https://example.com/market-composition.jpg',
|
||
]
|
||
);
|
||
assert.deepEqual(normalizeMainImageRetryIndexes([3, 2, 3, 0, 6, '2']), [3, 2]);
|
||
});
|
||
|
||
test('removing a pricing SKU prunes it from the auto-listing workflow payloads', () => {
|
||
const scenePayload = {
|
||
total: 3,
|
||
selectedSkuCodes: ['SKU-A', 'SKU-B', 'SKU-C'],
|
||
rows: [
|
||
{ skuCode: 'SKU-A', generatedImageUrl: 'https://example.com/a.jpg' },
|
||
{ skuCode: 'SKU-B', generatedImageUrl: 'https://example.com/b.jpg' },
|
||
{ skuCode: 'SKU-C', generatedImageUrl: 'https://example.com/c.jpg' },
|
||
],
|
||
};
|
||
const namePayload = {
|
||
rows: [
|
||
{ skuCode: 'SKU-A', selectedName: 'A1' },
|
||
{ skuCode: 'SKU-B', selectedName: 'B1' },
|
||
{ skuCode: 'SKU-C', selectedName: 'C1' },
|
||
],
|
||
};
|
||
|
||
const nextScenePayload = pruneSelectionSkuPayload(
|
||
scenePayload,
|
||
['sku-a', 'SKU-C'],
|
||
{ syncSelection: true }
|
||
);
|
||
const nextNamePayload = pruneSelectionSkuPayload(namePayload, ['SKU-A', 'SKU-C']);
|
||
|
||
assert.equal(nextScenePayload.total, 2);
|
||
assert.deepEqual(nextScenePayload.selectedSkuCodes, ['SKU-A', 'SKU-C']);
|
||
assert.deepEqual(nextScenePayload.rows.map((row) => row.skuCode), ['SKU-A', 'SKU-C']);
|
||
assert.deepEqual(nextNamePayload.rows.map((row) => row.skuCode), ['SKU-A', 'SKU-C']);
|
||
assert.equal(scenePayload.total, 3);
|
||
assert.equal(namePayload.rows.length, 3);
|
||
});
|
||
|
||
test('one SPU delivery package contains base and PDD activity pricing tables', () => {
|
||
const baseRow = mapPricingRow({
|
||
plan_item_id: 3,
|
||
sku_code: 'SG008830N1049V01',
|
||
sku_name: '公司原名',
|
||
ai_sku_name_candidates_json: JSON.stringify(['名称1', '名称2', '名称3', '名称4', '名称5']),
|
||
selected_sku_name: '名称3',
|
||
group_price: 399.9,
|
||
});
|
||
baseRow.productCode = 'SPU-ACCIDENT';
|
||
const activityRow = mapActivityPricingRow({
|
||
plan_item_id: 3,
|
||
sku_code: 'SG008830N1049V01',
|
||
group_price: 399.9,
|
||
final_group_price: 399.9,
|
||
activity_prices_json: JSON.stringify(buildEmptyPddActivityPrices()),
|
||
});
|
||
const payload = buildDeliveryPayload({
|
||
id: 1,
|
||
schemeCode: 'FB-TEST',
|
||
items: [{
|
||
id: 3,
|
||
slotNo: 1,
|
||
marketItemId: '10001',
|
||
priceBand: '100 - 200',
|
||
priceBandList: ['100 - 200', '200 - 300'],
|
||
selectedSpuCode: 'SG0088',
|
||
pricingRows: [baseRow],
|
||
activityPricingRows: [activityRow],
|
||
}],
|
||
});
|
||
|
||
assert.equal(payload.pricingTables[0].tableType, 'base');
|
||
assert.equal(payload.pricingTables[0].rows[0].skuName, '名称3');
|
||
assert.equal(payload.pricingTables[0].rows[0].optimizedSkuNames.length, 5);
|
||
assert.equal(payload.pricingTables[0].rows[0].finalSkuPrice, 399.9);
|
||
assert.equal(payload.pricingTables[0].rows[0].skuCode, 'SG008830N1049V01');
|
||
assert.equal(payload.pricingTables[0].rows[0].sourceSkuCode, 'SG008830N1049V01');
|
||
assert.equal(payload.activityPricingTables[0].tableType, 'pdd_activity');
|
||
assert.equal(payload.activityPricingTables[0].rows[0].skuCode, 'SG008830N1049V01');
|
||
assert.ok(payload.activityPricingTables[0].rows[0].activityPrices.seckill65);
|
||
assert.equal(payload.activityPricingTables[0].rows[0].finalSkuPrice, 399.9);
|
||
assert.equal(payload.marketImages[0].priceBand, '100 - 200');
|
||
assert.deepEqual(payload.automaticListingTasks[0].priceBandList, ['100 - 200', '200 - 300']);
|
||
});
|
||
|
||
test('one market item can create multiple unique SPU listing tasks', () => {
|
||
const selections = normalizeSpuSelections({
|
||
selections: [
|
||
{ spuCode: 'SG0088', spuName: '书柜A' },
|
||
{ productCode: 'SG0099', spuName: '书柜B' },
|
||
{ spuCode: 'sg0088', spuName: '重复项' },
|
||
],
|
||
});
|
||
|
||
assert.deepEqual(selections.map((row) => row.spuCode), ['SG0088', 'SG0099']);
|
||
});
|
||
|
||
test('workflow summary exposes seven visible nodes and multi-SPU progress', () => {
|
||
const workflow = buildWorkflowSummary({
|
||
workflowStatus: 'waiting',
|
||
workflowStep: 3,
|
||
workflowProgress: 30,
|
||
items: [
|
||
{ id: 1, sourceItemId: 1, slotNo: 1, variantNo: 1, selectedSpuCode: 'SG0088', workflowStatus: 'waiting' },
|
||
{ id: 2, sourceItemId: 1, slotNo: 1, variantNo: 2, selectedSpuCode: 'SG0099', workflowStatus: 'waiting' },
|
||
{ id: 3, sourceItemId: 3, slotNo: 2, variantNo: 1, selectedSpuCode: 'SG0100', workflowStatus: 'waiting' },
|
||
],
|
||
});
|
||
|
||
assert.equal(workflow.steps.length, 7);
|
||
assert.equal(workflow.currentStepLabel, '主视觉商品智能重构');
|
||
assert.deepEqual(workflow.steps.map((step) => step.progress), [100, 100, 100, 0, 0, 0, 0]);
|
||
assert.ok(workflow.steps.every((step) => step.progress >= 0 && step.progress <= 100));
|
||
assert.equal(workflow.sourceTotal, 2);
|
||
assert.equal(workflow.taskTotal, 3);
|
||
assert.equal(workflow.requiresUser, true);
|
||
});
|
||
|
||
test('workflow summary returns older plans to an unconfirmed AI approval node', () => {
|
||
const workflow = buildWorkflowSummary({
|
||
workflowStatus: 'waiting',
|
||
workflowStep: 7,
|
||
workflowProgress: 74,
|
||
items: [{
|
||
id: 1,
|
||
sourceItemId: 1,
|
||
slotNo: 1,
|
||
variantNo: 1,
|
||
selectedSpuCode: 'CBG0054',
|
||
whiteBackgroundAssets: { rows: [{ skuCode: 'SKU-1', previewImage: { previewUrl: 'https://example.com/main-sku.jpg' } }] },
|
||
selectedMainSkuImageUrl: 'https://example.com/main-sku.jpg',
|
||
mainImageGeneration: {
|
||
rows: Array.from({ length: 5 }, (_, index) => ({ imageUrl: `https://example.com/main-${index}.jpg` })),
|
||
},
|
||
skuSnapshot: { rows: [{ skuCode: 'SKU-1' }] },
|
||
skuSceneImages: {
|
||
selectedSkuCodes: ['SKU-1'],
|
||
rows: [{ skuCode: 'SKU-1', generatedImageUrl: 'https://example.com/sku-1.jpg' }],
|
||
},
|
||
skuNameCandidates: {
|
||
rows: [{ skuCode: 'SKU-1', names: ['名称1', '名称2', '名称3', '名称4', '名称5'], selectedName: '' }],
|
||
},
|
||
optimizedTitleCandidates: ['标题1', '标题2', '标题3', '标题4', '标题5'],
|
||
workflowStatus: 'waiting',
|
||
}],
|
||
});
|
||
|
||
assert.equal(workflow.currentStep, 8);
|
||
assert.equal(workflow.currentStepLabel, '全渠道价格策略编排');
|
||
assert.equal(workflow.progress, 30);
|
||
assert.equal(workflow.requiresUser, true);
|
||
});
|
||
|
||
test('legacy video step is treated as complete without stored video assets', () => {
|
||
assert.equal(isSelectionWorkflowTaskStepComplete({}, 5), true);
|
||
const workflow = buildWorkflowSummary({
|
||
workflowStatus: 'waiting',
|
||
workflowStep: 5,
|
||
workflowProgress: 52,
|
||
items: [],
|
||
});
|
||
|
||
assert.equal(workflow.currentStep, 6);
|
||
assert.equal(workflow.steps.some((step) => step.step === 5), false);
|
||
});
|
||
|
||
test('automatic-listing delivery hydrates video assets by SPU', async () => {
|
||
const originalGet = axios.get;
|
||
const originalBaseUrl = process.env.SELECTION_VIDEO_ASSETS_BASE_URL;
|
||
const originalApiKey = process.env.SELECTION_VIDEO_ASSETS_API_KEY;
|
||
process.env.SELECTION_VIDEO_ASSETS_BASE_URL = 'https://video.example.com';
|
||
process.env.SELECTION_VIDEO_ASSETS_API_KEY = 'test-video-key';
|
||
axios.get = async (url, config) => {
|
||
assert.equal(url, 'https://video.example.com/api/external/video-assets');
|
||
assert.equal(config.params.keyword, 'SG0088');
|
||
assert.equal(config.headers['X-API-Key'], 'test-video-key');
|
||
return {
|
||
data: {
|
||
data: {
|
||
items: [{ materialId: 'video-1', downloadUrl: 'https://example.com/video-1.mp4' }],
|
||
},
|
||
},
|
||
};
|
||
};
|
||
|
||
try {
|
||
const hydratedPlan = await hydrateDeliveryVideoAssets({
|
||
items: [{ id: 1, selectedSpuCode: 'SG0088' }],
|
||
listingTasks: [{ id: 1, selectedSpuCode: 'SG0088' }],
|
||
});
|
||
const mainVideo = buildDeliveryVideoPayload(hydratedPlan.listingTasks[0]);
|
||
|
||
assert.equal(mainVideo.spuCode, 'SG0088');
|
||
assert.equal(mainVideo.videos.length, 1);
|
||
assert.equal(mainVideo.videos[0].materialId, 'video-1');
|
||
assert.equal(mainVideo.warning, null);
|
||
} finally {
|
||
axios.get = originalGet;
|
||
if (originalBaseUrl === undefined) delete process.env.SELECTION_VIDEO_ASSETS_BASE_URL;
|
||
else process.env.SELECTION_VIDEO_ASSETS_BASE_URL = originalBaseUrl;
|
||
if (originalApiKey === undefined) delete process.env.SELECTION_VIDEO_ASSETS_API_KEY;
|
||
else process.env.SELECTION_VIDEO_ASSETS_API_KEY = originalApiKey;
|
||
}
|
||
});
|
||
|
||
test('desktop delivery materials are fetched by company SPU on the backend', async () => {
|
||
const originalGet = axios.get;
|
||
const originalApiKey = process.env.SELECTION_MATERIAL_ASSETS_API_KEY;
|
||
process.env.SELECTION_MATERIAL_ASSETS_API_KEY = 'server-only-material-key';
|
||
axios.get = async (_url, config) => {
|
||
assert.equal(config.params.spu, 'SG0082');
|
||
assert.equal(config.headers['X-API-Key'], 'server-only-material-key');
|
||
if (config.params.resourceType === 'white-background') {
|
||
return { data: { data: { materialId: 'white-1', skuList: [] } } };
|
||
}
|
||
assert.equal(config.params.resourceType, 'task-detail');
|
||
return {
|
||
data: {
|
||
data: {
|
||
resourceType: 'task-detail',
|
||
candidateCount: 1,
|
||
candidates: [{
|
||
materialId: 'detail-1',
|
||
downloadUrls: { detail: ['https://example.com/company-detail.jpg'] },
|
||
}],
|
||
},
|
||
},
|
||
};
|
||
};
|
||
|
||
try {
|
||
const materials = await buildDeliveryMaterials({ selectedSpuCode: 'sg0082' });
|
||
|
||
assert.equal(materials.length, 1);
|
||
assert.equal(materials[0].source, 'company-material-service');
|
||
assert.deepEqual(
|
||
materials[0].taskDetail.candidates[0].downloadUrls.detail,
|
||
['https://example.com/company-detail.jpg']
|
||
);
|
||
} finally {
|
||
axios.get = originalGet;
|
||
if (originalApiKey === undefined) delete process.env.SELECTION_MATERIAL_ASSETS_API_KEY;
|
||
else process.env.SELECTION_MATERIAL_ASSETS_API_KEY = originalApiKey;
|
||
}
|
||
});
|
||
|
||
test('desktop app claim payload contains only the claimed SPU child task', () => {
|
||
const firstTask = {
|
||
id: 101,
|
||
taskCode: 'FB-TEST-S01-01',
|
||
sourceItemId: 11,
|
||
slotNo: 1,
|
||
variantNo: 1,
|
||
marketItemId: 'market-1',
|
||
mainImageUrl: 'https://example.com/market-1.jpg',
|
||
replacedImageUrl: 'https://example.com/generated-1.jpg',
|
||
selectedSpuCode: 'SG0088',
|
||
selectedSpuName: '书柜A',
|
||
selectedMainSkuCode: 'SG0088-A',
|
||
selectedMainSkuImageUrl: 'https://example.com/white-a.jpg',
|
||
selectedProductTitle: 'AI标题A',
|
||
optimizedTitleCandidates: ['标题1', '标题2', '标题3', '标题4', '标题5'],
|
||
pricingTableSavedAt: '2026-08-01 09:00:00',
|
||
pricingTableVersion: 1,
|
||
pricingRows: [mapPricingRow({
|
||
plan_item_id: 101,
|
||
sku_code: 'SG0088-A',
|
||
sku_name: '公司原名A',
|
||
ai_sku_name_candidates_json: JSON.stringify(['名称1', '名称2', '名称3', '名称4', '名称5']),
|
||
selected_sku_name: '名称1',
|
||
generated_image_url: 'https://example.com/sku-a.jpg',
|
||
group_price: 399.9,
|
||
})],
|
||
activityPricingRows: [mapActivityPricingRow({
|
||
plan_item_id: 101,
|
||
sku_code: 'SG0088-A',
|
||
group_price: 399.9,
|
||
final_group_price: 399.9,
|
||
activity_prices_json: JSON.stringify(buildEmptyPddActivityPrices()),
|
||
})],
|
||
skuSnapshot: { rows: [{
|
||
skuCode: 'SG0088-A',
|
||
skuName: 'SG0088-A',
|
||
skuImageUrl: 'https://example.com/white-a.jpg',
|
||
}] },
|
||
skuSceneImages: {
|
||
mode: 'source_original',
|
||
selectedSkuCodes: ['SG0088-A'],
|
||
rows: [createVerifiedSkuImageRow({
|
||
skuCode: 'SG0088-A',
|
||
sourceImageUrl: 'https://example.com/white-a.jpg',
|
||
generatedImageUrl: 'https://example.com/sku-a.jpg',
|
||
})],
|
||
},
|
||
};
|
||
const secondTask = {
|
||
...firstTask,
|
||
id: 102,
|
||
taskCode: 'FB-TEST-S01-02',
|
||
variantNo: 2,
|
||
selectedSpuCode: 'SG0099',
|
||
selectedSpuName: '书柜B',
|
||
};
|
||
const plan = {
|
||
id: 9,
|
||
schemeCode: 'FB-TEST',
|
||
dingtalkUserName: '张三',
|
||
sourceItems: Array.from({ length: 5 }, (_, index) => ({
|
||
id: 11 + index,
|
||
sourceItemId: 11 + index,
|
||
marketItemId: `market-${index + 1}`,
|
||
})),
|
||
items: [firstTask, secondTask],
|
||
listingTasks: [firstTask, secondTask],
|
||
};
|
||
|
||
const taskPayload = buildDeliveryTaskPayload(plan, secondTask);
|
||
|
||
assert.equal(taskPayload.taskId, 102);
|
||
assert.equal(taskPayload.taskCode, 'FB-TEST-S01-02');
|
||
assert.equal(taskPayload.spuCode, 'SG0099');
|
||
assert.equal(taskPayload.pricingTable.taskId, 102);
|
||
assert.equal(taskPayload.activityPricingTable.taskId, 102);
|
||
assert.equal(taskPayload.marketImage.taskId, 102);
|
||
assert.deepEqual(taskPayload.marketImages.map((row) => row.taskId), [102]);
|
||
assert.deepEqual(taskPayload.selectedSpus.map((row) => row.taskId), [102]);
|
||
assert.deepEqual(taskPayload.productTitles.map((row) => row.taskId), [102]);
|
||
assert.deepEqual(taskPayload.skuImageSets.map((row) => row.taskId), [102]);
|
||
assert.deepEqual(taskPayload.mainVideos.map((row) => row.taskId), [102]);
|
||
assert.deepEqual(taskPayload.pricingTables.map((row) => row.taskId), [102]);
|
||
assert.deepEqual(taskPayload.activityPricingTables.map((row) => row.taskId), [102]);
|
||
assert.deepEqual(taskPayload.automaticListingTasks.map((row) => row.taskId), [102]);
|
||
assert.equal(taskPayload.readiness.complete, true, JSON.stringify(taskPayload.readiness.missing));
|
||
assert.deepEqual(taskPayload.mainVideo.videos, []);
|
||
assert.equal(taskPayload.skuFinalPrices[0].finalSkuPrice, 399.9);
|
||
assert.equal(
|
||
taskPayload.skuFinalPrices[0].finalSkuPriceSource,
|
||
'profitPricingTable.groupPrice'
|
||
);
|
||
});
|
||
|
||
test('five market IDs keep independent main-image and SKU-image sets', () => {
|
||
const listingTasks = Array.from({ length: 5 }, (_, index) => {
|
||
const number = index + 1;
|
||
const taskId = 200 + number;
|
||
const marketItemId = `market-${number}`;
|
||
const spuCode = `SPU-${number}`;
|
||
const skuCodes = [`${spuCode}-A`, `${spuCode}-B`];
|
||
return {
|
||
id: taskId,
|
||
taskCode: `FB-FIVE-S${String(number).padStart(2, '0')}-01`,
|
||
sourceItemId: taskId,
|
||
slotNo: number,
|
||
variantNo: 1,
|
||
marketItemId,
|
||
mainImageUrl: `https://assets.example.com/${marketItemId}/market.jpg`,
|
||
replacedImageUrl: `https://assets.example.com/${marketItemId}/main-1.jpg`,
|
||
mainImageGeneration: {
|
||
rows: Array.from({ length: 5 }, (_, imageIndex) => ({
|
||
imageUrl: `https://assets.example.com/${marketItemId}/main-${imageIndex + 1}.jpg`,
|
||
})),
|
||
},
|
||
selectedSpuCode: spuCode,
|
||
selectedSpuName: `商品 ${number}`,
|
||
selectedMainSkuCode: skuCodes[0],
|
||
selectedMainSkuImageUrl: `https://assets.example.com/${marketItemId}/white.jpg`,
|
||
selectedProductTitle: `商品标题 ${number}`,
|
||
optimizedTitleCandidates: Array.from({ length: 5 }, (_, titleIndex) => `标题 ${number}-${titleIndex + 1}`),
|
||
pricingTableSavedAt: '2026-08-14 10:00:00',
|
||
skuSnapshot: { rows: skuCodes.map((skuCode) => ({
|
||
skuCode,
|
||
skuName: skuCode,
|
||
skuImageUrl: `https://assets.example.com/${marketItemId}/${skuCode}-source.jpg`,
|
||
})) },
|
||
skuSceneImages: {
|
||
mode: 'source_original',
|
||
selectedSkuCodes: skuCodes,
|
||
rows: skuCodes.map((skuCode) => createVerifiedSkuImageRow({
|
||
skuCode,
|
||
sourceImageUrl: `https://assets.example.com/${marketItemId}/${skuCode}-source.jpg`,
|
||
generatedImageUrl: `https://assets.example.com/${marketItemId}/${skuCode}-generated.jpg`,
|
||
})),
|
||
},
|
||
pricingRows: skuCodes.map((skuCode) => ({
|
||
skuCode,
|
||
skuName: `原始名称 ${skuCode}`,
|
||
selectedSkuName: `上架名称 ${skuCode}`,
|
||
optimizedSkuNames: Array.from({ length: 5 }, (_, nameIndex) => `名称 ${skuCode}-${nameIndex + 1}`),
|
||
skuImageUrl: `https://assets.example.com/${marketItemId}/${skuCode}-source.jpg`,
|
||
generatedImageUrl: `https://assets.example.com/${marketItemId}/${skuCode}-generated.jpg`,
|
||
groupPrice: 100 + number,
|
||
})),
|
||
activityPricingRows: skuCodes.map((skuCode) => ({
|
||
skuCode,
|
||
groupPrice: 100 + number,
|
||
})),
|
||
};
|
||
});
|
||
const plan = {
|
||
id: 20,
|
||
schemeCode: 'FB-FIVE',
|
||
sourceItems: listingTasks.map((task) => ({
|
||
id: task.id,
|
||
sourceItemId: task.id,
|
||
marketItemId: task.marketItemId,
|
||
})),
|
||
items: listingTasks,
|
||
listingTasks,
|
||
};
|
||
|
||
const payload = buildDeliveryPayload(plan);
|
||
assert.equal(payload.marketImages.length, 5);
|
||
assert.equal(payload.skuImageSets.length, 5);
|
||
|
||
listingTasks.forEach((task, index) => {
|
||
const number = index + 1;
|
||
const marketItemId = `market-${number}`;
|
||
const mainImageSet = payload.marketImages.find((row) => row.taskId === task.id);
|
||
const skuImageSet = payload.skuImageSets.find((row) => row.taskId === task.id);
|
||
assert.equal(mainImageSet.marketItemId, marketItemId);
|
||
assert.equal(mainImageSet.generatedMainImageUrls.length, 5);
|
||
assert.ok(mainImageSet.generatedMainImageUrls.every((url) => url.includes(`/${marketItemId}/`)));
|
||
assert.equal(skuImageSet.marketItemId, marketItemId);
|
||
assert.equal(skuImageSet.skus.length, 2);
|
||
assert.ok(skuImageSet.skus.every((sku) => (
|
||
sku.skuCode.startsWith(`SPU-${number}-`)
|
||
&& sku.generatedImageUrl.includes(`/${marketItemId}/`)
|
||
)));
|
||
|
||
const claimedPayload = buildDeliveryTaskPayload(plan, task);
|
||
assert.equal(claimedPayload.marketItemId, marketItemId);
|
||
assert.equal(claimedPayload.marketImage.generatedMainImageUrls.length, 5);
|
||
assert.ok(claimedPayload.marketImage.generatedMainImageUrls.every((url) => url.includes(`/${marketItemId}/`)));
|
||
assert.ok(claimedPayload.skuImageSet.skus.every((sku) => sku.generatedImageUrl.includes(`/${marketItemId}/`)));
|
||
});
|
||
});
|
||
|
||
test('desktop app delivery does not require a shop selected by the web app', () => {
|
||
const plan = {
|
||
schemeCode: 'FB-DESKTOP-SHOP',
|
||
sourceItems: [{ id: 1, marketItemId: 'market-1' }],
|
||
listingTasks: [{
|
||
id: 1,
|
||
marketItemId: 'market-1',
|
||
listingStatus: 'pending',
|
||
mainImageUrl: 'https://example.com/market.jpg',
|
||
replacedImageUrl: 'https://example.com/generated.jpg',
|
||
selectedSpuCode: 'SG0088',
|
||
selectedMainSkuCode: 'SG0088-A',
|
||
selectedMainSkuImageUrl: 'https://example.com/white.jpg',
|
||
selectedProductTitle: '测试商品标题',
|
||
optimizedTitleCandidates: ['标题1', '标题2', '标题3', '标题4', '标题5'],
|
||
pricingTableSavedAt: '2026-08-11 10:00:00',
|
||
skuSnapshot: { rows: [{
|
||
skuCode: 'SG0088-A',
|
||
skuName: 'SG0088-A',
|
||
skuImageUrl: 'https://example.com/sku-source.jpg',
|
||
}] },
|
||
skuSceneImages: {
|
||
mode: 'source_original',
|
||
selectedSkuCodes: ['SG0088-A'],
|
||
rows: [createVerifiedSkuImageRow({
|
||
skuCode: 'SG0088-A',
|
||
sourceImageUrl: 'https://example.com/sku-source.jpg',
|
||
generatedImageUrl: 'https://example.com/sku.jpg',
|
||
})],
|
||
},
|
||
pricingRows: [{
|
||
skuCode: 'SG0088-A',
|
||
selectedSkuName: '测试SKU',
|
||
optimizedSkuNames: ['名称1', '名称2', '名称3', '名称4', '名称5'],
|
||
generatedImageUrl: 'https://example.com/sku.jpg',
|
||
groupPrice: 399,
|
||
}],
|
||
activityPricingRows: [{ skuCode: 'SG0088-A', groupPrice: 399 }],
|
||
}],
|
||
};
|
||
const readiness = buildDeliveryReadiness(plan, { requireSubmitted: true });
|
||
|
||
assert.equal(readiness.complete, true, JSON.stringify(readiness.missing));
|
||
assert.equal(readiness.missing.some((item) => item.field === 'listingShop'), false);
|
||
|
||
const beforeSubmission = buildDeliveryReadiness({
|
||
...plan,
|
||
listingTasks: plan.listingTasks.map((item) => ({ ...item, listingStatus: 'draft' })),
|
||
}, { requireSubmitted: true });
|
||
assert.equal(beforeSubmission.complete, false);
|
||
assert.equal(beforeSubmission.missing.some((item) => item.field === 'listingStatus'), true);
|
||
});
|
||
|
||
test('delivery accepts a confirmed SKU name without retaining AI name candidates', () => {
|
||
const skuCode = 'SG0088-A';
|
||
const task = {
|
||
id: 1,
|
||
marketItemId: 'market-1',
|
||
mainImageUrl: 'https://example.com/market.jpg',
|
||
replacedImageUrl: 'https://example.com/generated.jpg',
|
||
selectedSpuCode: 'SG0088',
|
||
selectedMainSkuCode: skuCode,
|
||
selectedMainSkuImageUrl: 'https://example.com/white.jpg',
|
||
selectedProductTitle: '测试商品标题',
|
||
optimizedTitleCandidates: ['标题1', '标题2', '标题3', '标题4', '标题5'],
|
||
pricingTableSavedAt: '2026-08-21 10:00:00',
|
||
skuSnapshot: { rows: [{
|
||
skuCode,
|
||
skuName: '公司原始SKU名',
|
||
skuImageUrl: 'https://example.com/sku-source.jpg',
|
||
}] },
|
||
skuSceneImages: {
|
||
mode: 'source_original',
|
||
selectedSkuCodes: [skuCode],
|
||
rows: [createVerifiedSkuImageRow({
|
||
skuCode,
|
||
skuName: '公司原始SKU名',
|
||
sourceImageUrl: 'https://example.com/sku-source.jpg',
|
||
generatedImageUrl: 'https://example.com/sku-source.jpg',
|
||
})],
|
||
},
|
||
pricingRows: [{
|
||
skuCode,
|
||
selectedSkuName: '用户确认的上架SKU名',
|
||
optimizedSkuNames: [],
|
||
generatedImageUrl: 'https://example.com/sku-source.jpg',
|
||
groupPrice: 399,
|
||
}],
|
||
activityPricingRows: [{ skuCode, groupPrice: 399 }],
|
||
};
|
||
const readiness = buildDeliveryReadiness({
|
||
schemeCode: 'FB-CONFIRMED-NAME',
|
||
sourceItems: [{ id: 1, marketItemId: 'market-1' }],
|
||
listingTasks: [task],
|
||
});
|
||
|
||
assert.equal(readiness.complete, true, JSON.stringify(readiness.missing));
|
||
});
|
||
|
||
test('1688 delivery validates and exports only the SKU subset selected for this listing', () => {
|
||
const pricingRows = ['SKU-A', 'SKU-B'].map((skuCode) => ({
|
||
skuCode,
|
||
selectedSkuName: `上架名称 ${skuCode}`,
|
||
color: '白色',
|
||
size: '80cm',
|
||
internalProductCode: `STYLE-1688-${skuCode}`,
|
||
productCodeProductName: `编码名称 ${skuCode}`,
|
||
productCodeColorSpec: `颜色规格 ${skuCode}`,
|
||
optimizedSkuNames: Array.from({ length: 5 }, (_, index) => `AI 名称 ${skuCode}-${index + 1}`),
|
||
generatedImageUrl: `https://example.com/${skuCode}.jpg`,
|
||
groupPrice: 99,
|
||
}));
|
||
const currentSchemaPricingRows = pricingRows.map((row) => {
|
||
const next = { ...row };
|
||
delete next.optimizedSkuNames;
|
||
delete next.generatedImageUrl;
|
||
return next;
|
||
});
|
||
const item = {
|
||
id: 1688,
|
||
workflowProfile: 'alibaba_1688_pending',
|
||
marketItemId: '1688:123456',
|
||
title: '1688 原始标题',
|
||
listingStatus: 'pending',
|
||
mainImageUrl: 'https://example.com/source.jpg',
|
||
replacedImageUrl: 'https://example.com/main.jpg',
|
||
selectedSpuCode: '1688:123456',
|
||
selectedMainSkuCode: 'SKU-A',
|
||
selectedMainSkuImageUrl: 'https://example.com/source.jpg',
|
||
selectedProductTitle: '1688 原始标题',
|
||
optimizedTitleCandidates: ['AI标题1', 'AI标题2', 'AI标题3', 'AI标题4', 'AI标题5'],
|
||
pricingTableSavedAt: '2026-08-15 12:00:00',
|
||
productCodeStatus: 'completed',
|
||
productCodeStyleCode: 'STYLE-1688',
|
||
metadata: {
|
||
sourceRow: {
|
||
product: {
|
||
detailImages: ['https://example.com/detail.jpg'],
|
||
skus: [
|
||
{ skuId: 'SKU-A', name: '白色', price: 60 },
|
||
{ skuId: 'SKU-B', name: '黑色', price: 65 },
|
||
],
|
||
},
|
||
},
|
||
},
|
||
skuSnapshot: {
|
||
rows: [
|
||
{ skuCode: 'SKU-A', skuName: '白色', skuImageUrl: 'https://example.com/SKU-A-source.jpg', purchasePrice: 60 },
|
||
{ skuCode: 'SKU-B', skuName: '黑色', purchasePrice: 65 },
|
||
],
|
||
},
|
||
mainImageGeneration: {
|
||
rows: Array.from({ length: 10 }, (_, index) => ({
|
||
index: index + 1,
|
||
imageUrl: `https://example.com/main-${index + 1}.jpg`,
|
||
})),
|
||
},
|
||
skuSceneImages: {
|
||
mode: 'source_original',
|
||
selectedSkuCodes: ['SKU-A'],
|
||
rows: [createVerifiedSkuImageRow({
|
||
skuCode: 'SKU-A',
|
||
skuName: '白色',
|
||
sourceImageUrl: 'https://example.com/SKU-A-source.jpg',
|
||
generatedImageUrl: 'https://example.com/SKU-A.jpg',
|
||
})],
|
||
},
|
||
skuNameCandidates: {
|
||
rows: ['SKU-A', 'SKU-B'].map((skuCode) => ({
|
||
skuCode,
|
||
names: Array.from({ length: 5 }, (_, index) => `AI-${skuCode}-${index + 1}`),
|
||
selectedName: `上架名称 ${skuCode}`,
|
||
})),
|
||
},
|
||
pricingRows: currentSchemaPricingRows,
|
||
activityPricingRows: [],
|
||
};
|
||
const plan = {
|
||
id: 1688,
|
||
schemeCode: '1688-SUBSET',
|
||
workflowProfile: 'alibaba_1688_pending',
|
||
sourceItems: [{ id: item.id, marketItemId: item.marketItemId }],
|
||
listingTasks: [item],
|
||
};
|
||
|
||
assert.deepEqual(filterWorkflowRowsBySelectedSkus(item, pricingRows).map((row) => row.skuCode), ['SKU-A']);
|
||
assert.equal(buildDeliveryReadiness(plan, { requireSubmitted: true }).complete, true);
|
||
|
||
const payload = buildDeliveryPayload(plan);
|
||
assert.deepEqual(payload.pricingTables[0].rows.map((row) => row.skuCode), ['STYLE-1688-SKU-A']);
|
||
assert.deepEqual(payload.activityPricingTables, []);
|
||
assert.deepEqual(payload.skuImageSets[0].skus.map((row) => row.skuCode), ['STYLE-1688-SKU-A']);
|
||
assert.deepEqual(payload.skuImageSets[0].skus[0].specs, [
|
||
{ name: '颜色', value: '白色' },
|
||
{ name: '尺寸', value: '80cm' },
|
||
]);
|
||
assert.deepEqual(payload.pricingTables[0].rows[0].specs, [
|
||
{ name: '颜色', value: '白色' },
|
||
{ name: '尺寸', value: '80cm' },
|
||
]);
|
||
|
||
const expectedProductCodeFields = {
|
||
sourceSkuCode: 'SKU-A',
|
||
internalProductCode: 'STYLE-1688-SKU-A',
|
||
productCodeProductName: '编码名称 SKU-A',
|
||
productCodeColorSpec: '颜色规格 SKU-A',
|
||
productCode: 'STYLE-1688-SKU-A',
|
||
listingSkuCode: 'STYLE-1688-SKU-A',
|
||
};
|
||
for (const row of [
|
||
payload.pricingTables[0].rows[0],
|
||
payload.skuImageSets[0].skus[0],
|
||
payload.automaticListingTasks[0].skuProductCodes[0],
|
||
]) {
|
||
assert.deepEqual(
|
||
Object.fromEntries(Object.keys(expectedProductCodeFields).map((key) => [key, row[key]])),
|
||
expectedProductCodeFields
|
||
);
|
||
}
|
||
|
||
const taskPayload = buildDeliveryTaskPayload(plan, item);
|
||
assert.deepEqual(taskPayload.skuProductCodes[0], {
|
||
skuCode: 'STYLE-1688-SKU-A',
|
||
...expectedProductCodeFields,
|
||
});
|
||
assert.deepEqual(
|
||
Object.fromEntries(Object.keys(expectedProductCodeFields).map((key) => [
|
||
key,
|
||
taskPayload.skuFinalPrices[0][key],
|
||
])),
|
||
expectedProductCodeFields
|
||
);
|
||
|
||
for (const missingField of [
|
||
'internalProductCode',
|
||
'productCodeProductName',
|
||
'productCodeColorSpec',
|
||
]) {
|
||
const incompleteItem = {
|
||
...item,
|
||
listingCompliance: { complete: true, blockers: [], warnings: [] },
|
||
pricingRows: pricingRows.map((row) => (
|
||
row.skuCode === 'SKU-A' ? { ...row, [missingField]: '' } : row
|
||
)),
|
||
};
|
||
const incompletePlan = { ...plan, listingTasks: [incompleteItem] };
|
||
const compliance = buildAlibaba1688ListingCompliance(incompleteItem, incompletePlan);
|
||
assert.equal(compliance.checks.encodedSkuCount, 0);
|
||
assert.ok(compliance.blockers.some((row) => row.field === 'productCodes'));
|
||
const readiness = buildDeliveryReadiness(incompletePlan, { requireSubmitted: true });
|
||
assert.equal(readiness.complete, false);
|
||
assert.ok(readiness.missing.some((row) => row.field === 'productCodes'));
|
||
}
|
||
});
|
||
|
||
test('1688 title candidates remain one original title plus five distinct AI alternatives', () => {
|
||
const item = {
|
||
workflowProfile: 'alibaba_1688_pending',
|
||
title: '1688 原始标题',
|
||
};
|
||
const generated = validateAdditionalProductTitleCandidates(item, [
|
||
'AI 标题 1',
|
||
'AI 标题 2',
|
||
'AI 标题 3',
|
||
'AI 标题 4',
|
||
'AI 标题 5',
|
||
]);
|
||
assert.equal(generated.length, 5);
|
||
assert.deepEqual(getWorkflowProductTitleOptions({
|
||
...item,
|
||
optimizedTitleCandidates: generated,
|
||
}), [
|
||
'1688 原始标题',
|
||
'AI 标题 1',
|
||
'AI 标题 2',
|
||
'AI 标题 3',
|
||
'AI 标题 4',
|
||
'AI 标题 5',
|
||
]);
|
||
assert.throws(() => validateAdditionalProductTitleCandidates(item, [
|
||
'1688 原始标题',
|
||
'AI 标题 1',
|
||
'AI 标题 2',
|
||
'AI 标题 3',
|
||
'AI 标题 4',
|
||
]), /必须返回5条不同于原始标题/);
|
||
});
|
||
|
||
test('relisting selects only successfully listed child tasks', () => {
|
||
const tasks = getRelistableSelectionTasks({
|
||
listingTasks: [
|
||
{ id: 1, listingStatus: 'listed', listedItemId: '10001' },
|
||
{ id: 2, listingStatus: 'pending', listedItemId: '' },
|
||
{ id: 3, listingStatus: 'failed', listedItemId: '' },
|
||
{ id: 4, listingStatus: 'pending', listedItemId: 'legacy-10004' },
|
||
],
|
||
});
|
||
|
||
assert.deepEqual(tasks.map((item) => item.id), [1, 4]);
|
||
});
|