Files
memind/wechat/subscribe-morning-reminder.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

261 lines
8.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import test from 'node:test';
import assert from 'node:assert/strict';
import {
buildSubscribeMorningReminderPromptLines,
commitSubscribeMorningReminder,
createSubscribeMorningReminderPendingStore,
formatSubscribeMorningReminderCancelledReply,
formatSubscribeMorningReminderCommittedReply,
formatSubscribeMorningReminderUpdatedReply,
handleSubscribeMorningReminderManageTurn,
handleSubscribeMorningReminderTurn,
isSubscribeMorningConfirmReply,
parseSubscribeMorningCancelRequest,
parseSubscribeMorningConfirmReply,
parseSubscribeMorningModifyRequest,
} from './subscribe-morning-reminder.mjs';
test('parseSubscribeMorningConfirmReply accepts 1 and 1 with custom time', () => {
assert.deepEqual(parseSubscribeMorningConfirmReply('1', { defaultHour: 8, defaultMinute: 0 }), {
hour: 8,
minute: 0,
});
assert.deepEqual(parseSubscribeMorningConfirmReply(' 1 7点 '), { hour: 7, minute: 0 });
assert.deepEqual(parseSubscribeMorningConfirmReply(' 7:30'), { hour: 7, minute: 30 });
assert.equal(parseSubscribeMorningConfirmReply('确认'), null);
assert.equal(parseSubscribeMorningConfirmReply('11'), null);
assert.equal(isSubscribeMorningConfirmReply('1 7点'), true);
});
test('parseSubscribeMorningCancelRequest detects cancel phrases', () => {
assert.equal(parseSubscribeMorningCancelRequest('取消早安'), true);
assert.equal(parseSubscribeMorningCancelRequest('不要早安了'), true);
assert.equal(parseSubscribeMorningCancelRequest('1'), false);
});
test('parseSubscribeMorningModifyRequest parses morning time change', () => {
assert.deepEqual(parseSubscribeMorningModifyRequest('早安改到7点'), { hour: 7, minute: 0 });
assert.deepEqual(parseSubscribeMorningModifyRequest('把早安改成7点半'), { hour: 7, minute: 30 });
assert.equal(parseSubscribeMorningModifyRequest('你好'), null);
});
test('handleSubscribeMorningReminderTurn creates daily reminder only on reply 1', async () => {
const store = createSubscribeMorningReminderPendingStore();
const calls = [];
const scheduleService = {
async findActiveItemByMetadataSource() {
return null;
},
async createItem(payload) {
calls.push(['createItem', payload]);
return { id: 'item-1', ...payload };
},
async createReminder(payload) {
calls.push(['createReminder', payload]);
return { id: 'rem-1', ...payload };
},
};
await store.setPending({ appId: 'wx-app', openid: 'openid-a' });
const ignored = await handleSubscribeMorningReminderTurn({
appId: 'wx-app',
openid: 'openid-a',
text: '你好',
pendingStore: store,
scheduleService,
boundUser: { userId: 'user-1' },
hour: 8,
minute: 0,
});
assert.equal(ignored, null);
assert.equal(calls.length, 0);
assert.equal(await store.getPending({ appId: 'wx-app', openid: 'openid-a' }), null);
await store.setPending({ appId: 'wx-app', openid: 'openid-b' });
const committed = await handleSubscribeMorningReminderTurn({
appId: 'wx-app',
openid: 'openid-b',
text: '1 7点',
pendingStore: store,
scheduleService,
boundUser: { userId: 'user-2' },
hour: 8,
minute: 0,
});
assert.equal(
committed,
formatSubscribeMorningReminderCommittedReply({ hour: 7, minute: 0 }),
);
assert.equal(calls.length, 2);
assert.equal(calls[0][1].metadata.dailyHour, 7);
assert.equal(calls[1][1].channel, 'wechat');
});
test('handleSubscribeMorningReminderTurn keeps pending when unbound user replies 1', async () => {
const store = createSubscribeMorningReminderPendingStore();
await store.setPending({ appId: 'wx-app', openid: 'openid-c' });
const reply = await handleSubscribeMorningReminderTurn({
appId: 'wx-app',
openid: 'openid-c',
text: '1',
pendingStore: store,
scheduleService: null,
boundUser: null,
bindUrl: 'https://m.tkmind.cn/bind',
});
assert.match(reply, /请先完成绑定/);
assert.match(reply, /https:\/\/m\.tkmind\.cn\/bind/);
assert.ok(await store.getPending({ appId: 'wx-app', openid: 'openid-c' }));
});
test('handleSubscribeMorningReminderManageTurn updates and cancels active reminder', async () => {
const calls = [];
const scheduleService = {
async findActiveItemByMetadataSource() {
return {
id: 'item-morning',
metadata: { source: 'subscribe_morning_reminder', dailyHour: 8, dailyMinute: 0 },
};
},
async updateDailyScheduleItem(payload) {
calls.push(['update', payload]);
return payload;
},
async cancelScheduleItem(payload) {
calls.push(['cancel', payload]);
return payload;
},
};
const updated = await handleSubscribeMorningReminderManageTurn({
text: '早安改到7点',
scheduleService,
boundUser: { userId: 'user-1' },
});
assert.equal(updated, formatSubscribeMorningReminderUpdatedReply({ hour: 7, minute: 0 }));
assert.deepEqual(calls[0], ['update', {
userId: 'user-1',
itemId: 'item-morning',
hour: 7,
minute: 0,
timezone: 'Asia/Shanghai',
}]);
const cancelled = await handleSubscribeMorningReminderManageTurn({
text: '取消早安',
scheduleService,
boundUser: { userId: 'user-1' },
});
assert.equal(cancelled, formatSubscribeMorningReminderCancelledReply());
assert.equal(calls[1][0], 'cancel');
});
test('buildSubscribeMorningReminderPromptLines mentions reply 1 and custom time', () => {
const text = buildSubscribeMorningReminderPromptLines({ hour: 8, minute: 0 }).join('\n');
assert.match(text, /回复 1 确认/);
assert.match(text, /1 7点/);
assert.match(text, /取消早安/);
});
test('handleSubscribeMorningReminderTurn uses llm fallback before clearing pending', async () => {
const store = createSubscribeMorningReminderPendingStore();
const calls = [];
const scheduleService = {
async findActiveItemByMetadataSource() {
return null;
},
async createItem(payload) {
calls.push(['createItem', payload]);
return { id: 'item-llm', ...payload };
},
async createReminder(payload) {
calls.push(['createReminder', payload]);
return { id: 'rem-llm', ...payload };
},
};
const llmProviderService = {
async createChatCompletion() {
return {
ok: true,
reply: JSON.stringify({
action: 'confirm',
hour: 7,
minute: 30,
confidence: 0.95,
}),
};
},
};
const configService = {
async isSubscribeMorningLlmEnabled() {
return true;
},
async getConfig() {
return {
modelProviderKeyId: null,
model: null,
minConfidence: 0.65,
timeoutMs: 4000,
};
},
};
await store.setPending({ appId: 'wx-app', openid: 'openid-llm' });
const reply = await handleSubscribeMorningReminderTurn({
appId: 'wx-app',
openid: 'openid-llm',
text: '不要八点,改七点半',
pendingStore: store,
scheduleService,
boundUser: { userId: 'user-llm' },
hour: 8,
minute: 0,
wechatSubscribeMorningLlmConfigService: configService,
llmProviderService,
});
assert.equal(
reply,
formatSubscribeMorningReminderCommittedReply({ hour: 7, minute: 30 }),
);
assert.equal(calls[0][1].metadata.dailyHour, 7);
assert.equal(await store.getPending({ appId: 'wx-app', openid: 'openid-llm' }), null);
});
test('commitSubscribeMorningReminder updates existing item instead of creating duplicate', async () => {
const calls = [];
const scheduleService = {
async findActiveItemByMetadataSource() {
return { id: 'item-existing' };
},
async updateDailyScheduleItem(payload) {
calls.push(['updateDailyScheduleItem', payload]);
return { item: { id: 'item-existing', ...payload }, reminder: { id: 'rem-2' } };
},
async createItem(payload) {
calls.push(['createItem', payload]);
return { id: 'item-new', ...payload };
},
async createReminder(payload) {
calls.push(['createReminder', payload]);
return { id: 'rem-new', ...payload };
},
};
const committed = await commitSubscribeMorningReminder({
userId: 'user-3',
scheduleService,
hour: 7,
minute: 30,
timezone: 'Asia/Shanghai',
});
assert.equal(committed.item.id, 'item-existing');
assert.equal(calls.length, 1);
assert.equal(calls[0][0], 'updateDailyScheduleItem');
});