opportunity.test.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. import { test } from 'node:test';
  2. import assert from 'node:assert/strict';
  3. import { scoreStats, weightedScore, normalizeScores } from '../src/core/opportunity/score.js';
  4. import { toTime } from '../src/core/kfs/util.js';
  5. import { SCORE_DIMS } from '../src/core/taxonomy/taxonomy.js';
  6. import {
  7. normalizeOpportunity, sortOpportunities, statusPatch, scorePatch, normUpload,
  8. } from '../src/core/opportunity/opportunity.js';
  9. test('多人评分:平均、同一人取最后一次、非法分丢弃', () => {
  10. const raw = [
  11. { user: '李娜', score: 86, at: '2026-10-28T01:00:00Z' },
  12. { user: '李娜', score: 90, at: '2026-10-28T02:00:00Z' }, // 重评 → 以 90 为准
  13. { user: '吴敏', score: 80, at: '2026-10-28T03:00:00Z' },
  14. { user: '评价 Agent', score: 70, at: '2026-10-28T04:00:00Z' },
  15. { user: '周涛', score: 120 }, // 越界
  16. { user: '', score: 88 }, // 没名字
  17. { user: '孙悦', score: '90' }, // 字符串
  18. ];
  19. const s = scoreStats(raw, toTime);
  20. assert.equal(s.n, 3);
  21. assert.equal(s.avg, 80); // (90+80+70)/3
  22. assert.equal(s.humanAvg, 85); // (90+80)/2
  23. assert.equal(s.agentAvg, 70);
  24. assert.equal(s.byUser['李娜'].score, 90);
  25. assert.equal(s.max, 90);
  26. });
  27. test('没有评分时 avg 为 null(页面显示「暂无评分」而不是 0)', () => {
  28. const s = scoreStats([], toTime);
  29. assert.equal(s.avg, null);
  30. assert.equal(s.n, 0);
  31. });
  32. test('Parse Date 对象格式的 at 也能参与时间排序', () => {
  33. const list = normalizeScores([
  34. { user: 'a', score: 1, at: { __type: 'Date', iso: '2026-01-02T00:00:00Z' } },
  35. { user: 'b', score: 2, at: '2026-01-01T00:00:00Z' },
  36. ], toTime);
  37. assert.deepEqual(list.map((x) => x.user), ['b', 'a']);
  38. });
  39. test('五维加权综合分', () => {
  40. assert.equal(weightedScore({ demand: 10, competition: 10, feasibility: 10, fit: 10, scale: 10 }, SCORE_DIMS), 100);
  41. assert.equal(weightedScore({ demand: 5, competition: 5, feasibility: 5, fit: 5, scale: 5 }, SCORE_DIMS), 50);
  42. assert.equal(weightedScore(null, SCORE_DIMS), null);
  43. });
  44. const mk = (o) => ({
  45. objectId: o.id,
  46. title: '', oneLiner: '', category: '拌面', priority: 'S', status: 'candidate', evidenceLevel: 'high',
  47. stage: 'S3 机会识别', scores: [], updatedAt: '2026-10-29T00:00:00Z', createdAt: '2026-10-29T00:00:00Z',
  48. ...o,
  49. });
  50. test('上传四要素:缺哪个就标哪个,不拿 createdAt 冒充 uploadedAt', () => {
  51. const u = normUpload({ uploadedBy: '张伟', agentName: '', uploadMethod: 'session', uploadedAt: null });
  52. assert.equal(u.complete, false);
  53. assert.deepEqual(u.missing, ['Agent', '时间']);
  54. assert.equal(u.uploadedAt, null);
  55. // 历史上 uploadMethod 被错写成 uploadedBy=skill 的记录
  56. const fix = normUpload({ uploadedBy: 'skill', uploadedByKey: 'sess-01', agentName: 'A', uploadedAt: '2026-10-01T00:00:00Z' }, { by: '张伟' });
  57. assert.equal(fix.uploadedBy, '张伟');
  58. assert.equal(fix.uploadMethod, 'skill');
  59. });
  60. test('排序:优先级与评分切换后顺序真实变化', () => {
  61. const rows = [
  62. normalizeOpportunity(mk({ id: 'a', title: 'A', priority: 'B', scores: [{ user: 'x', score: 95 }] })),
  63. normalizeOpportunity(mk({ id: 'b', title: 'B', priority: 'S', scores: [{ user: 'x', score: 60 }] })),
  64. normalizeOpportunity(mk({ id: 'c', title: 'C', priority: 'A', scores: [{ user: 'x', score: 40 }], createdAt: '2026-10-29T00:00:01Z' })),
  65. normalizeOpportunity(mk({ id: 'd', title: 'D', priority: 'A', scores: [] })),
  66. ];
  67. assert.deepEqual(sortOpportunities(rows, 'priority').map((r) => r.id), ['b', 'c', 'd', 'a'], 'A 级里按分:C 40 > D 无评分');
  68. assert.deepEqual(sortOpportunities(rows, 'score').map((r) => r.id), ['a', 'b', 'c', 'd'], '按平均分:A 95 > B 60 > C 40 > D(无评分)沉底');
  69. assert.deepEqual(sortOpportunities(rows, 'priority').map((r) => r.id), ['b', 'c', 'd', 'a'], '排序不改原数组');
  70. });
  71. test('改状态:产出 patch 与一条决策;同状态不改', () => {
  72. const opp = normalizeOpportunity(mk({ id: 'a', status: 'candidate' }));
  73. const r = statusPatch(opp, 'approved', { by: '刘洋', reason: '验证通过', at: '2026-10-29T08:00:00Z' });
  74. assert.equal(r.changed, true);
  75. assert.equal(r.patch.status, 'approved');
  76. assert.equal(r.patch.decision.type, 'approve');
  77. assert.equal(r.patch.decision.by, '刘洋');
  78. assert.equal(r.patch.decisionLog.__op, 'AddUnique');
  79. assert.equal(r.patch.decisionLog.objects[0].reason, '验证通过');
  80. assert.equal(statusPatch(opp, 'candidate', { by: 'x' }).changed, false);
  81. assert.throws(() => statusPatch(opp, 'approved', { by: '' }), /是谁/);
  82. assert.throws(() => statusPatch(opp, 'bogus', { by: 'x' }), /未知状态/);
  83. });
  84. test('追加评分:同一人重评替换旧分', () => {
  85. const opp = normalizeOpportunity(mk({ id: 'a', scores: [{ user: '李娜', score: 70 }, { user: '吴敏', score: 80 }] }));
  86. const r = scorePatch(opp, { user: '李娜', score: 92 });
  87. assert.equal(r.scores.length, 2);
  88. assert.equal(r.scores.find((s) => s.user === '李娜').score, 92);
  89. assert.throws(() => scorePatch(opp, { user: 'x', score: 101 }), /0–100/);
  90. });