import assert from 'node:assert/strict'; import test from 'node:test'; import { createTaskUnifiedService, formatUnifiedTaskListReply, mapCommitToUnifiedTask, } from './task-unified-service.mjs'; test('mapCommitToUnifiedTask maps timed reminder commit', () => { const mapped = mapCommitToUnifiedTask({ userId: 'user-1', kind: 'timed_reminder', committed: { item: { id: 'item-1', title: '项目计划例会', startAt: 1000, timezone: 'Asia/Shanghai' }, reminder: { id: 'rem-1', remindAt: 1000, channel: 'wechat' }, }, }); assert.equal(mapped.type, 'reminder'); assert.equal(mapped.title, '项目计划例会'); assert.deepEqual(mapped.legacyRef, { table: 'h5_schedule_reminders', id: 'rem-1' }); }); test('mapCommitToUnifiedTask maps scheduled task commit', () => { const mapped = mapCommitToUnifiedTask({ userId: 'user-1', kind: 'scheduled_task', committed: { task: { id: 'task-1', title: '每日新闻页', taskSpec: '搜索今日新闻并生成 HTML 页面', recurrence: 'daily', hour: 6, minute: 0, nextRunAt: 2000, timezone: 'Asia/Shanghai', }, }, }); assert.equal(mapped.type, 'automation'); assert.equal(mapped.actionLevel, 2); }); test('formatUnifiedTaskListReply renders mixed task types', () => { const text = formatUnifiedTaskListReply([ { type: 'automation', title: '每日新闻页', trigger: { repeat: 'daily', hour: 6, minute: 0 }, nextRunAt: null, }, { type: 'reminder', title: '项目计划例会', trigger: { at: 1000, timezone: 'Asia/Shanghai' }, nextRunAt: 1000, }, ]); assert.match(text, /任务一览/); assert.match(text, /每日新闻页/); assert.match(text, /项目计划例会/); }); function createMemoryPool() { const tasks = []; return { tasks, async query(sql, params = []) { if (sql.includes('INSERT INTO h5_tasks')) { const row = { id: params[0], user_id: params[1], type: params[2], title: params[3], spec_json: params[4], trigger_json: params[5], action_json: params[6], action_level: params[7], notify_channel: params[8], status: params[9], next_run_at: params[10], last_run_at: params[11], legacy_ref_json: params[12], source_channel: params[13], source_message_id: params[14], source_text: params[15], created_at: params[16], updated_at: params[17], }; tasks.push(row); return [{ affectedRows: 1 }]; } if (sql.includes('SELECT id FROM h5_tasks') && sql.includes('legacy_ref_json')) { const ref = params[1]; const hit = tasks.find((row) => row.user_id === params[0] && row.legacy_ref_json === ref); return [[hit].filter(Boolean)]; } if (sql.includes('SELECT * FROM h5_tasks WHERE id = ?')) { return [[tasks.find((row) => row.id === params[0])].filter(Boolean)]; } if (sql.includes('SELECT * FROM h5_tasks') && sql.includes('user_id = ?')) { return [tasks.filter((row) => row.user_id === params[0] && row.status === params[1])]; } if (sql.includes('FROM h5_scheduled_tasks')) return [[]]; if (sql.includes('FROM h5_schedule_reminders')) return [[]]; if (sql.includes('FROM h5_schedule_digest_subscriptions')) return [[]]; throw new Error(`Unexpected SQL: ${sql}`); }, }; } test('syncFromCommit inserts unified task row', async () => { const pool = createMemoryPool(); const service = createTaskUnifiedService(pool, { clock: { now: () => 3000 } }); const row = await service.syncFromCommit({ userId: 'user-1', kind: 'create_todo', committed: { item: { id: 'item-1', title: '跟进合同', timezone: 'Asia/Shanghai' } }, }); assert.equal(row.type, 'todo'); assert.equal(row.title, '跟进合同'); assert.equal(pool.tasks.length, 1); }); test('listUserTasks merges stored and legacy without duplicates', async () => { const pool = createMemoryPool(); pool.query = async (sql, params = []) => { if (sql.includes('INSERT INTO h5_tasks') || sql.includes('SELECT id FROM h5_tasks')) { return createMemoryPool().query(sql, params); } if (sql.includes('SELECT * FROM h5_tasks') && sql.includes('user_id = ?')) { return [[{ id: 'unified-1', user_id: 'user-1', type: 'automation', title: '每日新闻页', spec_json: '{}', trigger_json: '{"repeat":"daily","hour":6,"minute":0}', action_json: '{"kind":"agent_run"}', action_level: 2, notify_channel: 'both', status: 'active', next_run_at: 5000, last_run_at: null, legacy_ref_json: '{"table":"h5_scheduled_tasks","id":"task-1"}', source_channel: 'wechat', source_message_id: null, source_text: null, created_at: 1000, updated_at: 1000, }]]; } if (sql.includes('FROM h5_scheduled_tasks')) { return [[{ id: 'task-1', user_id: 'user-1', title: '每日新闻页', task_spec: 'news', recurrence: 'daily', hour: 6, minute: 0, weekday: null, timezone: 'Asia/Shanghai', next_run_at: 5000, status: 'active', source_channel: 'wechat', source_message_id: null, source_text: null, created_at: 1000, updated_at: 1000, }]]; } if (sql.includes('FROM h5_schedule_reminders')) return [[]]; if (sql.includes('FROM h5_schedule_digest_subscriptions')) return [[]]; if (sql.includes('FROM h5_balance_alert_subscriptions')) return [[]]; if (sql.includes('FROM h5_schedule_items') && sql.includes("kind = 'task'")) return [[]]; throw new Error(`Unexpected SQL: ${sql}`); }; const service = createTaskUnifiedService(pool); const tasks = await service.listUserTasks({ userId: 'user-1', limit: 10 }); assert.equal(tasks.length, 1); assert.equal(tasks[0].id, 'unified-1'); });