industry-brief.test.js 5.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
  1. import { test } from 'node:test';
  2. import assert from 'node:assert/strict';
  3. import { innovationBrief } from '../src/core/industry-brief.js';
  4. import { innovationFigures, innovationMaterials } from '../src/core/industry-materials.js';
  5. test('方向资料只包含选中的方向及数据库定位信息', () => {
  6. const evidence = { reviewId: 'jd:1:r1', content: '此用户未填写评价内容', appendContent: '全包太辣', analysisEligible: false, hits: [] };
  7. const direction = id => ({ id, title: '调味控制' + id, status: '需要补证', reason: '1组线索', question: '怎么改善', preserve: '保留风味', action: '对照指引', unknown: '现有说明待核', support: [evidence], benefits: [] });
  8. 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: ['样本有限'] };
  9. const output = innovationBrief(data, { filters: { category: 'banmian', brand: 'ksf' }, label: v => ({ banmian: '拌面', ksf: '康师傅' })[v] || v, selected: ['a'] });
  10. for (const text of ['品类:拌面', '品牌:康师傅', 'g1', 'jd:1:r1', '调味控制a']) assert.ok(output.includes(text), text);
  11. for (const text of ['调味控制b', 'undefined', '请先', '员工', '保留风味', '验证对象', '查库要求']) assert.ok(!output.includes(text), text);
  12. });
  13. function scopedData() {
  14. 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: [] };
  15. 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: ['钠'] });
  16. const all = [direction('seasoning'), direction('value')];
  17. 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: [] } };
  18. }
  19. test('copy scope preserves explicitly selected directions across focus without adding unselected directions', () => {
  20. const d = scopedData(), before = structuredClone(d);
  21. assert.deepEqual(innovationMaterials(d, { selected: ['seasoning'] }).directions.map(x => x.id), ['seasoning']);
  22. assert.deepEqual(innovationMaterials(d, { mode: 'current', selected: ['seasoning'] }).directions.map(x => x.id), ['value']);
  23. assert.equal(innovationMaterials(d, { mode: 'all' }).directions.length, 2);
  24. assert.throws(() => innovationMaterials(d, { mode: 'selected' }), /MATERIAL_SELECTION_CHANGED/);
  25. assert.throws(() => innovationMaterials(d, { mode: 'selected', selected: ['stale'] }), /MATERIAL_SELECTION_CHANGED/);
  26. assert.deepEqual(d, before);
  27. });
  28. test('方向资料保留完整ID和真实来源,关联记录不继承其他商品的来源', () => {
  29. const d = scopedData();
  30. const before = structuredClone(d);
  31. const output = innovationBrief(d, { selected: ['seasoning'], filters: { category: 'banmian', platform: 'jd', shopId: 's1' } });
  32. 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);
  33. for (const text of ['研究value', 'ksf-voc-library', 'contract', 'cursor', '请按', '员工', '只读', '相反证据', 'undefined', '127.0.0.1']) assert.ok(!output.includes(text), text);
  34. const rows = output.split('\n').filter(line => line.startsWith('{')).map(JSON.parse);
  35. const raw = rows.find(row => row.id === 'jd:1:review-a');
  36. assert.equal(raw.source_record_id, 'src-a');
  37. assert.equal(raw.productId, 'jd:1');
  38. assert.ok(!Object.hasOwn(raw, 'content'));
  39. assert.ok(!Object.hasOwn(rows.find(row => row.id === 'jd:2:review-a'), 'source_record_id'));
  40. assert.ok(!Object.hasOwn(rows.find(row => row.id === 'jd:2:review-a'), 'productId'));
  41. assert.equal(rows.filter(row => row.id === 'jd:1:review-a').length, 1);
  42. assert.deepEqual(d, before);
  43. });
  44. test('visual figures use their own true denominators and never turn missing data into zero', () => {
  45. const figures = innovationFigures(scopedData().scope);
  46. assert.equal(figures[0].percent, '20.0');
  47. assert.equal(figures[0].rest, 8);
  48. assert.equal(figures[1].percent, '15.0');
  49. assert.equal(figures[1].total, 20);
  50. assert.equal(figures[1].rest, 17);
  51. assert.equal(innovationFigures({ products: 0, reviewedProducts: 0 })[0].percent, null);
  52. assert.equal(innovationFigures({ products: 10 })[0].percent, null);
  53. assert.equal(innovationFigures({ products: 1, reviewedProducts: 2 })[0].percent, null);
  54. });