Files
memind/schedule-reminder-worker.test.mjs
T
john c36558e0f3
Memind CI / Test, build, and release guards (push) Successful in 4m20s
feat(wechat): keep morning/weather push for suspended billing users
欠费停用用户仍送达早安与天气,跳过复杂页面订阅与定时任务补发。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-22 16:30:40 +08:00

468 lines
12 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { startScheduleReminderWorker } from './schedule-reminder-worker.mjs';
import { pushSubscriptionMetadataSource } from './wechat/push-subscription-catalog.mjs';
test('schedule reminder worker sends due daily todo digest', async () => {
const calls = [];
const sent = [];
const subscription = {
id: 'sub-1',
userId: 'user-1',
hour: 7,
minute: 0,
timezone: 'Asia/Shanghai',
channel: 'wechat',
attempts: 1,
};
const worker = startScheduleReminderWorker({
intervalMs: 60_000,
scheduleService: {
async listDueReminders() {
return [];
},
async lockReminder() {
return null;
},
async listDueDigestSubscriptions() {
calls.push('list');
return [subscription];
},
async lockDigestSubscription(id) {
calls.push(`lock:${id}`);
return subscription;
},
async buildTodoDigestText({ userId }) {
calls.push(`digest:${userId}`);
return '6月18日 待办记录\n\n1. 开会';
},
async logDelivery(input) {
calls.push(`log:${input.status}`);
},
async markDigestSent(input) {
calls.push(`sent:${input.id}`);
},
async markDigestFailed() {
calls.push('failed');
},
},
async sendWechatTextToUser(userId, text) {
sent.push({ userId, text });
},
logger: { warn() {} },
runOnStart: false,
});
await worker.runOnce();
worker.stop();
assert.deepEqual(sent, [{ userId: 'user-1', text: '6月18日 待办记录\n\n1. 开会' }]);
assert.deepEqual(calls, ['list', 'lock:sub-1', 'digest:user-1', 'log:success', 'sent:sub-1']);
});
test('schedule reminder worker sends due balance alert for same user only', async () => {
const sent = [];
const alerts = [];
const worker = startScheduleReminderWorker({
intervalMs: 60_000,
scheduleService: {
async listDueReminders() {
return [];
},
async lockReminder() {
return null;
},
async listDueDigestSubscriptions() {
return [];
},
async lockDigestSubscription() {
return null;
},
async listDueBalanceAlerts() {
return [
{
id: 'alert-1',
userId: 'user-1',
thresholdCents: 2000,
channel: 'wechat',
attempts: 0,
},
];
},
async lockBalanceAlert(id) {
return { id, userId: 'user-1', thresholdCents: 2000, channel: 'wechat', attempts: 0 };
},
async getUserWalletSnapshot(userId) {
return userId === 'user-1' ? { balanceCents: 1500 } : { balanceCents: 9999 };
},
async logDelivery(input) {
alerts.push(`log:${input.status}:${input.userId}`);
},
async markBalanceAlertSent(input) {
alerts.push(`sent:${input.id}`);
},
async markBalanceAlertFailed() {
alerts.push('failed');
},
async buildTodoDigestText() {
return '';
},
async markDigestSent() {},
async markDigestFailed() {},
},
async sendWechatTextToUser(userId, text) {
sent.push({ userId, text });
},
logger: { warn() {} },
runOnStart: false,
});
await worker.runOnce();
worker.stop();
assert.deepEqual(sent, [
{ userId: 'user-1', text: '余额不足提醒\n\n你的账户余额已低于 20.00 元,请及时充值。' },
]);
assert.deepEqual(alerts, ['log:success:user-1', 'sent:alert-1']);
});
test('schedule reminder worker sends due item reminders via wechat', async () => {
const sent = [];
const calls = [];
const reminder = {
id: 'rem-1',
userId: 'user-1',
itemId: 'item-1',
remindAt: Date.now() - 1000,
channel: 'wechat',
attempts: 1,
};
const worker = startScheduleReminderWorker({
intervalMs: 60_000,
scheduleService: {
async listDueReminders() {
calls.push('list-reminders');
return [reminder];
},
async lockReminder(id) {
calls.push(`lock-reminder:${id}`);
return reminder;
},
async buildReminderText(input) {
calls.push(`text:${input.id}`);
return '【待办提醒】带材料\n提醒时间:09:00';
},
async createUserNotification(input) {
calls.push(`notify:${input.notificationType}`);
},
async logDelivery(input) {
calls.push(`log:${input.status}:${input.reminderId}`);
},
async markReminderSent(input) {
calls.push(`sent:${input.id}`);
},
async markReminderCancelled() {
calls.push('cancelled');
},
async markReminderFailed() {
calls.push('failed');
},
async listDueDigestSubscriptions() {
return [];
},
async lockDigestSubscription() {
return null;
},
async listDueBalanceAlerts() {
return [];
},
async lockBalanceAlert() {
return null;
},
async buildTodoDigestText() {
return '';
},
async markDigestSent() {},
async markDigestFailed() {},
},
async sendWechatTextToUser(userId, text) {
sent.push({ userId, text });
},
logger: { warn() {} },
runOnStart: false,
});
await worker.runOnce();
worker.stop();
assert.deepEqual(sent, [
{ userId: 'user-1', text: '【待办提醒】带材料\n提醒时间:09:00' },
]);
assert.deepEqual(calls, [
'list-reminders',
'lock-reminder:rem-1',
'text:rem-1',
'notify:schedule_reminder',
'log:success:rem-1',
'sent:rem-1',
]);
});
test('schedule reminder worker skips wechat for in_app reminders', async () => {
const sent = [];
const reminder = {
id: 'rem-2',
userId: 'user-2',
itemId: 'item-2',
remindAt: Date.now() - 1000,
channel: 'in_app',
attempts: 0,
};
const worker = startScheduleReminderWorker({
intervalMs: 60_000,
scheduleService: {
async listDueReminders() {
return [reminder];
},
async lockReminder() {
return reminder;
},
async buildReminderText() {
return '【待办提醒】开会';
},
async createUserNotification() {},
async logDelivery() {},
async markReminderSent() {},
async markReminderCancelled() {},
async markReminderFailed() {},
async listDueDigestSubscriptions() {
return [];
},
async lockDigestSubscription() {
return null;
},
async listDueBalanceAlerts() {
return [];
},
async lockBalanceAlert() {
return null;
},
async buildTodoDigestText() {
return '';
},
async markDigestSent() {},
async markDigestFailed() {},
},
async sendWechatTextToUser(userId, text) {
sent.push({ userId, text });
},
logger: { warn() {} },
runOnStart: false,
});
await worker.runOnce();
worker.stop();
assert.deepEqual(sent, []);
});
test('schedule reminder worker skips nonessential push subscriptions for suspended users', async () => {
const sent = [];
const calls = [];
const reminder = {
id: 'rem-news',
userId: 'user-suspended',
itemId: 'item-news',
remindAt: Date.now() - 1000,
channel: 'wechat',
attempts: 1,
};
const worker = startScheduleReminderWorker({
intervalMs: 60_000,
scheduleService: {
async listDueReminders() {
return [reminder];
},
async lockReminder() {
return reminder;
},
async getUserStatusSnapshot() {
return 'suspended';
},
async getReminderPushSource() {
return pushSubscriptionMetadataSource('news');
},
async buildReminderDelivery() {
return {
text: '📰 每日新闻早报\n\n链接 https://m.tkmind.cn/page.html',
verifiedHtmlUrls: ['https://m.tkmind.cn/page.html'],
};
},
async logDelivery(input) {
calls.push(`log:${input.status}:${input.errorMessage ?? ''}`);
},
async markReminderSent(input) {
calls.push(`sent:${input.id}`);
},
async scheduleNextDailyReminder() {
calls.push('next');
},
async markReminderCancelled() {
calls.push('cancelled');
},
async markReminderFailed() {
calls.push('failed');
},
async listDueDigestSubscriptions() {
return [];
},
async lockDigestSubscription() {
return null;
},
async listDueBalanceAlerts() {
return [];
},
async lockBalanceAlert() {
return null;
},
},
async sendWechatTextToUser(userId, text) {
sent.push({ userId, text });
},
logger: { warn() {} },
runOnStart: false,
});
await worker.runOnce();
worker.stop();
assert.deepEqual(sent, []);
assert.deepEqual(calls, [
'log:skipped:欠费停用:已跳过非基础推送',
'sent:rem-news',
'next',
]);
});
test('schedule reminder worker still sends morning push for suspended users', async () => {
const sent = [];
const reminder = {
id: 'rem-morning',
userId: 'user-suspended',
itemId: 'item-morning',
remindAt: Date.now() - 1000,
channel: 'wechat',
attempts: 1,
};
const worker = startScheduleReminderWorker({
intervalMs: 60_000,
scheduleService: {
async listDueReminders() {
return [reminder];
},
async lockReminder() {
return reminder;
},
async getUserStatusSnapshot() {
return 'suspended';
},
async getReminderPushSource() {
return pushSubscriptionMetadataSource('morning');
},
async buildReminderDelivery() {
return { text: '☀️ 早安问候\n\n今天也要加油', verifiedHtmlUrls: [] };
},
async createUserNotification() {},
async logDelivery() {},
async markReminderSent() {},
async scheduleNextDailyReminder() {},
async markReminderCancelled() {},
async markReminderFailed() {},
async listDueDigestSubscriptions() {
return [];
},
async lockDigestSubscription() {
return null;
},
async listDueBalanceAlerts() {
return [];
},
async lockBalanceAlert() {
return null;
},
},
async sendWechatTextToUser(userId, text) {
sent.push({ userId, text });
},
logger: { warn() {} },
runOnStart: false,
});
await worker.runOnce();
worker.stop();
assert.equal(sent.length, 1);
assert.match(sent[0].text, /早安/);
});
test('schedule reminder worker retries when wechat delivery is deferred', async () => {
const calls = [];
const reminder = {
id: 'rem-deferred',
userId: 'user-1',
itemId: 'item-1',
remindAt: Date.now() - 1000,
channel: 'wechat',
attempts: 1,
};
const worker = startScheduleReminderWorker({
intervalMs: 60_000,
scheduleService: {
async listDueReminders() {
return [reminder];
},
async lockReminder() {
return reminder;
},
async buildReminderText() {
return '【待办提醒】开会';
},
async createUserNotification() {},
async logDelivery(input) {
calls.push(`log:${input.status}`);
},
async markReminderSent() {
calls.push('sent');
},
async markReminderFailed() {
calls.push('failed');
},
async markReminderCancelled() {},
async listDueDigestSubscriptions() {
return [];
},
async lockDigestSubscription() {
return null;
},
async listDueBalanceAlerts() {
return [];
},
async lockBalanceAlert() {
return null;
},
},
notificationDispatcher: {
async sendScheduleNotification() {
return false;
},
},
logger: { warn() {} },
runOnStart: false,
});
await worker.runOnce();
worker.stop();
assert.deepEqual(calls, ['log:failed', 'failed']);
});