import { test } from 'node:test'; import assert from 'node:assert/strict'; import { pipelineSummary, reportOverview, findingsOf, chapters } from '../src/core/report.js'; import { liveBoard, forMe, stuckMembers } from '../src/core/kfs/stats.js'; import { buildModel } from '../src/core/kfs/build.js'; import { reportEvidence, reportConclusions, conclusionToLedger, traceInsight } from '../src/core/evidence/chain.js'; import { latest, fmtAgo, fmtDate, toTime, truncate, clamp } from '../src/core/kfs/util.js'; import { normDecision, cardSummary } from '../src/core/opportunity/opportunity.js'; const raw = { artifact: [ { objectId: 'r', title: '报告 - competitor_report', ownerName: '张伟', stage: 'S1品类分析', report: { title: '测试报告', subtitle: '副标题', pipeline: [{ label: '采集', value: 2727 }, { label: '去噪', value: 1190 }, { label: '精准', value: 730 }], chapters: [ { id: 'ch1', no: 1, title: '一', stage: 'S1', lead: '问题一', sections: [{ kind: 'para', text: 'x' }] }, { id: 'ch2', no: 2, title: '二', stage: 'S2', lead: '问题二', sections: [{ kind: 'findings', data: [{ conclusion: 'A', evidence: 'e1' }, { conclusion: 'A', evidence: 'e1' }] }] }, { id: 'ch3', no: 3, title: '三', stage: 'S3', sections: [{ kind: 'mystery', data: { a: 1 } }] }, ], evidence: { e1: { claim: 'A', ownerCount: 3, quotes: [{ text: 'q' }], derivedFrom: ['a1'] } }, } }, { objectId: 'a1', title: '成果一', ownerName: '李娜', stage: 'S2需求洞察' }, ], insight: [{ objectId: 'i1', title: '归纳', derivedArtifacts: ['a1'] }], session: [], project: [], source: [], opportunity: [], }; const db = buildModel(raw); test('数据管线:首尾推算过滤率', () => { const p = pipelineSummary({ pipeline: [{ label: 'a', value: 2727 }, { label: 'b', value: 1190 }, { label: 'c', value: 730 }] }); assert.equal(p.filterRate, 73); assert.equal(p.steps.length, 3); assert.equal(pipelineSummary({}), null); }); test('报告概览:章数 / 结论 / 引用 / 原文 / 跨阶段', () => { const r = reportOverview(db, 'r'); assert.equal(r.chapterCount, 3); assert.equal(r.conclusionCount, 1); assert.equal(r.refsInLib, 1); assert.equal(r.quoteCount, 1); assert.deepEqual(r.stages, ['S1', 'S2', 'S3']); assert.equal(r.pipeline.filterRate, 73); assert.equal(reportOverview(db, 'a1'), null, '没有 report 的成果不进报告目录'); }); test('结论块去重(同一证据锚点只出现一次)', () => { const f = findingsOf(db, 'r'); assert.equal(f.length, 1); assert.equal(f[0].ownerCount, 3); }); test('表格和段落小结的全部逐字引文都有证据入口,来源关系保留', () => { const report = { chapters: [{ title: '资料小结', sections: [{ kind: 'para', text: '资料范围' }, { kind: 'table', data: { cols: ['产品'], rows: [['拌面']] } }] }], evidence: { competitor: { sourceId: 'source-competitor', claim: '官网口味说明', quotes: [{ text: '官网原文' }] }, own: { sourceId: 'source-own', claim: '2022年品牌通告', quotes: [{ text: '品牌升级' }, { text: '辣味选择' }, { text: '滤水口' }] }, }, }; const model = buildModel({ artifact: [{ objectId: 'summary', report }], source: [] }); const entries = findingsOf(model, 'summary'); assert.deepEqual(entries.map(e => [e.evId, e.quoteCount]), [['competitor', 1], ['own', 3]]); assert.ok(entries.every(e => e.ownerCount === null), '品牌文案不填消费者人数'); const own = reportEvidence(model, 'summary', 'own'); assert.ok(own.quotes.every(q => q.sourceId === 'source-own' && q.art === null)); assert.equal(reportOverview(model, 'summary').quoteCount, 4); report.chapters[0].sections.push({ kind: 'findings', data: [{ conclusion: '原有判断', evidence: 'own' }] }); const mixed = findingsOf(model, 'summary'); assert.deepEqual(mixed.map(e => e.evId), ['own', 'competitor']); assert.equal(mixed[0].conclusion, '原有判断'); assert.equal(mixed[1].conclusion, '官网口味说明'); const fallback = reportEvidence(buildModel({ artifact: [{ objectId: 'empty', report: { evidence: { x: { quotes: [{ text: '无来源' }] } } } }] }), 'empty', 'x'); assert.equal(fallback.quotes[0].sourceId, null); }); test('章节归一:缺 id 用序号补齐', () => { const c = chapters({ chapters: [{ title: 'x' }, { id: 'y', title: 'z' }] }); assert.equal(c[0].id, 'ch1'); assert.equal(c[1].no, 2); }); test('证据锚点 → 台账行 id(含不存在的成果也如实返回)', () => { db.artifactById.ghost = undefined; assert.deepEqual(conclusionToLedger(db, 'r', 'e1'), ['a1']); assert.equal(reportEvidence(db, 'r', 'nope'), null); assert.deepEqual(reportConclusions(db, 'r').map((c) => c.id), ['e1']); }); test('归纳 → 来自的报告', () => { // 现实中归纳引用的成果是带报告的成果本身(reportArtifact),不是旁证;用这种形态验证 const db2 = buildModel({ insight: [{ objectId: 'i2', title: '归纳2', derivedArtifacts: ['r'] }], artifact: raw.artifact }); const t = traceInsight(db2, 'i2'); assert.deepEqual(t.reports.map((r) => r.id), ['r']); assert.equal(t.reports[0].title, '测试报告'); }); test('时间工具:Parse 日期 / ISO / 毫秒都能读,空值不炸', () => { assert.equal(toTime({ __type: 'Date', iso: '2026-10-29T00:00:00Z' }), Date.parse('2026-10-29T00:00:00Z')); assert.equal(toTime(null), 0); assert.equal(toTime('坏数据'), 0); assert.equal(latest([null, '2026-01-01T00:00:00Z', '2025-01-01T00:00:00Z']), '2026-01-01T00:00:00.000Z'); assert.equal(fmtDate(null), '—'); assert.equal(fmtAgo(new Date(Date.now() - 3 * 3600000).toISOString()), '3\u2009小时前'); assert.equal(truncate('一二三四五', 3), '一二三…'); assert.equal(clamp(120, 0, 100), 100); }); test('决策归一:未知类型保留 label,缺 label 给「决策」', () => { assert.equal(normDecision({ type: 'weird', label: '暂缓(补原文)' }).label, '暂缓(补原文)'); assert.equal(normDecision({ type: 'weird' }).label, '决策'); assert.equal(normDecision(null), null); }); test('卡片摘要不含内部大对象', () => { const o = db.opportunities[0] || buildModel({ opportunity: [{ objectId: 'o', title: 'T', oneLiner: 'x'.repeat(200) }] }).opportunities[0]; const s = cardSummary(o, 10); assert.equal(s.title, 'T'); assert.equal(Object.keys(s).includes('linkedArtifacts'), false); }); test('现场看板:没有任何会话时不崩', () => { const b = liveBoard(db); assert.equal(b.stages.length, 5); assert.deepEqual(stuckMembers(db), []); assert.equal(forMe(db, '').count, 0); });