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>
122 lines
4.3 KiB
JavaScript
122 lines
4.3 KiB
JavaScript
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' } });
|
|
});
|