test(wechat): simulate tang image report and follow-up edit flow
Memind CI / Test, build, and release guards (push) Failing after 3m27s

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 <cursoragent@cursor.com>
This commit is contained in:
john
2026-08-15 09:19:24 +08:00
parent 9f7633be24
commit 2b58cdc2c8
+178
View File
@@ -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';