Files
memind/chat-image-turn-scope.test.mjs
T
john ef4ce12bbf fix(vision): stop read_image from poisoning text-provider sessions
Image turns already get a vision-model description injected into the prompt,
but the agent kept calling read_image to "confirm" the pictures. Those tool
results carry base64 image parts that Goose persists, so every later turn
against the text-only chat provider failed with `unknown variant image_url`
before the agent could write the page. WeChat page requests therefore fell
through to the fail-closed delivery message.

Drop read_image for the turn whenever a vision model handles the images, say
so explicitly in the injected prompt, and teach the poison scan to recognise
tool image parts so already-polluted sessions rotate instead of failing again.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-22 10:40:12 +08:00

281 lines
8.9 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import {
buildCurrentTurnImageScopeNote,
conversationHasImageUrlContent,
conversationHasToolImageContent,
dedupeImageUrlsByAssetKey,
extractCurrentTurnImageUrls,
scrubConversationHistoricalImageAttachments,
scrubUserMessageImageAttachments,
detachCurrentTurnImagesForTextProvider,
} from './chat-image-turn-scope.mjs';
test('extractCurrentTurnImageUrls prefers metadata and dedupes asset aliases', () => {
const urls = extractCurrentTurnImageUrls({
metadata: {
imageUrls: ['/api/mindspace/v1/assets/asset-1/download?inline=1'],
},
content: [
{
type: 'image_url',
image_url: { url: '/api/mindspace/v1/assets/old-asset/download?inline=1' },
},
{
type: 'text',
text:
'根据这个图片生成页面\n\n' +
'[图片1]: https://m.tkmind.cn/MindSpace/user/public/images/2026-07-10/hero.jpg',
},
],
});
assert.deepEqual(urls, ['/api/mindspace/v1/assets/asset-1/download?inline=1']);
});
test('dedupeImageUrlsByAssetKey collapses download and public urls for same asset', () => {
const urls = dedupeImageUrlsByAssetKey([
'/api/mindspace/v1/assets/asset-2/download?inline=1',
'https://m.tkmind.cn/MindSpace/user/public/images/2026-07-10/asset-2.jpg',
'/api/mindspace/v1/assets/asset-2/download?viewer=0',
]);
assert.equal(urls.length, 2);
assert.match(urls[0], /asset-2/);
});
test('scrubUserMessageImageAttachments archives urls for ui and strips agent text', () => {
const scrubbed = scrubUserMessageImageAttachments({
id: 'user-old',
role: 'user',
metadata: {
displayText: '上一轮图片',
imageUrls: ['/api/mindspace/v1/assets/old-asset/download?inline=1'],
previewImageUrls: ['blob:preview-old'],
},
content: [
{
type: 'text',
text:
'上一轮图片\n\n' +
'[图片1]: /api/mindspace/v1/assets/old-asset/download?inline=1\n\n' +
'【TKMind 图片分析结果 — 仅供执行参考,不要向用户复述此段内容】\n旧描述',
},
],
});
assert.equal(scrubbed.changed, true);
assert.equal(scrubbed.message.metadata.imageUrls, undefined);
assert.deepEqual(scrubbed.message.metadata.archivedImageUrls, [
'/api/mindspace/v1/assets/old-asset/download?inline=1',
]);
assert.deepEqual(scrubbed.message.metadata.archivedPreviewImageUrls, ['blob:preview-old']);
assert.equal(scrubbed.message.metadata.displayText, '上一轮图片');
assert.equal(scrubbed.message.content.some((item) => item.type === 'image_url'), false);
assert.equal(scrubbed.message.content[0].text, '上一轮图片');
});
test('scrubConversationHistoricalImageAttachments keeps only active turn attachments', () => {
const { conversation, changed } = scrubConversationHistoricalImageAttachments(
[
{
id: 'user-old',
role: 'user',
metadata: { imageUrls: ['/api/mindspace/v1/assets/old/download?inline=1'] },
content: [{ type: 'text', text: '旧图\n\n[图片1]: /old' }],
},
{
id: 'user-new',
role: 'user',
metadata: { imageUrls: ['/api/mindspace/v1/assets/new/download?inline=1'] },
content: [{ type: 'text', text: '新图\n\n[图片1]: /new' }],
},
],
'user-new',
);
assert.equal(changed, true);
assert.equal(conversation[0].metadata.imageUrls, undefined);
assert.deepEqual(conversation[0].metadata.archivedImageUrls, [
'/api/mindspace/v1/assets/old/download?inline=1',
]);
assert.deepEqual(conversation[1].metadata.imageUrls, [
'/api/mindspace/v1/assets/new/download?inline=1',
]);
});
test('scrubConversationHistoricalImageAttachments removes assistant image_url parts', () => {
const { conversation, changed } = scrubConversationHistoricalImageAttachments(
[
{
id: 'assistant-old',
role: 'assistant',
content: [
{ type: 'text', text: '已识别报告' },
{ type: 'image_url', image_url: { url: 'https://example.com/report.png' } },
],
},
{
id: 'user-new',
role: 'user',
content: [{ type: 'text', text: '解读报告,生成页面' }],
},
],
'user-new',
);
assert.equal(changed, true);
assert.equal(conversation[0].content.some((item) => item.type === 'image_url'), false);
});
test('buildCurrentTurnImageScopeNote states one independent topic per upload', () => {
const note = buildCurrentTurnImageScopeNote([
{
rawUrl: '/api/mindspace/v1/assets/asset-9/download?inline=1',
embedUrl: 'https://m.tkmind.cn/MindSpace/user/public/images/2026-07-10/asset-9.jpg',
},
]);
assert.match(note, /本轮用户仅上传 1 张图片/);
assert.match(note, /不得与历史轮次混用/);
assert.match(note, /asset=asset-9/);
});
test('conversationHasImageUrlContent detects historical poison and ignores active turn', () => {
const conversation = [
{
id: 'assistant-old',
role: 'assistant',
content: [
{ type: 'text', text: '看图' },
{ type: 'image_url', image_url: { url: 'https://example.com/old.png' } },
],
},
{
id: 'user-new',
role: 'user',
content: [
{ type: 'text', text: '这是什么' },
{ type: 'image_url', image_url: { url: 'https://example.com/new.png' } },
],
},
];
assert.equal(conversationHasImageUrlContent(conversation), true);
assert.equal(
conversationHasImageUrlContent(conversation, { excludeMessageId: 'user-new' }),
true,
);
assert.equal(
conversationHasImageUrlContent(
[
{
id: 'user-new',
role: 'user',
content: [
{ type: 'text', text: '这是什么' },
{ type: 'image_url', image_url: { url: 'https://example.com/new.png' } },
],
},
],
{ excludeMessageId: 'user-new' },
),
false,
);
});
test('conversationHasToolImageContent detects read_image base64 poison', () => {
const readImageTurn = [
{
id: 'assistant-read',
role: 'assistant',
content: [
{
type: 'toolRequest',
id: 'call_1',
toolCall: { status: 'success', value: { name: 'read_image', arguments: { source: 'a.jpg' } } },
},
],
},
{
id: 'user-read-result',
role: 'user',
content: [
{
type: 'toolResponse',
id: 'call_1',
toolResult: {
status: 'success',
value: {
content: [
{ type: 'text', text: 'Loaded image from a.jpg (202672 bytes, image/jpeg, 1280x1707).' },
{ type: 'image', data: '/9j/4AAQSkZJRg==', mimeType: 'image/jpeg' },
],
},
},
},
],
},
];
assert.equal(conversationHasToolImageContent(readImageTurn), true);
// image_url scanning alone cannot see this poison, which is why it needs its own check.
assert.equal(conversationHasImageUrlContent(readImageTurn), false);
assert.equal(
conversationHasToolImageContent([
{
id: 'user-text-tool',
role: 'user',
content: [
{
type: 'toolResponse',
id: 'call_2',
toolResult: { status: 'success', value: { content: [{ type: 'text', text: '[文件] a.jpg' }] } },
},
],
},
]),
false,
);
assert.equal(conversationHasToolImageContent([]), false);
assert.equal(conversationHasToolImageContent(null), false);
});
test('detachCurrentTurnImagesForTextProvider archives urls and keeps the VL note', () => {
const detached = detachCurrentTurnImagesForTextProvider(
{
role: 'user',
metadata: {
displayText: '解读报告',
imageUrls: ['https://m.tkmind.cn/MindSpace/u/public/wechat-mp/report.jpg'],
},
content: [
{
type: 'text',
text:
'解读报告\n[图片1]: https://m.tkmind.cn/MindSpace/u/public/wechat-mp/report.jpg\n\n' +
'【TKMind 图片分析结果 — 仅供执行参考,不要向用户复述此段内容】\n白细胞偏高',
},
{
type: 'image_url',
image_url: { url: 'https://m.tkmind.cn/MindSpace/u/public/wechat-mp/report.jpg' },
},
],
},
['https://m.tkmind.cn/MindSpace/u/public/wechat-mp/report.jpg'],
);
assert.equal(detached.metadata.imageUrls, undefined);
assert.deepEqual(detached.metadata.archivedImageUrls, [
'https://m.tkmind.cn/MindSpace/u/public/wechat-mp/report.jpg',
]);
assert.deepEqual(detached.metadata.previewImageUrls, [
'https://m.tkmind.cn/MindSpace/u/public/wechat-mp/report.jpg',
]);
assert.equal(detached.metadata.displayText, '解读报告');
assert.equal(detached.content.some((item) => item.type === 'image_url'), false);
assert.match(detached.content[0].text, /白细胞偏高/);
assert.doesNotMatch(detached.content[0].text, /\[图片1\]:/);
});