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>
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createIntentDraftService } from './intent-draft-service.mjs';
|
||||
|
||||
function createMemoryPool() {
|
||||
const rows = new Map();
|
||||
return {
|
||||
async query(sql, params = []) {
|
||||
if (sql.includes('UPDATE h5_intent_drafts') && sql.includes("status = 'expired'")) {
|
||||
return [{ affectedRows: 0 }];
|
||||
}
|
||||
if (sql.includes('UPDATE h5_intent_drafts') && sql.includes("status = 'cancelled'") && sql.includes("status = 'draft'")) {
|
||||
for (const row of rows.values()) {
|
||||
if (row.user_id === params[1] && row.status === 'draft') {
|
||||
row.status = 'cancelled';
|
||||
row.updated_at = params[0];
|
||||
}
|
||||
}
|
||||
return [{ affectedRows: 1 }];
|
||||
}
|
||||
if (sql.includes('INSERT INTO h5_intent_drafts')) {
|
||||
const id = params[0];
|
||||
rows.set(id, {
|
||||
id,
|
||||
user_id: params[1],
|
||||
layer: params[2],
|
||||
draft_type: params[3],
|
||||
action_level: params[4],
|
||||
title: params[5],
|
||||
payload_json: params[6],
|
||||
card_text: params[7],
|
||||
status: 'draft',
|
||||
source_channel: params[9],
|
||||
source_message_id: params[10],
|
||||
source_text: params[11],
|
||||
committed_ref_json: null,
|
||||
event_log_json: params[12],
|
||||
expires_at: params[13],
|
||||
created_at: params[14],
|
||||
updated_at: params[15],
|
||||
});
|
||||
return [{ affectedRows: 1 }];
|
||||
}
|
||||
if (sql.includes('SELECT * FROM h5_intent_drafts WHERE id = ? AND user_id = ?')) {
|
||||
const row = rows.get(params[0]);
|
||||
return [[row && row.user_id === params[1] ? row : undefined].filter(Boolean)];
|
||||
}
|
||||
if (sql.includes('SELECT * FROM h5_intent_drafts') && sql.includes('user_id = ?') && sql.includes("status = 'draft'")) {
|
||||
const userId = params[0];
|
||||
const draft = [...rows.values()]
|
||||
.filter((row) => row.user_id === userId && row.status === 'draft')
|
||||
.sort((a, b) => b.created_at - a.created_at)[0];
|
||||
return [[draft ?? undefined].filter(Boolean)];
|
||||
}
|
||||
if (sql.includes('SELECT * FROM h5_intent_drafts WHERE id = ?')) {
|
||||
const row = rows.get(params[0]);
|
||||
return [[row].filter(Boolean)];
|
||||
}
|
||||
if (sql.includes("SET status = 'cancelled'") && sql.includes('WHERE id = ?')) {
|
||||
const row = rows.get(params[2]);
|
||||
if (row) {
|
||||
row.status = 'cancelled';
|
||||
row.event_log_json = params[0];
|
||||
row.updated_at = params[1];
|
||||
}
|
||||
return [{ affectedRows: 1 }];
|
||||
}
|
||||
if (sql.includes("SET status = 'committed'")) {
|
||||
const row = rows.get(params[3]);
|
||||
if (row) {
|
||||
row.status = 'committed';
|
||||
row.committed_ref_json = params[0];
|
||||
row.event_log_json = params[1];
|
||||
row.updated_at = params[2];
|
||||
}
|
||||
return [{ affectedRows: 1 }];
|
||||
}
|
||||
throw new Error(`Unexpected SQL: ${sql}`);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('createDraft replaces previous pending draft for same user', async () => {
|
||||
const pool = createMemoryPool();
|
||||
const service = createIntentDraftService(pool, { clock: { now: () => 1000 }, ttlMs: 60000 });
|
||||
const first = await service.createDraft({
|
||||
userId: 'user-1',
|
||||
layer: 'L1',
|
||||
draftType: 'timed_reminder',
|
||||
title: '第一次',
|
||||
payload: { title: '第一次' },
|
||||
cardText: 'card-1',
|
||||
});
|
||||
const second = await service.createDraft({
|
||||
userId: 'user-1',
|
||||
layer: 'L1',
|
||||
draftType: 'timed_reminder',
|
||||
title: '第二次',
|
||||
payload: { title: '第二次' },
|
||||
cardText: 'card-2',
|
||||
});
|
||||
const pending = await service.getPendingDraft('user-1');
|
||||
assert.notEqual(first.id, second.id);
|
||||
assert.equal(pending.title, '第二次');
|
||||
});
|
||||
|
||||
test('markDraftCommitted stores committed ref', async () => {
|
||||
const pool = createMemoryPool();
|
||||
const service = createIntentDraftService(pool, { clock: { now: () => 2000 }, ttlMs: 60000 });
|
||||
const draft = await service.createDraft({
|
||||
userId: 'user-1',
|
||||
layer: 'L1',
|
||||
draftType: 'create_todo',
|
||||
title: '跟进合同',
|
||||
payload: { title: '跟进合同' },
|
||||
cardText: 'card',
|
||||
});
|
||||
const committed = await service.markDraftCommitted(draft.id, 'user-1', { kind: 'create_todo', item: { id: 'item-1' } });
|
||||
assert.equal(committed.status, 'committed');
|
||||
assert.deepEqual(committed.committedRef, { kind: 'create_todo', item: { id: 'item-1' } });
|
||||
});
|
||||
Reference in New Issue
Block a user