import { test } from 'node:test'; import assert from 'node:assert/strict'; import { innovationBrief } from '../src/core/industry-brief.js'; import { innovationFigures, innovationMaterials } from '../src/core/industry-materials.js'; test('方向资料只包含选中的方向及数据库定位信息', () => { const evidence = { reviewId: 'jd:1:r1', content: '此用户未填写评价内容', appendContent: '全包太辣', analysisEligible: false, hits: [] }; const direction = id => ({ id, title: '调味控制' + id, status: '需要补证', reason: '1组线索', question: '怎么改善', preserve: '保留风味', action: '对照指引', unknown: '现有说明待核', support: [evidence], benefits: [] }); const data = { generation: 'g1', rulesVersion: 'rule1', scope: { products: 3, reviewedProducts: 1, reviewRows: 1, distinctReadablePairs: 1, eligibleRows: 0 }, findings: [], opportunities: [direction('a'), direction('b')], prices: { products: 0, verifiedUnitProducts: 0, note: '无可比价格' }, notes: ['样本有限'] }; const output = innovationBrief(data, { filters: { category: 'banmian', brand: 'ksf' }, label: v => ({ banmian: '拌面', ksf: '康师傅' })[v] || v, selected: ['a'] }); for (const text of ['品类:拌面', '品牌:康师傅', 'g1', 'jd:1:r1', '调味控制a']) assert.ok(output.includes(text), text); for (const text of ['调味控制b', 'undefined', '请先', '员工', '保留风味', '验证对象', '查库要求']) assert.ok(!output.includes(text), text); }); function scopedData() { const support = { reviewId: 'jd:1:review-a', productId: 'jd:1', sourceRecordId: 'src-a', date: '2026-11-05 11:06:30', content: '此用户未填写评价内容', appendContent: '调味包全放太辣,味道不错。', analysisEligible: false, isDefault: null, identityState: 'unverified', associationIds: ['jd:1:review-a', 'jd:2:review-a'], hits: [] }; const direction = id => ({ id, title: '研究' + id, question: '待验证问题', status: '需要补证', reason: '2组线索,其中0组合格', supportUnits: 2, qualifiedUnits: 0, preserve: '保留风味', unknown: '实际指引', action: '比较原指引与分次投放', support: [support], evidenceIds: [support.reviewId, 'jd:3:review-b'], benefits: [], policyTerms: ['钠'] }); const all = [direction('seasoning'), direction('value')]; return { generation: 'g-fixed', rulesVersion: 'r1', focus: 'value', scope: { products: 10, reviewedProducts: 2, reviewRows: 20, distinctReadablePairs: 12, eligibleRows: 3 }, notes: ['样本有限'], opportunities: [all[1]], directionOptions: all, themeFacets: ['seasoning', 'value'].map(id => ({ id, name: id, units: 2, products: 1, qualifiedUnits: 0, pendingUnits: 2, types: { problem: 1, benefit: 1, adjustment: 0, wish: 0 } })), brands: [], claims: [], representativeProducts: [], prices: { products: 2, verifiedUnitProducts: 0, note: '规格未核,不能算单位价', groups: [] } }; } test('copy scope preserves explicitly selected directions across focus without adding unselected directions', () => { const d = scopedData(), before = structuredClone(d); assert.deepEqual(innovationMaterials(d, { selected: ['seasoning'] }).directions.map(x => x.id), ['seasoning']); assert.deepEqual(innovationMaterials(d, { mode: 'current', selected: ['seasoning'] }).directions.map(x => x.id), ['value']); assert.equal(innovationMaterials(d, { mode: 'all' }).directions.length, 2); assert.throws(() => innovationMaterials(d, { mode: 'selected' }), /MATERIAL_SELECTION_CHANGED/); assert.throws(() => innovationMaterials(d, { mode: 'selected', selected: ['stale'] }), /MATERIAL_SELECTION_CHANGED/); assert.deepEqual(d, before); }); test('方向资料保留完整ID和真实来源,关联记录不继承其他商品的来源', () => { const d = scopedData(); const before = structuredClone(d); const output = innovationBrief(d, { selected: ['seasoning'], filters: { category: 'banmian', platform: 'jd', shopId: 's1' } }); for (const text of ['g-fixed', '"shopId":"s1"', 'jd:1:review-a', 'jd:3:review-b', 'src-a', 'source_record_id', '研究seasoning']) assert.ok(output.includes(text), text); for (const text of ['研究value', 'ksf-voc-library', 'contract', 'cursor', '请按', '员工', '只读', '相反证据', 'undefined', '127.0.0.1']) assert.ok(!output.includes(text), text); const rows = output.split('\n').filter(line => line.startsWith('{')).map(JSON.parse); const raw = rows.find(row => row.id === 'jd:1:review-a'); assert.equal(raw.source_record_id, 'src-a'); assert.equal(raw.productId, 'jd:1'); assert.ok(!Object.hasOwn(raw, 'content')); assert.ok(!Object.hasOwn(rows.find(row => row.id === 'jd:2:review-a'), 'source_record_id')); assert.ok(!Object.hasOwn(rows.find(row => row.id === 'jd:2:review-a'), 'productId')); assert.equal(rows.filter(row => row.id === 'jd:1:review-a').length, 1); assert.deepEqual(d, before); }); test('visual figures use their own true denominators and never turn missing data into zero', () => { const figures = innovationFigures(scopedData().scope); assert.equal(figures[0].percent, '20.0'); assert.equal(figures[0].rest, 8); assert.equal(figures[1].percent, '15.0'); assert.equal(figures[1].total, 20); assert.equal(figures[1].rest, 17); assert.equal(innovationFigures({ products: 0, reviewedProducts: 0 })[0].percent, null); assert.equal(innovationFigures({ products: 10 })[0].percent, null); assert.equal(innovationFigures({ products: 1, reviewedProducts: 2 })[0].percent, null); });