Files
memind/scripts/verify-intent-transaction-layer.mjs
T
john e13df82021
Memind CI / Test, build, and release guards (push) Failing after 14m19s
fix(wechat): pass inbound messages to goose and gate side effects at MCP tools
Stop ITL regex from intercepting new messages before Agent execution; move
scheduled task and schedule write confirmation to MCP tool calls with draft commit on 确认.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 22:16:23 +08:00

267 lines
9.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* Phase A ITL 离线验证:分类 → Draft → Confirm → Commitmock 服务,无需 DB
*/
import assert from 'node:assert/strict';
import { classifyUserIntent } from '../intent-classifier.mjs';
import { handleWechatIntentTransaction } from '../wechat/handlers/intent-transaction.mjs';
import { isIntentTransactionEnabled } from '../intent-transaction-config.mjs';
import { attachUnifiedTaskSync } from '../task-unified-sync.mjs';
import { createTaskUnifiedService } from '../task-unified-service.mjs';
import { formatQueryGuardReply } from '../intent-query-guard.mjs';
const env = {
H5_INTENT_TRANSACTION_ENABLED: '1',
H5_UNIFIED_TASKS_ENABLED: '1',
H5_DEFAULT_TIMEZONE: 'Asia/Shanghai',
};
let passed = 0;
let failed = 0;
function pass(label, detail = '') {
passed += 1;
console.log(`${label}${detail ? `: ${detail}` : ''}`);
}
function fail(label, detail = '') {
failed += 1;
console.error(`${label}${detail ? `: ${detail}` : ''}`);
}
function createDraftStore() {
let pending = null;
return {
async getPendingDraft(userId) {
return pending?.userId === userId ? pending : null;
},
async createDraft(payload) {
pending = {
id: 'draft-verify-1',
status: 'draft',
...payload,
payload: payload.payload,
};
return pending;
},
async cancelDraft(id, userId) {
if (pending?.id === id && pending.userId === userId) pending = null;
return { id, status: 'cancelled' };
},
async markDraftCommitted(id, userId, committedRef) {
pending = { ...pending, id, userId, status: 'committed', committedRef };
return pending;
},
};
}
function createScheduleService() {
const calls = [];
return {
calls,
async createItem(payload) {
calls.push(['createItem', payload]);
return { id: 'item-verify-1', ...payload };
},
async createReminder(payload) {
calls.push(['createReminder', payload]);
return { id: 'reminder-verify-1', ...payload };
},
buildTodoDigestText() {
return '今天有 0 条待办。';
},
};
}
async function main() {
if (!isIntentTransactionEnabled(env)) {
fail('feature flag', 'H5_INTENT_TRANSACTION_ENABLED 应为 1');
process.exit(1);
}
pass('feature flag enabled');
const query = classifyUserIntent('有没有我的新闻定时任务');
if (query.layer === 'L0') pass('query guard routes inventory to L0');
else fail('query guard routes inventory to L0', JSON.stringify(query));
const bare = classifyUserIntent('设置提醒');
if (bare.layer === 'L1' && bare.clarify?.length) pass('bare reminder asks for slot fill');
else fail('bare reminder asks for slot fill', JSON.stringify(bare));
const cancel = classifyUserIntent('取消每日新闻任务');
if (cancel.action === 'cancel_scheduled_task') pass('cancel routes to manage action');
else fail('cancel routes to manage action', JSON.stringify(cancel));
const drafts = createDraftStore();
const scheduleService = createScheduleService();
const passthrough = await handleWechatIntentTransaction({
intent: { agentText: '下午2点半提醒我开项目计划例会', msgId: 'verify-1', msgType: 'text' },
user: { userId: 'verify-user' },
intentDraftService: drafts,
scheduleService,
env,
});
if (passthrough === null) pass('new messages pass through to goose');
else fail('new messages pass through to goose', passthrough);
await drafts.createDraft({
userId: 'verify-user',
layer: 'L1',
draftType: 'timed_reminder',
actionLevel: 1,
title: '项目计划例会',
payload: {
title: '项目计划例会',
remindLocal: '2026-08-28 14:30',
hour: 14,
minute: 30,
recurrence: 'once',
sourceChannel: 'agent',
},
cardText: '确认卡片',
});
const pending = await drafts.getPendingDraft('verify-user');
if (pending?.draftType === 'timed_reminder') pass('pending draft can be committed after tool gate');
else fail('pending draft can be committed after tool gate', JSON.stringify(pending));
const committed = await handleWechatIntentTransaction({
intent: { agentText: '确认', msgId: 'verify-2', msgType: 'text' },
user: { userId: 'verify-user' },
intentDraftService: drafts,
scheduleService,
env,
});
if (committed?.includes('已设置提醒') && scheduleService.calls.length === 2) {
pass('confirm commits reminder to schedule service');
} else {
fail('confirm commits reminder to schedule service', `${committed} calls=${scheduleService.calls.length}`);
}
const slotFill = await handleWechatIntentTransaction({
intent: { agentText: '设置提醒', msgId: 'verify-3', msgType: 'text' },
user: { userId: 'verify-user-2' },
intentDraftService: createDraftStore(),
scheduleService,
env,
});
if (slotFill === null) pass('incomplete requests pass through to goose');
else fail('incomplete requests pass through to goose', slotFill);
const pagePassthrough = await handleWechatIntentTransaction({
intent: { agentText: '我想帮我做一个页面,页面上每天自动更新天气', msgId: 'verify-page', msgType: 'text' },
user: { userId: 'verify-user-page' },
intentDraftService: createDraftStore(),
scheduleService,
env,
});
if (pagePassthrough === null) pass('page delivery with auto refresh passes through to goose');
else fail('page delivery with auto refresh passes through to goose', pagePassthrough);
const disabled = await handleWechatIntentTransaction({
intent: { agentText: '下午2点提醒我', msgId: 'verify-4', msgType: 'text' },
user: { userId: 'verify-user-3' },
intentDraftService: createDraftStore(),
scheduleService,
env: { H5_INTENT_TRANSACTION_ENABLED: '0' },
});
if (disabled === null) pass('returns null when feature disabled');
else fail('returns null when feature disabled', disabled);
const syncCalls = [];
const memoryPool = {
tasks: [],
async query(sql, params = []) {
if (sql.includes('JOIN h5_schedule_reminders')) {
return [[{
id: 'item-verify-2',
title: '项目计划例会',
start_at: 9999,
timezone: 'Asia/Shanghai',
source_channel: 'wechat',
source_message_id: null,
source_text: null,
reminder_id: 'reminder-verify-2',
remind_at: 9999,
channel: 'wechat',
reminder_status: 'pending',
}]];
}
if (sql.includes('INSERT INTO h5_tasks')) {
this.tasks.push({ legacy_ref_json: params[12], user_id: params[1] });
return [{ affectedRows: 1 }];
}
if (sql.includes('SELECT * FROM h5_tasks') && sql.includes('legacy_ref_json')) return [[]];
if (sql.includes('SELECT * FROM h5_tasks WHERE id = ?')) {
return [[{
id: params[0],
user_id: 'verify-user',
type: 'reminder',
title: '项目计划例会',
spec_json: '{}',
trigger_json: '{}',
action_json: '{}',
action_level: 1,
notify_channel: 'wechat',
status: 'active',
next_run_at: 1000,
last_run_at: null,
legacy_ref_json: '{"table":"h5_schedule_reminders","id":"reminder-verify-1"}',
source_channel: 'wechat',
source_message_id: null,
source_text: null,
created_at: 1,
updated_at: 1,
}]];
}
return [[]];
},
};
const unifiedService = createTaskUnifiedService(memoryPool, { clock: { now: () => 1234 } });
const wrappedSchedule = {
async createItem(payload) {
return { id: 'item-verify-2', userId: payload.userId, kind: 'event', title: payload.title, timezone: 'Asia/Shanghai', status: 'active' };
},
async createReminder(payload) {
return { id: 'reminder-verify-2', itemId: payload.itemId, remindAt: payload.remindAt, channel: 'wechat' };
},
};
attachUnifiedTaskSync({
scheduleService: wrappedSchedule,
scheduledTaskService: {},
taskUnifiedService: {
...unifiedService,
async syncFromCommit(payload) {
syncCalls.push(payload.kind);
return unifiedService.syncFromCommit(payload);
},
},
pool: memoryPool,
env,
});
await wrappedSchedule.createReminder({ userId: 'verify-user', itemId: 'item-verify-2', remindAt: 9999 });
if (syncCalls.includes('timed_reminder')) pass('schedule reminder path dual-writes via attachUnifiedTaskSync');
else fail('schedule reminder path dual-writes via attachUnifiedTaskSync', syncCalls.join(','));
const queryReply = await formatQueryGuardReply('有没有我的新闻定时任务', {
taskUnifiedService: {
async listUserTasks() {
return [{ type: 'automation', title: '每日新闻页', trigger: { repeat: 'daily', hour: 5, minute: 30 }, nextRunAt: 1000 }];
},
},
userId: 'verify-user',
});
if (queryReply.includes('任务一览') && queryReply.includes('每日新闻页')) {
pass('query guard uses unified task list');
} else {
fail('query guard uses unified task list', queryReply);
}
console.log(`\n${passed} passed, ${failed} failed`);
process.exit(failed > 0 ? 1 : 0);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});