| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130 |
- 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);
- });
|