Files
memind/task-unified-service.test.mjs
T
john d58dc2a251 feat(wechat): add Intent Transaction Layer with unified task schema
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>
2026-08-24 15:59:17 +08:00

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