Files
workphone-sdk/sdk/tests/test_forward_message_logic.mjs

170 lines
8.2 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
/**
* WP-WX-01 离线回归forwardMessage 文本转发真 Frida 分支逻辑
*
* 真机铁律:本测试**仅作离线回归**,不替代真机 Frida attach E2E。
* 它从 wechat_hook_v2.js 抽取真实 forwardMessage 源码(不复制、不漂移),
* 注入 mock 依赖,验证三条分支:
* 1) 文本类(type=1) → _sendMessageInternal 真发送成功 → 返回 text_resend不走 intent
* 2) 非文本 / DB miss → 降级 _intentAction标 non_text_or_db_miss不伪造成功
* 3) 真发送未验证(success:false) → 不假成功,降级 intent
* 4) 缺参 → 直接 error
*
* 运行node sdk/tests/test_forward_message_logic.mjs
*/
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const JS_PATH = path.join(__dirname, '..', 'agent', 'hook', 'wechat_hook_v2.js');
const src = readFileSync(JS_PATH, 'utf8');
// 抽取 forwardMessage 函数体(花括号配平),避免复制造成漂移
function extractFunc(name) {
const re = new RegExp(name + '\\s*:\\s*function\\s*\\([^)]*\\)\\s*\\{');
const m = re.exec(src);
if (!m) throw new Error('未找到函数 ' + name);
const bodyStart = src.indexOf('{', m.index + m[0].length - 1);
let depth = 0;
for (let i = bodyStart; i < src.length; i++) {
if (src[i] === '{') depth++;
else if (src[i] === '}') {
depth--;
if (depth === 0) {
// 返回 "function(params){ ... }" 形式
const argsStart = src.indexOf('(', m.index);
const argsEnd = src.indexOf(')', argsStart);
const args = src.slice(argsStart + 1, argsEnd);
return new Function(args, src.slice(bodyStart + 1, i));
}
}
}
throw new Error('花括号未配平');
}
let assertions = 0;
function assert(cond, msg) {
assertions++;
if (!cond) { console.error(' ✗ FAIL: ' + msg); process.exitCode = 1; }
else console.log(' ✓ ' + msg);
}
// ---- mock harness ----
let calls;
function resetMocks(dbRows, sendResult, revokeResult) {
calls = { execSQL: [], sendInternal: [], intent: [], event: [], revoke: [] };
globalThis.Java = { performNow: (fn) => fn() };
globalThis._execSQL = (sql) => { calls.execSQL.push(sql); return dbRows; };
globalThis._sendMessageInternal = (toId, content, type) => {
calls.sendInternal.push({ toId, content, type });
return sendResult;
};
globalThis._intentAction = (action, payload, evt, extra) => {
calls.intent.push({ action, payload, evt, extra });
return { success: false, action, _intent: true, extra };
};
// revokeMessage 已改走真 Frida RevokeMsgEventmock 其返回以验证诚实分支
globalThis._revokeMessageByRevokeMsgEvent = (msgSvrId, row) => {
calls.revoke.push({ msgSvrId, row });
return revokeResult || { success: false, error: 'revoke_frida_failed', msg_svr_id: msgSvrId, eligible: true };
};
globalThis.emitEvent = (evt, data) => { calls.event.push({ evt, data }); };
globalThis.String = String;
}
const forwardMessage = extractFunc('forwardMessage');
console.log('[WP-WX-01] forwardMessage 离线分支回归');
// 1) 文本类真发送成功
resetMocks([{ type: 1, content: '你好转发' }], { success: true, message_id: 'm123', verified: true });
let r = forwardMessage({ msg_svr_id: 's1', to_id: 'wxid_a' });
assert(r.success === true, '文本类转发成功');
assert(r.forward_kind === 'text_resend', '标记 text_resend');
assert(calls.sendInternal.length === 1 && calls.sendInternal[0].content === '你好转发', '走 _sendMessageInternal 真发送');
assert(calls.intent.length === 0, '成功路径不降级 intent');
assert(calls.event.length === 1 && calls.event[0].evt === 'message_forwarded', '发 message_forwarded 事件');
// 2) 非文本type=3 图片)→ intent 降级,不假成功
resetMocks([{ type: 3, content: '[图片]' }], { success: true });
r = forwardMessage({ msg_svr_id: 's2', to_id: 'wxid_b' });
assert(calls.sendInternal.length === 0, '非文本不调真发送');
assert(calls.intent.length === 1 && calls.intent[0].extra.kind === 'non_text_or_db_miss', '非文本降级 intent (non_text_or_db_miss)');
assert(r._intent === true && r.success === false, '降级 intent 不伪造成功');
// 3) DB miss无行→ intent 降级
resetMocks([], { success: true });
r = forwardMessage({ msg_svr_id: 's3', to_id: 'wxid_c' });
assert(calls.intent.length === 1, 'DB miss 降级 intent');
// 4) 文本但真发送未验证(success:false) → 不假成功,降级 intent
resetMocks([{ type: 1, content: 'x' }], { success: false, error: 'unverified' });
r = forwardMessage({ msg_svr_id: 's4', to_id: 'wxid_d' });
assert(calls.sendInternal.length === 1, '尝试真发送');
assert(calls.intent.length === 1, '真发送未验证 → 降级 intent不假成功');
// 5) 缺参
r = forwardMessage({ msg_svr_id: '', to_id: '' });
assert(r.success === false && /缺少/.test(r.error), '缺参直接 error');
// ---- forwardMultiple 批量转发 ----
console.log('[WP-WX-01] forwardMultiple 离线分支回归');
const forwardMultiple = extractFunc('forwardMultiple');
// 6) 全文本 → 全部真发送forwarded=2
resetMocks([{ type: 1, content: 'a' }], { success: true, message_id: 'm' });
r = forwardMultiple({ msg_svr_ids: ['s1', 's2'], to_id: 'wxid_x' });
assert(r.success === true && r.forwarded === 2 && r.fallback === 0, '全文本批量转发 forwarded=2');
assert(calls.sendInternal.length === 2, '逐条调真发送 2 次');
assert(calls.intent.length === 0, '有成功条不整体降级');
// 7) 全非文本 → forwarded=0 整体降级 intent不假成功
resetMocks([{ type: 3, content: '[图片]' }], { success: true });
r = forwardMultiple({ msg_svr_ids: ['s1', 's2'], to_id: 'wxid_y' });
assert(r._intent === true && r.success === false, '全非文本整体降级 intent 不假成功');
assert(calls.intent[0].extra.forwarded === 0, 'intent 标 forwarded=0');
// 8) 缺参
r = forwardMultiple({ msg_svr_ids: [], to_id: 'wxid_z' });
assert(r.success === false && /缺少/.test(r.error), 'forwardMultiple 缺参 error');
// ---- revokeMessage 撤回资格预检 ----
console.log('[WP-WX] revokeMessage 资格预检回归');
const revokeMessage = extractFunc('revokeMessage');
// 9) 消息不存在 → 诚实 msg_not_found不发 intent
resetMocks([], null);
r = revokeMessage({ msg_svr_id: 's1' });
assert(r.success === false && r.error_code === 'msg_not_found', '消息不存在→msg_not_found');
assert(calls.intent.length === 0, '不存在不发 intent 假成功');
// 10) 非本人发送 → not_own_message
resetMocks([{ isSend: 0, createTime: Date.now() }], null);
r = revokeMessage({ msg_svr_id: 's2' });
assert(r.success === false && r.error_code === 'not_own_message', '非本人→not_own_message');
// 11) 超 2 分钟窗口 → revoke_window_expired
resetMocks([{ isSend: 1, createTime: Date.now() - 200000 }], null);
r = revokeMessage({ msg_svr_id: 's3' });
assert(r.success === false && r.error_code === 'revoke_window_expired', '超窗→revoke_window_expired');
// 12) 资格通过(本人+窗口内) → 走真 Frida RevokeMsgEventDB 已确认→成功发 message_revoked
resetMocks([{ isSend: 1, createTime: Date.now() - 5000 }], null, { success: true, verified: true, msg_svr_id: 's4' });
r = revokeMessage({ msg_svr_id: 's4' });
assert(calls.revoke.length === 1, '资格通过→走真 Frida RevokeMsgEvent');
assert(r.success === true && r.verified === true, '真撤回 DB 确认→成功');
assert(calls.event.length === 1 && calls.event[0].evt === 'message_revoked', '发 message_revoked 事件');
// 12b) 资格通过但真 RPC 未 DB 确认 → 诚实失败,不假成功、不发事件
resetMocks([{ isSend: 1, createTime: Date.now() - 5000 }], null, { success: false, error: 'revoke_frida_failed', eligible: true });
r = revokeMessage({ msg_svr_id: 's4b' });
assert(calls.revoke.length === 1, '资格通过→尝试真 Frida 撤回');
assert(r.success === false && calls.event.length === 0, '真撤回未确认→诚实失败不发事件');
// 13) 缺参
r = revokeMessage({ msg_svr_id: '' });
assert(r.success === false && /缺少/.test(r.error), 'revokeMessage 缺参 error');
console.log(`\n[done] 断言 ${assertions}exit=${process.exitCode || 0}`);