e42417bd6e
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>
261 lines
8.2 KiB
JavaScript
261 lines
8.2 KiB
JavaScript
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('1 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');
|
||
});
|