import assert from 'node:assert/strict'; import test from 'node:test'; import { createScheduleService, shouldAutoCreateReminderAtStart } from './schedule-service.mjs'; test('shouldAutoCreateReminderAtStart detects reminder intent from title or description', () => { assert.equal( shouldAutoCreateReminderAtStart({ title: '吃药提醒', description: '今天上午10点吃药', startAt: 1, }), true, ); assert.equal( shouldAutoCreateReminderAtStart({ title: '开会', description: '明天下午三点', startAt: 1, }), false, ); assert.equal( shouldAutoCreateReminderAtStart({ title: '吃药提醒', description: '今天上午10点吃药', startAt: 1, noReminder: true, }), false, ); assert.equal( shouldAutoCreateReminderAtStart({ title: '买菜', startAt: null, }), false, ); }); test('listUserNotifications accepts mysql JSON columns returned as objects', async () => { const service = createScheduleService({ async query(sql) { assert.match(sql, /FROM h5_user_notifications/); return [ [ { id: 'notification-1', user_id: 'user-1', channel: 'wechat', notification_type: 'balance_low', title: '余额不足提醒', body: '你的账户余额已低于 20.00 元,请及时充值。', data_json: { thresholdCents: 2000 }, status: 'unread', read_at: null, created_at: 1782313300000, updated_at: 1782313300000, }, ], ]; }, }); const notifications = await service.listUserNotifications({ userId: 'user-1' }); assert.equal(notifications.length, 1); assert.deepEqual(notifications[0].data, { thresholdCents: 2000 }); }); test('buildReminderText formats reminder with event time', async () => { const queries = []; const service = createScheduleService({ async query(sql, params) { queries.push(sql.trim().slice(0, 40)); if (sql.includes('FROM h5_schedule_items')) { return [ [ { id: 'item-1', user_id: 'user-1', kind: 'event', title: '开会', description: null, status: 'active', start_at: 1_786_000_000_000, end_at: null, due_at: null, all_day: 0, timezone: 'Asia/Shanghai', location: '会议室 A', created_at: 1, updated_at: 1, }, ], ]; } return [[]]; }, clock: { now: () => 1_786_000_000_000 }, }); const text = await service.buildReminderText({ userId: 'user-1', itemId: 'item-1', remindAt: 1_785_964_000_000, }); assert.match(text, /【待办提醒】开会/); assert.match(text, /事项时间:/); assert.match(text, /提醒时间:/); assert.match(text, /地点:会议室 A/); }); test('parseLocalDateTimeString resolves Asia/Shanghai wall clock', async () => { const { parseLocalDateTimeString, assertReasonableScheduleEpoch } = await import('./schedule-time.mjs'); const ms = parseLocalDateTimeString('2026-07-01 06:00', 'Asia/Shanghai'); assert.equal(new Date(ms).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }), '2026/7/1 06:00:00'); const now = Date.UTC(2026, 5, 29, 16, 0, 0); assert.throws( () => assertReasonableScheduleEpoch(1779127200000, { now, fieldName: '开始时间' }), /超出合理范围/, ); }); test('cancelReminder marks pending reminder as cancelled', async () => { const service = createScheduleService({ async query(sql) { if (sql.includes('FROM h5_schedule_reminders r') && sql.includes('WHERE r.id = ?')) { return [ [ { id: 'rem-1', user_id: 'user-1', item_id: 'item-1', remind_at: 1782856500000, offset_minutes: 5, channel: 'wechat', status: 'pending', attempts: 0, last_error: null, locked_until: null, sent_at: null, created_at: 1, updated_at: 1, item_title: '早起跑步', item_kind: 'event', item_timezone: 'Asia/Shanghai', item_start_at: 1782856800000, item_end_at: 1782858600000, }, ], ]; } if (sql.includes('UPDATE h5_schedule_reminders') && sql.includes('cancelled')) { return [{ affectedRows: 1 }]; } return [[], undefined]; }, }); const reminder = await service.cancelReminder({ userId: 'user-1', reminderId: 'rem-1' }); assert.equal(reminder.status, 'cancelled'); }); test('deleteReminders bulk deletes by user', async () => { const service = createScheduleService({ async query(sql, params) { if (sql.startsWith('DELETE FROM h5_schedule_reminders')) { assert.equal(params[0], 'user-1'); assert.deepEqual(params.slice(1), ['rem-1', 'rem-2']); return [{ affectedRows: 2 }]; } return [[], undefined]; }, }); const deleted = await service.deleteReminders({ userId: 'user-1', reminderIds: ['rem-1', 'rem-2'], }); assert.equal(deleted, 2); });