feat: support uploaded images in debug voice preview
Allow paste/drag temporary image assets so vision turns work without a live camera frame. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -139,6 +139,8 @@ class ConversationRecorderTest(unittest.IsolatedAsyncioTestCase):
|
||||
input_id="input_photo",
|
||||
timestamp="2026-08-05T10:00:00+08:00",
|
||||
mime_type="image/jpeg",
|
||||
content="帮我看看",
|
||||
source="uploaded_asset",
|
||||
)
|
||||
|
||||
self.assertTrue(session.committed)
|
||||
@@ -146,7 +148,9 @@ class ConversationRecorderTest(unittest.IsolatedAsyncioTestCase):
|
||||
message, artifact = session.added
|
||||
self.assertEqual(message.content_type, "image")
|
||||
self.assertEqual(message.role, "user")
|
||||
self.assertEqual(message.content, "帮我看看")
|
||||
self.assertEqual(message.extra["input_id"], "input_photo")
|
||||
self.assertEqual(message.extra["source"], "uploaded_asset")
|
||||
self.assertEqual(artifact.message_id, message.id)
|
||||
self.assertEqual(artifact.kind, "image")
|
||||
self.assertEqual(artifact.size_bytes, len(b"jpeg-data"))
|
||||
|
||||
58
backend/tests/test_input_assets.py
Normal file
58
backend/tests/test_input_assets.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from io import BytesIO
|
||||
from unittest.mock import patch
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from services.input_assets import consume_input_image, store_input_image
|
||||
|
||||
|
||||
def png_bytes(size: tuple[int, int] = (32, 24)) -> bytes:
|
||||
output = BytesIO()
|
||||
Image.new("RGBA", size, (20, 80, 160, 180)).save(output, format="PNG")
|
||||
return output.getvalue()
|
||||
|
||||
|
||||
class InputAssetTests(unittest.TestCase):
|
||||
def test_store_normalizes_and_consume_removes_temporary_object(self):
|
||||
with (
|
||||
patch("services.input_assets.put_object") as put_object,
|
||||
patch("services.input_assets.get_object") as get_object,
|
||||
patch("services.input_assets.delete_object") as delete_object,
|
||||
):
|
||||
stored = store_input_image(png_bytes())
|
||||
|
||||
key, normalized, mime_type = put_object.call_args.args
|
||||
self.assertTrue(key.startswith("conversation-inputs/"))
|
||||
self.assertEqual(mime_type, "image/jpeg")
|
||||
self.assertTrue(normalized.startswith(b"\xff\xd8"))
|
||||
self.assertEqual((stored.width, stored.height), (32, 24))
|
||||
self.assertEqual(stored.size_bytes, len(normalized))
|
||||
|
||||
get_object.return_value = normalized
|
||||
consumed = consume_input_image(stored.token)
|
||||
|
||||
self.assertEqual(consumed, normalized)
|
||||
get_object.assert_called_once_with(key)
|
||||
delete_object.assert_called_once_with(key)
|
||||
|
||||
def test_tampered_token_is_rejected_before_storage_read(self):
|
||||
with patch("services.input_assets.put_object"):
|
||||
stored = store_input_image(png_bytes())
|
||||
tampered = f"{stored.token}x"
|
||||
|
||||
with patch("services.input_assets.get_object") as get_object:
|
||||
with self.assertRaisesRegex(ValueError, "签名无效"):
|
||||
consume_input_image(tampered)
|
||||
|
||||
get_object.assert_not_called()
|
||||
|
||||
def test_non_image_is_rejected(self):
|
||||
with self.assertRaisesRegex(ValueError, "可识别的图片"):
|
||||
store_input_image(b"not an image")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -32,7 +32,7 @@ class UserInputParserTests(unittest.TestCase):
|
||||
self.assertIsNotNone(value)
|
||||
self.assertEqual(value.text, "帮我看看")
|
||||
self.assertTrue(value.has_camera_frame)
|
||||
self.assertEqual(value.transcript_text, "帮我看看")
|
||||
self.assertEqual(value.transcript_text, "")
|
||||
|
||||
def test_rejects_legacy_and_unsupported_image_sources(self):
|
||||
self.assertIsNone(parse_user_input({"type": "user-text", "text": "旧协议"}))
|
||||
@@ -46,14 +46,40 @@ class UserInputParserTests(unittest.TestCase):
|
||||
{
|
||||
"type": "input_image",
|
||||
"source": {
|
||||
"type": "uploaded_asset",
|
||||
"asset_id": "asset_1",
|
||||
"type": "remote_url",
|
||||
"url": "https://example.com/image.jpg",
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
def test_parses_uploaded_image_asset(self):
|
||||
value = parse_user_input(
|
||||
{
|
||||
"type": "user-input",
|
||||
"schema_version": 1,
|
||||
"input_id": "input_upload",
|
||||
"parts": [
|
||||
{"type": "input_text", "text": "这是什么?"},
|
||||
{
|
||||
"type": "input_image",
|
||||
"source": {
|
||||
"type": "uploaded_asset",
|
||||
"asset_token": "signed-token",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
self.assertIsNotNone(value)
|
||||
self.assertTrue(value.has_image)
|
||||
self.assertFalse(value.has_camera_frame)
|
||||
self.assertEqual(value.image_asset_token, "signed-token")
|
||||
self.assertEqual(value.prompt_text, "这是什么?")
|
||||
self.assertEqual(value.transcript_text, "")
|
||||
|
||||
def test_image_only_input_has_no_synthetic_chat_text(self):
|
||||
value = parse_user_input(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user