'use strict'; const test = require('node:test'); const assert = require('node:assert/strict'); const { DEFAULT_NOTIFY_TIME, MAX_RECIPIENTS, buildIdempotencyKey, normalizeFlag, normalizeNotifyTime, normalizeRecipients, } = require('../src/services/dailyInventoryNotificationSettings'); const { ACTION_TITLE, NOTIFICATION_TITLE, buildNotificationContent, minutesOfDay, resolveBizDate, withRetry, } = require('../src/jobs/dailyInventoryNotificationJob'); const { chinaTimeParts } = require('../src/services/dailyInventoryScreenshot'); // ── 收件人名单规范化 ──────────────────────────────────────────── test('normalizeRecipients 去空值、去空白、保持顺序', () => { assert.deepEqual(normalizeRecipients(['张三', ' ', '李四', '', null, undefined]), ['张三', '李四']); }); test('normalizeRecipients 去重且忽略首尾空格', () => { assert.deepEqual(normalizeRecipients([' 张三 ', '张三', '李四', ' 李四']), ['张三', '李四']); }); test('normalizeRecipients 支持逗号/中文逗号/分号/换行分隔的字符串', () => { assert.deepEqual( normalizeRecipients('张三,李四,王五;赵六\n孙七'), ['张三', '李四', '王五', '赵六', '孙七'] ); }); test('normalizeRecipients 对非法输入返回空数组', () => { assert.deepEqual(normalizeRecipients(null), []); assert.deepEqual(normalizeRecipients(undefined), []); assert.deepEqual(normalizeRecipients(12345), []); }); test('normalizeRecipients 超量时截断到上限', () => { const list = Array.from({ length: MAX_RECIPIENTS + 5 }, (_, i) => `用户${i}`); const result = normalizeRecipients(list); assert.equal(result.length, MAX_RECIPIENTS); assert.equal(result[0], '用户0'); }); // ── 通知时间校验 ──────────────────────────────────────────────── test('normalizeNotifyTime 接受合法 HH:mm 并补零', () => { assert.equal(normalizeNotifyTime('1:05'), '01:05'); assert.equal(normalizeNotifyTime('23:59'), '23:59'); assert.equal(normalizeNotifyTime('00:00'), '00:00'); }); test('normalizeNotifyTime 支持 HH:mm:ss 并截断到分钟', () => { assert.equal(normalizeNotifyTime('01:00:30'), '01:00'); }); test('normalizeNotifyTime 非法值回退默认 01:00', () => { assert.equal(normalizeNotifyTime('24:00'), DEFAULT_NOTIFY_TIME); assert.equal(normalizeNotifyTime('12:60'), DEFAULT_NOTIFY_TIME); assert.equal(normalizeNotifyTime('abc'), DEFAULT_NOTIFY_TIME); assert.equal(normalizeNotifyTime(''), DEFAULT_NOTIFY_TIME); assert.equal(normalizeNotifyTime(null), DEFAULT_NOTIFY_TIME); }); test('normalizeFlag 只接受明确的布尔语义,未知值回退默认', () => { assert.equal(normalizeFlag('true', false), true); assert.equal(normalizeFlag('off', true), false); assert.equal(normalizeFlag('maybe', true), true); assert.equal(normalizeFlag(null, false), false); }); // ── 幂等键 ────────────────────────────────────────────────────── test('buildIdempotencyKey 正式推送按业务日期+收件人稳定生成', () => { const a = buildIdempotencyKey({ bizDate: '2026-09-25', recipient: '张三', mode: 'scheduled' }); const b = buildIdempotencyKey({ bizDate: '2026-09-25', recipient: '张三', mode: 'scheduled' }); const c = buildIdempotencyKey({ bizDate: '2026-09-25', recipient: '李四', mode: 'scheduled' }); assert.equal(a, b); assert.notEqual(a, c); }); test('buildIdempotencyKey 测试推送带随机数,不占用正式幂等记录', () => { const formal = buildIdempotencyKey({ bizDate: '2026-09-25', recipient: '张三', mode: 'scheduled' }); const t1 = buildIdempotencyKey({ bizDate: '2026-09-25', recipient: '张三', mode: 'test', nonce: 'n1' }); const t2 = buildIdempotencyKey({ bizDate: '2026-09-25', recipient: '张三', mode: 'test', nonce: 'n2' }); assert.notEqual(t1, t2); assert.notEqual(t1, formal); }); // ── 通知正文 ──────────────────────────────────────────────────── test('buildNotificationContent 包含数据日期、生成时间、图片地址与页面链接', () => { const content = buildNotificationContent({ bizDate: '2026-09-25', takenAt: '2026/09/25 01:00:00', imageUrl: 'https://cdn.example.com/daily-inventory/2026-09-25/daily-inventory-20260925-010000.png', pageUrl: 'https://ck.yydd.ai/planning/operation/daily-inventory', }); assert.match(content, /2026-09-25/); assert.match(content, /2026\/09\/25 01:00:00/); assert.match(content, /https:\/\/cdn\.example\.com\//); assert.match(content, /https:\/\/ck\.yydd\.ai\/planning\/operation\/daily-inventory/); assert.match(content, new RegExp(ACTION_TITLE)); assert.equal(NOTIFICATION_TITLE, '每日进销存数据'); }); // ── 业务日期与调度时刻 ────────────────────────────────────────── test('resolveBizDate 取北京时间昨日(与页面 T-1 口径一致)', () => { assert.equal(resolveBizDate(new Date('2026-09-25T01:00:00+08:00')), '2026-09-24'); assert.equal(resolveBizDate(new Date('2026-09-25T23:30:00+08:00')), '2026-09-24'); }); test('minutesOfDay 正确换算,非法值返回 null', () => { assert.equal(minutesOfDay('00:00'), 0); assert.equal(minutesOfDay('01:00'), 60); assert.equal(minutesOfDay('23:59'), 1439); assert.equal(minutesOfDay('24:00'), null); assert.equal(minutesOfDay('abc'), null); assert.equal(minutesOfDay(null), null); }); test('chinaTimeParts 生成对象键所需的日期与时间片段', () => { const parts = chinaTimeParts(new Date('2026-09-25T01:00:00+08:00')); assert.equal(parts.bizDate, '2026-09-25'); assert.equal(parts.dateKey, '20260925'); assert.equal(parts.timeKey, '010000'); assert.equal( `daily-inventory/${parts.bizDate}/daily-inventory-${parts.dateKey}-${parts.timeKey}.png`, 'daily-inventory/2026-09-25/daily-inventory-20260925-010000.png' ); }); // ── 重试与退避 ────────────────────────────────────────────────── test('withRetry 第二次成功后返回结果', async () => { let calls = 0; const result = await withRetry( async () => { calls += 1; if (calls < 2) throw new Error('临时失败'); return 'ok'; }, { label: '单元测试', attempts: 3, baseDelayMs: 1 } ); assert.equal(result, 'ok'); assert.equal(calls, 2); }); test('withRetry 超过次数后抛出最后一次错误', async () => { let calls = 0; await assert.rejects( withRetry( async () => { calls += 1; throw new Error(`失败${calls}`); }, { label: '单元测试', attempts: 3, baseDelayMs: 1 } ), /失败3/ ); assert.equal(calls, 3); });