From 2b58cdc2c8101023797f43f7e879e7afd0969b9f Mon Sep 17 00:00:00 2001 From: john Date: Sat, 15 Aug 2026 09:19:24 +0800 Subject: [PATCH] test(wechat): simulate tang image report and follow-up edit flow Add a four-step webhook simulation covering reset, image ack, report page turn, and polluted-session rotation on content-edit follow-ups. Co-authored-by: Cursor --- wechat-mp.test.mjs | 178 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) diff --git a/wechat-mp.test.mjs b/wechat-mp.test.mjs index 76f3778..436a117 100644 --- a/wechat-mp.test.mjs +++ b/wechat-mp.test.mjs @@ -5658,6 +5658,184 @@ test('wechat mp rotates polluted image session before a content-edit follow-up', assert.equal(activeSessionId, submitCalls[0].sessionId); }); +test('wechat mp simulates tang image report page and desensitized follow-up', async () => { + const token = 'token'; + const timestamp = '1710000000'; + const nonce = 'nonce'; + const testUserId = 'test-user-tang-report-followup'; + const submitCalls = []; + const customerReplies = []; + let activeSessionId = 'session-1'; + let nextSessionId = 2; + let routeCleared = false; + let reportPageCompleted = false; + let reportSessionId = null; + const pollutedConversation = [ + { + id: 'assistant-report', + role: 'assistant', + metadata: { userVisible: true }, + content: [{ type: 'text', text: '已解读化验报告并生成页面链接。' }], + }, + { + id: 'assistant-image', + role: 'assistant', + metadata: { userVisible: false }, + content: [{ type: 'image_url', image_url: { url: 'https://example.com/report.png' } }], + }, + ]; + + const service = createBoundWechatService({ + token, + config: { + mediaAnalysisGrayUsers: [testUserId], + }, + userAuth: { + async findWechatUserByOpenid() { + return { userId: testUserId, status: 'active', nickname: '唐' }; + }, + async getWechatAgentRoute() { + if (routeCleared || !activeSessionId) return null; + return { agentSessionId: activeSessionId, status: 'active', updatedAt: Date.now() }; + }, + async upsertWechatAgentRoute({ agentSessionId }) { + activeSessionId = agentSessionId; + routeCleared = false; + }, + async clearWechatAgentRoute() { + routeCleared = true; + }, + }, + startAgentSession: async () => ({ id: `session-${nextSessionId++}` }), + sessionApiFetch: async (sessionId, pathname) => { + if (pathname === `/sessions/${sessionId}`) { + const conversation = + reportSessionId && sessionId === reportSessionId && reportPageCompleted + ? pollutedConversation + : []; + return new Response(JSON.stringify({ conversation }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + } + if (pathname === `/sessions/${sessionId}/events`) { + const isReportStream = submitCalls.length === 0; + const text = isReportStream + ? '报告页面已生成:https://m.tkmind.cn/MindSpace/test/public/report.html' + : '已按你的要求脱敏并补充报告细节。'; + return new Response( + [ + `data: {"type":"Message","message":{"id":"assistant-${isReportStream ? 'report' : 'edit'}","role":"assistant","metadata":{"userVisible":true},"content":[{"type":"text","text":"${text}"}]}}\n\n`, + 'data: {"type":"Finish","token_state":{"inputTokens":1,"outputTokens":2}}\n\n', + ].join(''), + { status: 200, headers: { 'Content-Type': 'text/event-stream' } }, + ); + } + if (pathname === '/agent/harness_remember' || pathname === '/agent/harness_bootstrap') { + return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } }); + } + throw new Error(`unexpected api path: ${sessionId} ${pathname}`); + }, + submitSessionReply: async (input) => { + submitCalls.push(input); + if (!reportSessionId) { + reportSessionId = input.sessionId; + reportPageCompleted = true; + } + assert.equal(input.options?.requireHistoricalImageIsolation, true); + return { ok: true }; + }, + wechatFetch: async (url, init = {}) => { + if (String(url).includes('/cgi-bin/stable_token')) { + return new Response(JSON.stringify({ access_token: 'access-1', expires_in: 7200 }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + } + if (String(url).includes('/cgi-bin/media/get')) { + return new Response(Buffer.from([0x89, 0x50, 0x4e, 0x47]), { + status: 200, + headers: { 'Content-Type': 'image/png' }, + }); + } + if (String(url).includes('/cgi-bin/message/custom/send')) { + customerReplies.push(JSON.parse(init.body ?? '{}')); + return new Response(JSON.stringify({ errcode: 0, errmsg: 'ok' }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + } + throw new Error(`unexpected wechat url: ${url}`); + }, + }); + + try { + const resetResult = await service.handleInboundMessage( + inboundXml({ msgType: 'text', content: '换新会话', extraFields: { MsgId: 'tang-1' } }), + { timestamp, nonce, signature: signatureFor(token, timestamp, nonce) }, + ); + if (resetResult.task) await resetResult.task; + assert.equal(submitCalls.length, 0); + + const imageResult = await service.handleInboundMessage( + inboundXml({ + msgType: 'image', + content: '', + extraFields: { + MsgId: 'tang-2', + MediaId: 'media-report', + PicUrl: 'https://wx.example.com/report.png', + }, + }), + { timestamp, nonce, signature: signatureFor(token, timestamp, nonce) }, + ); + assert.equal(submitCalls.length, 0); + assert.match(imageResult.body ?? '', /已收到图片/); + + const reportResult = await service.handleInboundMessage( + inboundXml({ + msgType: 'text', + content: '解读报告并生成页面', + extraFields: { MsgId: 'tang-3' }, + }), + { timestamp, nonce, signature: signatureFor(token, timestamp, nonce) }, + ); + await reportResult.task; + + const editResult = await service.handleInboundMessage( + inboundXml({ + msgType: 'text', + content: '帮我把真名脱敏,报告再详细点', + extraFields: { MsgId: 'tang-4' }, + }), + { timestamp, nonce, signature: signatureFor(token, timestamp, nonce) }, + ); + await editResult.task; + } finally { + fs.rmSync(path.join(process.cwd(), 'MindSpace', testUserId), { recursive: true, force: true }); + } + + assert.ok(submitCalls.length >= 2, `submitCalls=${submitCalls.length}`); + const firstReportSubmit = submitCalls.find( + (call) => Array.isArray(call.userMessage.metadata?.imageUrls) && call.userMessage.metadata.imageUrls.length > 0, + ); + assert.ok(firstReportSubmit, 'missing report submit with imageUrls'); + assert.equal(firstReportSubmit.sessionId, reportSessionId); + assert.notEqual(submitCalls.at(-1).sessionId, reportSessionId); + assert.ok(customerReplies.length >= 2); + const replyTexts = customerReplies.map((payload) => String(payload?.text?.content ?? '')); + for (const text of replyTexts) { + assert.doesNotMatch(text, /unknown variant [`']?image_url/i); + assert.doesNotMatch(text, /Ran into this error:/i); + } + assert.ok(replyTexts.some((text) => /脱敏/.test(text)), replyTexts.join(' | ')); + assert.ok( + replyTexts.some((text) => /report\.html/.test(text)) + || submitCalls.some((call) => Array.isArray(call.userMessage.metadata?.imageUrls)), + replyTexts.join(' | '), + ); +}); + test('wechat mp groups consecutive images for one follow-up and clears the consumed batch', async () => { const token = 'token'; const timestamp = '1710000000';