d58dc2a251
Introduce Draft → Confirm → Commit flow for WeChat schedule intents behind feature flags, plus h5_tasks dual-write/read aggregation and rollout scripts so reminders and automations get explicit user confirmation before persisting. Co-authored-by: Cursor <cursoragent@cursor.com>
184 lines
6.0 KiB
JavaScript
184 lines
6.0 KiB
JavaScript
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');
|
|
});
|