#!/usr/bin/env node /** * Phase A ITL 离线验证:分类 → Draft → Confirm → Commit(mock 服务,无需 DB) */ import assert from 'node:assert/strict'; import { classifyUserIntent } from '../intent-classifier.mjs'; import { handleWechatIntentTransaction } from '../wechat/handlers/intent-transaction.mjs'; import { isIntentTransactionEnabled } from '../intent-transaction-config.mjs'; import { attachUnifiedTaskSync } from '../task-unified-sync.mjs'; import { createTaskUnifiedService } from '../task-unified-service.mjs'; import { formatQueryGuardReply } from '../intent-query-guard.mjs'; const env = { H5_INTENT_TRANSACTION_ENABLED: '1', H5_UNIFIED_TASKS_ENABLED: '1', H5_DEFAULT_TIMEZONE: 'Asia/Shanghai', }; let passed = 0; let failed = 0; function pass(label, detail = '') { passed += 1; console.log(`✔ ${label}${detail ? `: ${detail}` : ''}`); } function fail(label, detail = '') { failed += 1; console.error(`✘ ${label}${detail ? `: ${detail}` : ''}`); } function createDraftStore() { let pending = null; return { async getPendingDraft(userId) { return pending?.userId === userId ? pending : null; }, async createDraft(payload) { pending = { id: 'draft-verify-1', status: 'draft', ...payload, payload: payload.payload, }; return pending; }, async cancelDraft(id, userId) { if (pending?.id === id && pending.userId === userId) pending = null; return { id, status: 'cancelled' }; }, async markDraftCommitted(id, userId, committedRef) { pending = { ...pending, id, userId, status: 'committed', committedRef }; return pending; }, }; } function createScheduleService() { const calls = []; return { calls, async createItem(payload) { calls.push(['createItem', payload]); return { id: 'item-verify-1', ...payload }; }, async createReminder(payload) { calls.push(['createReminder', payload]); return { id: 'reminder-verify-1', ...payload }; }, buildTodoDigestText() { return '今天有 0 条待办。'; }, }; } async function main() { if (!isIntentTransactionEnabled(env)) { fail('feature flag', 'H5_INTENT_TRANSACTION_ENABLED 应为 1'); process.exit(1); } pass('feature flag enabled'); const query = classifyUserIntent('有没有我的新闻定时任务'); if (query.layer === 'L0') pass('query guard routes inventory to L0'); else fail('query guard routes inventory to L0', JSON.stringify(query)); const bare = classifyUserIntent('设置提醒'); if (bare.layer === 'L1' && bare.clarify?.length) pass('bare reminder asks for slot fill'); else fail('bare reminder asks for slot fill', JSON.stringify(bare)); const cancel = classifyUserIntent('取消每日新闻任务'); if (cancel.action === 'cancel_scheduled_task') pass('cancel routes to manage action'); else fail('cancel routes to manage action', JSON.stringify(cancel)); const drafts = createDraftStore(); const scheduleService = createScheduleService(); const card = await handleWechatIntentTransaction({ intent: { agentText: '下午2点半提醒我开项目计划例会', msgId: 'verify-1', msgType: 'text' }, user: { userId: 'verify-user' }, intentDraftService: drafts, scheduleService, env, }); if (card?.includes('我准备执行') && card.includes('确认')) { pass('creates action card for timed reminder'); } else { fail('creates action card for timed reminder', card); } const pending = await drafts.getPendingDraft('verify-user'); if (pending?.draftType === 'timed_reminder') pass('persists pending draft'); else fail('persists pending draft', JSON.stringify(pending)); const committed = await handleWechatIntentTransaction({ intent: { agentText: '确认', msgId: 'verify-2', msgType: 'text' }, user: { userId: 'verify-user' }, intentDraftService: drafts, scheduleService, env, }); if (committed?.includes('已设置提醒') && scheduleService.calls.length === 2) { pass('confirm commits reminder to schedule service'); } else { fail('confirm commits reminder to schedule service', `${committed} calls=${scheduleService.calls.length}`); } const slotFill = await handleWechatIntentTransaction({ intent: { agentText: '设置提醒', msgId: 'verify-3', msgType: 'text' }, user: { userId: 'verify-user-2' }, intentDraftService: createDraftStore(), scheduleService, env, }); if (slotFill?.includes('补充') && !slotFill.includes('我准备执行')) { pass('slot fill does not create confirmable draft'); } else { fail('slot fill does not create confirmable draft', slotFill); } const disabled = await handleWechatIntentTransaction({ intent: { agentText: '下午2点提醒我', msgId: 'verify-4', msgType: 'text' }, user: { userId: 'verify-user-3' }, intentDraftService: createDraftStore(), scheduleService, env: { H5_INTENT_TRANSACTION_ENABLED: '0' }, }); if (disabled === null) pass('returns null when feature disabled'); else fail('returns null when feature disabled', disabled); const syncCalls = []; const memoryPool = { tasks: [], async query(sql, params = []) { if (sql.includes('JOIN h5_schedule_reminders')) { return [[{ id: 'item-verify-2', title: '项目计划例会', start_at: 9999, timezone: 'Asia/Shanghai', source_channel: 'wechat', source_message_id: null, source_text: null, reminder_id: 'reminder-verify-2', remind_at: 9999, channel: 'wechat', reminder_status: 'pending', }]]; } if (sql.includes('INSERT INTO h5_tasks')) { this.tasks.push({ legacy_ref_json: params[12], user_id: params[1] }); return [{ affectedRows: 1 }]; } if (sql.includes('SELECT * FROM h5_tasks') && sql.includes('legacy_ref_json')) return [[]]; if (sql.includes('SELECT * FROM h5_tasks WHERE id = ?')) { return [[{ id: params[0], user_id: 'verify-user', type: 'reminder', title: '项目计划例会', spec_json: '{}', trigger_json: '{}', action_json: '{}', action_level: 1, notify_channel: 'wechat', status: 'active', next_run_at: 1000, last_run_at: null, legacy_ref_json: '{"table":"h5_schedule_reminders","id":"reminder-verify-1"}', source_channel: 'wechat', source_message_id: null, source_text: null, created_at: 1, updated_at: 1, }]]; } return [[]]; }, }; const unifiedService = createTaskUnifiedService(memoryPool, { clock: { now: () => 1234 } }); const wrappedSchedule = { async createItem(payload) { return { id: 'item-verify-2', userId: payload.userId, kind: 'event', title: payload.title, timezone: 'Asia/Shanghai', status: 'active' }; }, async createReminder(payload) { return { id: 'reminder-verify-2', itemId: payload.itemId, remindAt: payload.remindAt, channel: 'wechat' }; }, }; attachUnifiedTaskSync({ scheduleService: wrappedSchedule, scheduledTaskService: {}, taskUnifiedService: { ...unifiedService, async syncFromCommit(payload) { syncCalls.push(payload.kind); return unifiedService.syncFromCommit(payload); }, }, pool: memoryPool, env, }); await wrappedSchedule.createReminder({ userId: 'verify-user', itemId: 'item-verify-2', remindAt: 9999 }); if (syncCalls.includes('timed_reminder')) pass('schedule reminder path dual-writes via attachUnifiedTaskSync'); else fail('schedule reminder path dual-writes via attachUnifiedTaskSync', syncCalls.join(',')); const queryReply = await formatQueryGuardReply('有没有我的新闻定时任务', { taskUnifiedService: { async listUserTasks() { return [{ type: 'automation', title: '每日新闻页', trigger: { repeat: 'daily', hour: 5, minute: 30 }, nextRunAt: 1000 }]; }, }, userId: 'verify-user', }); if (queryReply.includes('任务一览') && queryReply.includes('每日新闻页')) { pass('query guard uses unified task list'); } else { fail('query guard uses unified task list', queryReply); } console.log(`\n${passed} passed, ${failed} failed`); process.exit(failed > 0 ? 1 : 0); } main().catch((err) => { console.error(err); process.exit(1); });