| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117 |
- import { test } from 'node:test';
- import assert from 'node:assert/strict';
- import { buildModel } from '../src/core/kfs/build.js';
- import { forMe, poolOverview, liveBoard, stuckMembers, stageBoards } from '../src/core/kfs/stats.js';
- import { traceArtifact, traceInsight, reportEvidence, conclusionToLedger } from '../src/core/evidence/chain.js';
- import { reportOverview, findingsOf } from '../src/core/report.js';
- /* 一份最小但覆盖各字段形态的「库」:只在测试里构造,不联网 */
- const NOW = Date.parse('2026-10-29T12:00:00Z');
- const H = 3600000;
- const ago = (h) => new Date(NOW - h * H).toISOString();
- const raw = {
- project: [{ objectId: 'p1', title: '夜宵项目', ownerName: '张伟', ownerSession: 's1', stage: 'S1品类分析' }],
- artifact: [
- { objectId: 'a1', title: '夜宵卡 - opportunity_card', ownerName: '张伟', ownerSession: 's1', stage: 'S3 机会识别', evidenceLevel: 'supported', artifactType: 'opportunity_card', createdAt: ago(10), uploadMethod: 'session', agentName: '主分身', agentId: 'agent-1', uploadedAt: ago(10) },
- { objectId: 'a2', title: '夜宵痛点 - pain_list', ownerName: '李娜', ownerSession: 's2', stage: 'S2需求洞察', evidenceLevel: 'verified', sources: [], createdAt: ago(9) },
- { objectId: 'rep', title: '夜宵竞品 - competitor_report', ownerName: '张伟', ownerSession: 's1', stage: 'S1品类分析', evidenceLevel: 'supported', createdAt: ago(8),
- report: {
- title: '夜宵洞察', pipeline: [{ label: '采集', value: 2727 }, { label: '去噪', value: 1190 }, { label: '精准', value: 730 }],
- chapters: [{ id: 'ch2', no: 2, title: '嵌套洞察', stage: 'S2', sections: [
- { kind: 'findings', data: [{ id: 'f', conclusion: '软塌是工艺问题', attitude: '…', evidence: 'c1' }] },
- { kind: 'findings', data: [{ id: 'f', conclusion: '软塌是工艺问题', attitude: '…', evidence: 'c1' }] },
- ] }],
- evidence: { c1: { claim: '软塌是工艺问题', ownerCount: 2, quotes: [{ text: '泡完就坨', ref: '电商评论', likes: 302 }], derivedFrom: ['a2', 'ghost'] } },
- } },
- ],
- insight: [
- { objectId: 'i1', title: '拌面口感', derivedFrom: ['a2'], derivedArtifacts: ['a1'] },
- { objectId: 'i2', title: '无出身推断', derivedFrom: [], layer: 'sediment' },
- ],
- source: [{ objectId: 'src1', title: '品类概览', sourceType: 'category' }],
- session: [
- { objectId: 's1', studentName: '张伟', stage: 'S5概念设计', msgCount: 20, lastActiveAt: ago(20) },
- { objectId: 's3', studentName: '何雨', stage: 'S4验证评估', msgCount: 13, lastActiveAt: ago(30) }, // 开工了、没交、超时 → 卡住
- { objectId: 's4', studentName: '新人', stage: 'S1品类分析', msgCount: 2, lastActiveAt: ago(30) }, // 消息太少 → 不算卡住
- { objectId: 's5', studentName: '老数据', stage: 'S2需求洞察', msgCount: 9, createdAt: ago(40) }, // 没有 lastActiveAt
- ],
- opportunity: [
- { objectId: 'o1', title: '夜宵低卡', stage: 'S3 机会识别', priority: 'S', status: 'candidate', evidenceLevel: 'high', linkedArtifacts: ['a1'], linkedProject: 'p1',
- scores: [{ user: '李娜', score: 86, at: ago(3) }, { user: '吴敏', score: 90, at: ago(2) }], uploadedBy: '张伟', uploadedByKey: 's1', agentName: '主分身', uploadMethod: 'session', uploadedAt: ago(6) },
- { objectId: 'o2', title: '银发低钠', stage: 'S1 品类市场分析', priority: 'B', status: 'approved', evidenceLevel: 'medium', linkedArtifacts: ['ghost'], scores: [],
- uploadedBy: '赵磊', agentName: '调研 Agent', uploadMethod: 'skill', uploadedAt: ago(1) },
- ],
- };
- const db = buildModel(raw, { now: NOW });
- test('模型:成果 / 成员 / 机会卡计数与归属', () => {
- assert.equal(db.artifacts.length, 3);
- assert.equal(db.members.length, 6, '张伟/李娜/何雨/新人/老数据 + 机会卡上传人赵磊');
- assert.equal(db.artifactById.a1.shortTitle, '夜宵卡');
- assert.equal(db.artifactById.a2.evidenceLabel, '有佐证', 'verified 但无原文 → 降级');
- assert.equal(db.artifactById.a1.layer, 'sediment', '被归纳引用且有出身 → 沉淀');
- assert.equal(db.insightById.i2.layer, 'record', '没有出身的归纳只能留档');
- assert.deepEqual(db.artifactById.a2.citedByInsightIds, ['i1']);
- });
- test('视角一「给我的」:交了几份、哪个阶段没交', () => {
- const m = forMe(db, '张伟');
- assert.equal(m.count, 2);
- assert.deepEqual(m.stagesDone, ['S1', 'S3']);
- assert.deepEqual(m.stagesMissing, ['S2', 'S4', 'S5']);
- assert.equal(m.opps.length, 1);
- });
- test('视角二「池子」:总数 / 状态分布 / 最新 / 最高分', () => {
- const p = poolOverview(db);
- assert.equal(p.total, 2);
- assert.deepEqual(p.byStatus, { candidate: 1, approved: 1, rejected: 0 });
- assert.equal(p.latest3[0].id, 'o2', '最新上传在前');
- assert.equal(p.top.id, 'o1');
- assert.equal(p.top.stats.avg, 88);
- });
- test('卡住:只列开工了、没交、超时的人名', () => {
- const s = stuckMembers(db);
- assert.deepEqual(s.map((x) => x.name), ['老数据', '何雨'], '老数据没有 lastActiveAt,按会话 createdAt(40h) 算,也确实超时');
- assert.equal(s[1].stage, 'S4\u2009机会验证评估');
- assert.equal(s[1].stuckHours, 30);
- });
- test('视角三「现场看板」:五阶段 × 人', () => {
- const b = liveBoard(db);
- assert.equal(b.stages.length, 5);
- const s3 = b.stages.find((x) => x.stage.code === 'S3');
- assert.equal(s3.submitted, 1);
- assert.equal(s3.people.find((p) => p.name === '张伟').count, 1);
- assert.equal(b.stalled.length, 2);
- });
- test('五阶段板块:产出数 + 最新 + 谁产的', () => {
- const b = stageBoards(db);
- const s2 = b.find((x) => x.stage.code === 'S2');
- assert.equal(s2.count, 1);
- assert.deepEqual(s2.producers, [{ name: '李娜', count: 1 }]);
- assert.equal(b.find((x) => x.stage.code === 'S3').oppCount, 1, '机会卡按阶段单独算,不混进成果数');
- });
- test('证据链:结论 → 具体成果名和人名(不存在的如实标出)', () => {
- const e = reportEvidence(db, 'rep', 'c1');
- assert.equal(e.claim, '软塌是工艺问题');
- assert.deepEqual(e.derived.map((d) => d.name), ['夜宵痛点', '(库里已不存在)']);
- assert.deepEqual(e.derivedOwners, ['李娜']);
- assert.deepEqual(conclusionToLedger(db, 'rep', 'c1'), ['a2', 'ghost']);
- assert.equal(e.quotes[0].likes, 302);
- });
- test('证据链:成果 → 引用它的结论 / 归纳 / 机会卡', () => {
- const t = traceArtifact(db, 'a2');
- assert.deepEqual([...new Set(t.citing.map((c) => `${c.reportTitle}/${c.evId}`))], ['夜宵竞品/c1']);
- assert.deepEqual(t.insights.map((i) => i.title), ['拌面口感']);
- const t1 = traceArtifact(db, 'a1');
- assert.deepEqual(t1.opportunities.map((o) => o.title), ['夜宵低卡']);
- const ti = traceInsight(db, 'i1');
- assert.equal(ti.reports.length, 0, 'i1 只引用了不带报告正文的成果,如实为空');
- assert.deepEqual(traceInsight(db, 'i1').artifacts.map((a) => a.name), ['夜宵卡']);
- });
|