devperf/backend/tests/unit/roi-engine.test.ts
zyc 5af612e3fd feat(roi): ROI 动态规则引擎 v1 + 业务体系归属
后端:
- 事件流模型(project_cost_events / project_revenue_events)+ launchedAt 截断
- 3 大业务体系归属(airhubs/airflow/aircore) + 项目类型(hw/sw) + identifier 自动生成
- AI 三件套推荐(category + bizSystem + projectType)
- 营收 mock API + 外部对接规范 + 资产摊销 cron
- 5 个 migration(0003 ROI 引擎 / 0004 driver factors / 0005 biz system)
- 单测 11/11 过

前端:
- 项目级 ROI 看板:4 KPI 卡片 + 折线图(周/月/年)+ 成本/产出事件流并排
- 全公司决策罗盘:3 大 ROI 指标 + 业务线堆叠 + 分类筛选 chip
- 项目列表 + 侧边栏:按产品线分组(可折叠 + localStorage 持久化)
- Admin: ROI 策略配置 + 项目映射 + 未映射收容

数据:
- 23 项目全部 AI 自动分类 + 自动 identifier(airhubs-hw-001 这种)
- launchedAt 按各项目首次 commit 时间设置

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 13:20:22 +08:00

87 lines
3.1 KiB
TypeScript

/**
* ROI 引擎纯函数单元测试。
* 仅测不依赖 DB 的逻辑;聚合/查询走集成测试。
*/
import { describe, it, expect } from 'bun:test';
import { evaluateConfidence } from '../../src/services/roi/confidence-evaluator';
import { bepFromTotals } from '../../src/services/roi/bep-calculator';
describe('confidence-evaluator', () => {
it('returns low when no events at all', () => {
expect(evaluateConfidence([], [])).toBe('low');
});
it('returns low when only cost, no revenue', () => {
expect(evaluateConfidence([{ dataSource: 'manual' }], [])).toBe('low');
});
it('returns low when all cost is auto-estimated', () => {
const costs = [
{ dataSource: 'auto_commits' },
{ dataSource: 'auto_tasks' },
];
const revenues = [{ dataSource: 'manual' }];
expect(evaluateConfidence(costs, revenues)).toBe('low');
});
it('returns high when 80%+ cost is plane_actual/manual + revenue exists', () => {
const costs = [
{ dataSource: 'plane_actual' },
{ dataSource: 'plane_actual' },
{ dataSource: 'manual' },
{ dataSource: 'manual' },
{ dataSource: 'auto_commits' }, // 1/5 = 20% 自动,刚好满足 80%+ 高质量
];
const revenues = [{ dataSource: 'api_pulled' }];
expect(evaluateConfidence(costs, revenues)).toBe('high');
});
it('returns medium when high-quality cost ratio is between 1% and 80%', () => {
const costs = [
{ dataSource: 'plane_actual' },
{ dataSource: 'auto_commits' },
{ dataSource: 'auto_commits' },
];
const revenues = [{ dataSource: 'manual' }];
// 1/3 = 33% 高质量,落在 medium 区间
expect(evaluateConfidence(costs, revenues)).toBe('medium');
});
it('returns high when all cost manual', () => {
const costs = [{ dataSource: 'manual' }, { dataSource: 'manual' }];
const revenues = [{ dataSource: 'manual' }];
expect(evaluateConfidence(costs, revenues)).toBe('high');
});
});
describe('bepFromTotals', () => {
it('returns 0 when already broken even (revenue >= cost)', () => {
expect(bepFromTotals(100000, 100000, 0, 0)).toBe(0);
expect(bepFromTotals(100000, 150000, 0, 0)).toBe(0);
});
it('returns null when recent net income is non-positive', () => {
// 累计亏 5w,近 30 天净产出为 0
expect(bepFromTotals(100000, 50000, 30000, 30000)).toBe(null);
// 近 30 天还在亏
expect(bepFromTotals(100000, 50000, 30000, 20000)).toBe(null);
});
it('returns positive days when on track to break even', () => {
// 总投入 100w,总产出 80w => 缺口 20w
// 近 30 天:成本 3w,收入 6w => 日均净 1000 元
// 预计天数 = 200000 / 1000 = 200 天
expect(bepFromTotals(1_000_000, 800_000, 30_000, 60_000)).toBe(200);
});
it('rounds up partial days', () => {
// 缺口 1000,日均净 300 => 3.33 天向上取整 = 4
expect(bepFromTotals(2000, 1000, 0, 9000)).toBe(4); // daily = 9000/30 = 300
});
it('respects custom windowDays', () => {
// 缺口 1000,近 10 天净 1000 => 日均 100 => 10 天回本
expect(bepFromTotals(2000, 1000, 0, 1000, 10)).toBe(10);
});
});