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'); });