const test = require('node:test') const assert = require('node:assert') const { expandEvents, computeAtRisk, LOOKAHEAD_DAYS } = require('../src/atRisk') const TODAY = new Date(2026, 7, 15) // 2026-08-15 // 距今 10 天的公历纪念日,提前 3 天提醒 const near = { id: 'a1', personName: '张三', remindEnabled: 1, importance: 'high', isLunar: false, solarMonth: 8, solarDay: 25, remindDays: 3 } // 距今 40 天 const mid = { id: 'a2', personName: '李四', remindEnabled: 1, importance: 'low', isLunar: false, solarMonth: 9, solarDay: 24, remindDays: 7 } test('LOOKAHEAD_DAYS 为 60', () => { assert.strictEqual(LOOKAHEAD_DAYS, 60) }) test('一条纪念日展开出「当天」和「提前」两个事件', () => { const events = expandEvents(near, TODAY) assert.strictEqual(events.length, 2) const onDay = events.find(e => e.kind === 'onDay') const ahead = events.find(e => e.kind === 'ahead') assert.strictEqual(onDay.fireInDays, 10) assert.strictEqual(ahead.fireInDays, 7) }) test('remindDays 为 0 时只有当天事件', () => { const events = expandEvents({ ...near, remindDays: 0 }, TODAY) assert.strictEqual(events.length, 1) assert.strictEqual(events[0].kind, 'onDay') }) test('超出 60 天窗口的事件被过滤掉', () => { const far = { ...near, solarMonth: 12, solarDay: 25, remindDays: 3 } assert.strictEqual(expandEvents(far, TODAY).length, 0) }) test('提前日已过、当天未到时,只剩当天事件', () => { // 距今 2 天,但提前 7 天的时点早已过去 const soon = { ...near, solarMonth: 8, solarDay: 17, remindDays: 7 } const events = expandEvents(soon, TODAY) assert.strictEqual(events.length, 1) assert.strictEqual(events[0].kind, 'onDay') }) test('余额足够时没有风险', () => { const r = computeAtRisk([near, mid], 10, TODAY) assert.strictEqual(r.atRiskCount, 0) assert.deepStrictEqual(r.atRiskNames, []) }) test('余额为 0 时全部有风险', () => { const r = computeAtRisk([near, mid], 0, TODAY) assert.strictEqual(r.atRiskCount, 4) assert.deepStrictEqual(r.atRiskNames, ['张三', '李四']) }) test('按时间先后消耗额度,后面的才有风险', () => { // 张三的两个事件在第 7、10 天,李四的在第 33、40 天 const r = computeAtRisk([near, mid], 2, TODAY) assert.strictEqual(r.atRiskCount, 2) assert.deepStrictEqual(r.atRiskNames, ['李四']) }) test('人名去重,同一人占两条只出现一次', () => { const r = computeAtRisk([near], 0, TODAY) assert.strictEqual(r.atRiskCount, 2) assert.deepStrictEqual(r.atRiskNames, ['张三']) }) test('未开启提醒的纪念日不参与计算', () => { const r = computeAtRisk([{ ...near, remindEnabled: 0 }], 0, TODAY) assert.strictEqual(r.atRiskCount, 0) }) // ------ 同一天内排序需与 reminder.js 的 runForUser 完全对齐 ------ // 四条纪念日都恰好在第 5 天产生一个事件: // 甲(当天/high) 乙(当天/low) 丙(提前/high) 丁(提前/medium) // 丙、丁的「当天」事件被安排在第 65 天(超出 60 天窗口),不会进入计算, // 这样每人恰好只贡献一个事件,排序结果可以精确断言。 const onDayHigh = { id: 'e1', personName: '甲', remindEnabled: 1, importance: 'high', isLunar: false, solarMonth: 8, solarDay: 20, remindDays: 0 } const onDayLow = { id: 'e2', personName: '乙', remindEnabled: 1, importance: 'low', isLunar: false, solarMonth: 8, solarDay: 20, remindDays: 0 } const aheadHigh = { id: 'e3', personName: '丙', remindEnabled: 1, importance: 'high', isLunar: false, solarMonth: 10, solarDay: 19, remindDays: 60 } const aheadMedium = { id: 'e4', personName: '丁', remindEnabled: 1, importance: 'medium', isLunar: false, solarMonth: 10, solarDay: 19, remindDays: 60 } // 刻意打乱输入顺序(不按「当天>提前、重要程度降序」排列), // 这样如果排序逻辑退化成只按 fireInDays 排(Array.sort 是稳定排序, // 同值会保留输入顺序),测试才能真正暴露出排序规则失效,而不是被输入顺序碰巧掩盖。 const sameDayGroup = [aheadMedium, aheadHigh, onDayLow, onDayHigh] test('同一天内,当天事件排在提前事件之前(与 reminder.js 一致)', () => { // 额度只够 2 条:应先消耗「当天」的甲、乙,风险留给「提前」的丙、丁 const r = computeAtRisk(sameDayGroup, 2, TODAY) assert.strictEqual(r.atRiskCount, 2) assert.deepStrictEqual(r.atRiskNames, ['丙', '丁']) }) test('同一天内,当天事件按重要程度降序(high 先于 low)', () => { // 额度只够 1 条:当天里 high 的甲应排在 low 的乙前面,先被消耗 const r = computeAtRisk(sameDayGroup, 1, TODAY) assert.strictEqual(r.atRiskCount, 3) assert.deepStrictEqual(r.atRiskNames, ['乙', '丙', '丁']) }) test('同一天内,提前事件按重要程度降序(high 先于 medium)', () => { // 额度够 3 条:甲、乙(当天)先消耗,提前事件里 high 的丙应先于 medium 的丁被消耗 const r = computeAtRisk(sameDayGroup, 3, TODAY) assert.strictEqual(r.atRiskCount, 1) assert.deepStrictEqual(r.atRiskNames, ['丁']) })