Files
memind/schedule-service.test.mjs
T
john e42417bd6e feat(wechat): add subscribe morning reminder, plaza welcome, and LLM fallback
Enable daily morning greeting on reply 1 (with custom time, modify, and cancel),
random greeting delivery, M发现 in subscribe welcome, and optional LLM parsing when
rules miss. Also fix WeChat MP draft/config error passthrough and add Tang E2E scripts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 17:32:10 +08:00

272 lines
8.1 KiB
JavaScript

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('updateDailyScheduleItem updates metadata and replaces pending reminder', async () => {
const queries = [];
let reminderSeq = 0;
const service = createScheduleService({
async query(sql, params) {
queries.push(String(sql).trim().slice(0, 48));
if (sql.includes('FROM h5_schedule_items') && sql.includes('WHERE id = ?')) {
return [[{
id: 'item-1',
user_id: 'user-1',
kind: 'event',
title: '早安问候',
status: 'active',
start_at: 1_786_000_000_000,
timezone: 'Asia/Shanghai',
metadata_json: JSON.stringify({
source: 'subscribe_morning_reminder',
recurrence: 'daily',
dailyHour: 8,
dailyMinute: 0,
}),
}]];
}
if (sql.includes('FROM h5_schedule_reminders') && sql.includes("status = 'pending'")) {
return [[{ id: 'rem-old', user_id: 'user-1', item_id: 'item-1', remind_at: 1, channel: 'wechat', status: 'pending' }]];
}
if (sql.includes('INSERT INTO h5_schedule_reminders')) {
reminderSeq += 1;
return [[]];
}
return [[]];
},
clock: { now: () => Date.parse('2026-09-10T10:00:00+08:00') },
});
const result = await service.updateDailyScheduleItem({
userId: 'user-1',
itemId: 'item-1',
hour: 7,
minute: 30,
});
assert.equal(result.item.metadata.dailyHour, 7);
assert.equal(result.item.metadata.dailyMinute, 30);
assert.ok(queries.some((q) => q.includes('UPDATE h5_schedule_items')));
assert.ok(queries.some((q) => q.includes('UPDATE h5_schedule_reminders')));
});
test('buildReminderText uses daily morning greeting library for subscribe reminders', async () => {
const service = createScheduleService({
async query(sql) {
if (sql.includes('FROM h5_schedule_items')) {
return [
[
{
id: 'item-morning',
user_id: 'user-morning',
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: null,
metadata_json: JSON.stringify({ source: 'subscribe_morning_reminder', recurrence: 'daily' }),
created_at: 1,
updated_at: 1,
},
],
];
}
return [[]];
},
clock: { now: () => Date.parse('2026-09-10T08:00:00+08:00') },
});
const text = await service.buildReminderText({
userId: 'user-morning',
itemId: 'item-morning',
remindAt: Date.parse('2026-09-10T08:00:00+08:00'),
});
assert.match(text, /^☀️ 早安\n\n/);
assert.doesNotMatch(text, /【待办提醒】/);
});
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);
});