report.test.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. import { test } from 'node:test';
  2. import assert from 'node:assert/strict';
  3. import { pipelineSummary, reportOverview, findingsOf, chapters } from '../src/core/report.js';
  4. import { liveBoard, forMe, stuckMembers } from '../src/core/kfs/stats.js';
  5. import { buildModel } from '../src/core/kfs/build.js';
  6. import { reportEvidence, reportConclusions, conclusionToLedger, traceInsight } from '../src/core/evidence/chain.js';
  7. import { latest, fmtAgo, fmtDate, toTime, truncate, clamp } from '../src/core/kfs/util.js';
  8. import { normDecision, cardSummary } from '../src/core/opportunity/opportunity.js';
  9. const raw = {
  10. artifact: [
  11. { objectId: 'r', title: '报告 - competitor_report', ownerName: '张伟', stage: 'S1品类分析',
  12. report: {
  13. title: '测试报告', subtitle: '副标题', pipeline: [{ label: '采集', value: 2727 }, { label: '去噪', value: 1190 }, { label: '精准', value: 730 }],
  14. chapters: [
  15. { id: 'ch1', no: 1, title: '一', stage: 'S1', lead: '问题一', sections: [{ kind: 'para', text: 'x' }] },
  16. { id: 'ch2', no: 2, title: '二', stage: 'S2', lead: '问题二', sections: [{ kind: 'findings', data: [{ conclusion: 'A', evidence: 'e1' }, { conclusion: 'A', evidence: 'e1' }] }] },
  17. { id: 'ch3', no: 3, title: '三', stage: 'S3', sections: [{ kind: 'mystery', data: { a: 1 } }] },
  18. ],
  19. evidence: { e1: { claim: 'A', ownerCount: 3, quotes: [{ text: 'q' }], derivedFrom: ['a1'] } },
  20. } },
  21. { objectId: 'a1', title: '成果一', ownerName: '李娜', stage: 'S2需求洞察' },
  22. ],
  23. insight: [{ objectId: 'i1', title: '归纳', derivedArtifacts: ['a1'] }],
  24. session: [],
  25. project: [],
  26. source: [],
  27. opportunity: [],
  28. };
  29. const db = buildModel(raw);
  30. test('数据管线:首尾推算过滤率', () => {
  31. const p = pipelineSummary({ pipeline: [{ label: 'a', value: 2727 }, { label: 'b', value: 1190 }, { label: 'c', value: 730 }] });
  32. assert.equal(p.filterRate, 73);
  33. assert.equal(p.steps.length, 3);
  34. assert.equal(pipelineSummary({}), null);
  35. });
  36. test('报告概览:章数 / 结论 / 引用 / 原文 / 跨阶段', () => {
  37. const r = reportOverview(db, 'r');
  38. assert.equal(r.chapterCount, 3);
  39. assert.equal(r.conclusionCount, 1);
  40. assert.equal(r.refsInLib, 1);
  41. assert.equal(r.quoteCount, 1);
  42. assert.deepEqual(r.stages, ['S1', 'S2', 'S3']);
  43. assert.equal(r.pipeline.filterRate, 73);
  44. assert.equal(reportOverview(db, 'a1'), null, '没有 report 的成果不进报告目录');
  45. });
  46. test('结论块去重(同一证据锚点只出现一次)', () => {
  47. const f = findingsOf(db, 'r');
  48. assert.equal(f.length, 1);
  49. assert.equal(f[0].ownerCount, 3);
  50. });
  51. test('章节归一:缺 id 用序号补齐', () => {
  52. const c = chapters({ chapters: [{ title: 'x' }, { id: 'y', title: 'z' }] });
  53. assert.equal(c[0].id, 'ch1');
  54. assert.equal(c[1].no, 2);
  55. });
  56. test('证据锚点 → 台账行 id(含不存在的成果也如实返回)', () => {
  57. db.artifactById.ghost = undefined;
  58. assert.deepEqual(conclusionToLedger(db, 'r', 'e1'), ['a1']);
  59. assert.equal(reportEvidence(db, 'r', 'nope'), null);
  60. assert.deepEqual(reportConclusions(db, 'r').map((c) => c.id), ['e1']);
  61. });
  62. test('归纳 → 来自的报告', () => {
  63. // 现实中归纳引用的成果是带报告的成果本身(reportArtifact),不是旁证;用这种形态验证
  64. const db2 = buildModel({ insight: [{ objectId: 'i2', title: '归纳2', derivedArtifacts: ['r'] }], artifact: raw.artifact });
  65. const t = traceInsight(db2, 'i2');
  66. assert.deepEqual(t.reports.map((r) => r.id), ['r']);
  67. assert.equal(t.reports[0].title, '测试报告');
  68. });
  69. test('时间工具:Parse 日期 / ISO / 毫秒都能读,空值不炸', () => {
  70. assert.equal(toTime({ __type: 'Date', iso: '2026-10-29T00:00:00Z' }), Date.parse('2026-10-29T00:00:00Z'));
  71. assert.equal(toTime(null), 0);
  72. assert.equal(toTime('坏数据'), 0);
  73. assert.equal(latest([null, '2026-01-01T00:00:00Z', '2025-01-01T00:00:00Z']), '2026-01-01T00:00:00.000Z');
  74. assert.equal(fmtDate(null), '—');
  75. assert.equal(fmtAgo(new Date(Date.now() - 3 * 3600000).toISOString()), '3\u2009小时前');
  76. assert.equal(truncate('一二三四五', 3), '一二三…');
  77. assert.equal(clamp(120, 0, 100), 100);
  78. });
  79. test('决策归一:未知类型保留 label,缺 label 给「决策」', () => {
  80. assert.equal(normDecision({ type: 'weird', label: '暂缓(补原文)' }).label, '暂缓(补原文)');
  81. assert.equal(normDecision({ type: 'weird' }).label, '决策');
  82. assert.equal(normDecision(null), null);
  83. });
  84. test('卡片摘要不含内部大对象', () => {
  85. const o = db.opportunities[0] || buildModel({ opportunity: [{ objectId: 'o', title: 'T', oneLiner: 'x'.repeat(200) }] }).opportunities[0];
  86. const s = cardSummary(o, 10);
  87. assert.equal(s.title, 'T');
  88. assert.equal(Object.keys(s).includes('linkedArtifacts'), false);
  89. });
  90. test('现场看板:没有任何会话时不崩', () => {
  91. const b = liveBoard(db);
  92. assert.equal(b.stages.length, 5);
  93. assert.deepEqual(stuckMembers(db), []);
  94. assert.equal(forMe(db, '').count, 0);
  95. });