import { test } from 'node:test'; import assert from 'node:assert/strict'; import { scoreStats, weightedScore, normalizeScores } from '../src/core/opportunity/score.js'; import { toTime } from '../src/core/kfs/util.js'; import { SCORE_DIMS } from '../src/core/taxonomy/taxonomy.js'; import { normalizeOpportunity, sortOpportunities, statusPatch, scorePatch, normUpload, } from '../src/core/opportunity/opportunity.js'; test('多人评分:平均、同一人取最后一次、非法分丢弃', () => { const raw = [ { user: '李娜', score: 86, at: '2026-10-28T01:00:00Z' }, { user: '李娜', score: 90, at: '2026-10-28T02:00:00Z' }, // 重评 → 以 90 为准 { user: '吴敏', score: 80, at: '2026-10-28T03:00:00Z' }, { user: '评价 Agent', score: 70, at: '2026-10-28T04:00:00Z' }, { user: '周涛', score: 120 }, // 越界 { user: '', score: 88 }, // 没名字 { user: '孙悦', score: '90' }, // 字符串 ]; const s = scoreStats(raw, toTime); assert.equal(s.n, 3); assert.equal(s.avg, 80); // (90+80+70)/3 assert.equal(s.humanAvg, 85); // (90+80)/2 assert.equal(s.agentAvg, 70); assert.equal(s.byUser['李娜'].score, 90); assert.equal(s.max, 90); }); test('没有评分时 avg 为 null(页面显示「暂无评分」而不是 0)', () => { const s = scoreStats([], toTime); assert.equal(s.avg, null); assert.equal(s.n, 0); }); test('Parse Date 对象格式的 at 也能参与时间排序', () => { const list = normalizeScores([ { user: 'a', score: 1, at: { __type: 'Date', iso: '2026-01-02T00:00:00Z' } }, { user: 'b', score: 2, at: '2026-01-01T00:00:00Z' }, ], toTime); assert.deepEqual(list.map((x) => x.user), ['b', 'a']); }); test('五维加权综合分', () => { assert.equal(weightedScore({ demand: 10, competition: 10, feasibility: 10, fit: 10, scale: 10 }, SCORE_DIMS), 100); assert.equal(weightedScore({ demand: 5, competition: 5, feasibility: 5, fit: 5, scale: 5 }, SCORE_DIMS), 50); assert.equal(weightedScore(null, SCORE_DIMS), null); }); const mk = (o) => ({ objectId: o.id, title: '', oneLiner: '', category: '拌面', priority: 'S', status: 'candidate', evidenceLevel: 'high', stage: 'S3 机会识别', scores: [], updatedAt: '2026-10-29T00:00:00Z', createdAt: '2026-10-29T00:00:00Z', ...o, }); test('上传四要素:缺哪个就标哪个,不拿 createdAt 冒充 uploadedAt', () => { const u = normUpload({ uploadedBy: '张伟', agentName: '', uploadMethod: 'session', uploadedAt: null }); assert.equal(u.complete, false); assert.deepEqual(u.missing, ['Agent', '时间']); assert.equal(u.uploadedAt, null); // 历史上 uploadMethod 被错写成 uploadedBy=skill 的记录 const fix = normUpload({ uploadedBy: 'skill', uploadedByKey: 'sess-01', agentName: 'A', uploadedAt: '2026-10-01T00:00:00Z' }, { by: '张伟' }); assert.equal(fix.uploadedBy, '张伟'); assert.equal(fix.uploadMethod, 'skill'); }); test('排序:优先级与评分切换后顺序真实变化', () => { const rows = [ normalizeOpportunity(mk({ id: 'a', title: 'A', priority: 'B', scores: [{ user: 'x', score: 95 }] })), normalizeOpportunity(mk({ id: 'b', title: 'B', priority: 'S', scores: [{ user: 'x', score: 60 }] })), normalizeOpportunity(mk({ id: 'c', title: 'C', priority: 'A', scores: [{ user: 'x', score: 40 }], createdAt: '2026-10-29T00:00:01Z' })), normalizeOpportunity(mk({ id: 'd', title: 'D', priority: 'A', scores: [] })), ]; assert.deepEqual(sortOpportunities(rows, 'priority').map((r) => r.id), ['b', 'c', 'd', 'a'], 'A 级里按分:C 40 > D 无评分'); assert.deepEqual(sortOpportunities(rows, 'score').map((r) => r.id), ['a', 'b', 'c', 'd'], '按平均分:A 95 > B 60 > C 40 > D(无评分)沉底'); assert.deepEqual(sortOpportunities(rows, 'priority').map((r) => r.id), ['b', 'c', 'd', 'a'], '排序不改原数组'); }); test('改状态:产出 patch 与一条决策;同状态不改', () => { const opp = normalizeOpportunity(mk({ id: 'a', status: 'candidate' })); const r = statusPatch(opp, 'approved', { by: '刘洋', reason: '验证通过', at: '2026-10-29T08:00:00Z' }); assert.equal(r.changed, true); assert.equal(r.patch.status, 'approved'); assert.equal(r.patch.decision.type, 'approve'); assert.equal(r.patch.decision.by, '刘洋'); assert.equal(r.patch.decisionLog.__op, 'AddUnique'); assert.equal(r.patch.decisionLog.objects[0].reason, '验证通过'); assert.equal(statusPatch(opp, 'candidate', { by: 'x' }).changed, false); assert.throws(() => statusPatch(opp, 'approved', { by: '' }), /是谁/); assert.throws(() => statusPatch(opp, 'bogus', { by: 'x' }), /未知状态/); }); test('追加评分:同一人重评替换旧分', () => { const opp = normalizeOpportunity(mk({ id: 'a', scores: [{ user: '李娜', score: 70 }, { user: '吴敏', score: 80 }] })); const r = scorePatch(opp, { user: '李娜', score: 92 }); assert.equal(r.scores.length, 2); assert.equal(r.scores.find((s) => s.user === '李娜').score, 92); assert.throws(() => scorePatch(opp, { user: 'x', score: 101 }), /0–100/); });