126 lines
5.8 KiB
Python
126 lines
5.8 KiB
Python
"""Local SQLite -> desktop decoder -> gateway request, with no live services."""
|
||
import io
|
||
import json
|
||
import sqlite3
|
||
import tempfile
|
||
from pathlib import Path
|
||
import unittest
|
||
from unittest import mock
|
||
|
||
import ai_chat
|
||
from wxwork_db import WXWorkDB
|
||
from wxwork_message_browser import _content_label
|
||
|
||
|
||
def varint(value):
|
||
output = bytearray()
|
||
while value > 127:
|
||
output.append((value & 127) | 128)
|
||
value >>= 7
|
||
output.append(value)
|
||
return bytes(output)
|
||
|
||
|
||
def field(number, value):
|
||
if isinstance(value, str):
|
||
value = value.encode('utf-8')
|
||
return varint(number << 3 | 2) + varint(len(value)) + value
|
||
|
||
|
||
def text_record(text):
|
||
return field(1, b'\x08\x00' + field(2, field(1, text)))
|
||
|
||
|
||
class DesktopPlaintextFlowTest(unittest.TestCase):
|
||
def setUp(self):
|
||
temporary = tempfile.TemporaryDirectory()
|
||
self.addCleanup(temporary.cleanup)
|
||
self.root = Path(temporary.name)
|
||
path = self.root / '100' / 'Data'
|
||
path.mkdir(parents=True)
|
||
self.connection = sqlite3.connect(path / 'message.db')
|
||
self.addCleanup(self.connection.close)
|
||
self.connection.execute('CREATE TABLE message_table(sender_id TEXT, conversation_id TEXT, '
|
||
'content_type INTEGER, send_time REAL, content BLOB, server_id TEXT)')
|
||
|
||
def add(self, sender, kind, content):
|
||
index = self.connection.execute('SELECT count(*) FROM message_table').fetchone()[0] + 1
|
||
self.connection.execute('INSERT INTO message_table VALUES(?,?,?,?,?,?)',
|
||
(sender, 'M:200', kind, 1789000000 + index, content, str(index)))
|
||
self.connection.commit()
|
||
|
||
def reader(self):
|
||
reader = WXWorkDB(str(self.root), {}, str(self.root / 'cache'))
|
||
self.addCleanup(reader.close)
|
||
return reader
|
||
|
||
def test_raw_database_records_become_readable_before_gateway_transport(self):
|
||
self.add('200', 0, text_record('在'))
|
||
self.add('100', 0, text_record('收到'))
|
||
secret_reference = '*1*' + 'SyntheticMediaReferenceOnly' * 30
|
||
self.add('200', 529, field(2, '朋友圈分享') + field(4, field(5, '合成动态正文')
|
||
+ field(6, field(2, secret_reference))))
|
||
self.add('100', 0, text_record('请提供读数'))
|
||
self.add('200', 0, text_record('14.1'))
|
||
reader = self.reader()
|
||
context = reader.get_conversation_context(reader._conv_display_name('100', 'M:200'),
|
||
account='100', conv_id='M:200', limit=100)
|
||
self.assertEqual(context['messages'][0]['content'], '在')
|
||
self.assertEqual(context['last_message']['content'], '14.1')
|
||
self.assertIn('合成动态正文', context['text'])
|
||
self.assertNotIn(secret_reference, context['text'])
|
||
self.assertNotIn(text_record('14.1').hex(), context['text'])
|
||
requests = []
|
||
|
||
def urlopen(request, timeout=None):
|
||
requests.append(json.loads(request.data.decode('utf-8')))
|
||
return io.BytesIO(json.dumps({'reply': '合成测试完成'}).encode('utf-8'))
|
||
|
||
provider = ai_chat.Provider(kind='gateway', base_url='https://synthetic.invalid/v1/answer',
|
||
api_key='synthetic-token', model='synthetic-model')
|
||
with mock.patch('urllib.request.urlopen', side_effect=urlopen), \
|
||
mock.patch('backend_client.device_id', return_value='synthetic-device'), \
|
||
mock.patch('ai_chat._system_prompt', return_value='合成测试规则'), \
|
||
mock.patch('ai_chat._finalize_reply', side_effect=lambda value, *a: value):
|
||
ai_chat.call_ai_text(context['text'], [], provider)
|
||
payload = requests[0]
|
||
self.assertEqual(payload['customer_text'], '14.1')
|
||
self.assertIn('请提供读数', payload['messages'][-1]['content'])
|
||
self.assertNotIn(secret_reference, json.dumps(payload))
|
||
self.assertNotIn('【本轮', payload['customer_text'])
|
||
|
||
def test_browser_keeps_short_text_but_never_displays_image_transport_paths(self):
|
||
self.assertEqual(_content_label(text_record('?'), 0), '?')
|
||
self.assertEqual(_content_label('123456', 0), '123456')
|
||
raw = field(1, '/storage/emulated/0/Android/data/synthetic/cache/photo.jpg')
|
||
label = _content_label(raw, 3)
|
||
self.assertIn('图片', label)
|
||
self.assertNotIn('storage', label)
|
||
|
||
def test_unreadable_text_is_explicit_and_cannot_trigger_protocol_model_call(self):
|
||
self.add('200', 0, b'\xff\xfe\x00\x01')
|
||
message = self.reader().get_new_messages(0)[0]
|
||
self.assertEqual(message['content_status'], 'unsupported')
|
||
self.assertNotIn('fffe', message['content'])
|
||
try:
|
||
import protocol_engine
|
||
except ModuleNotFoundError:
|
||
return # The separate visual-only workspace has no protocol engine.
|
||
self.assertFalse(protocol_engine._is_text_message(message))
|
||
self.assertEqual(protocol_engine._unanswered_manual_reason({'messages': [message]}),
|
||
protocol_engine.MANUAL_DECODE_REASON)
|
||
|
||
def test_guard_payload_remains_a_guard_prompt(self):
|
||
sent = []
|
||
def urlopen(request, timeout=None):
|
||
sent.append(json.loads(request.data))
|
||
return io.BytesIO(b'{"reply":"ok"}')
|
||
prompt = '界面识别合成测试'
|
||
provider = ai_chat.Provider(kind='gateway', base_url='https://synthetic.invalid/v1/answer',
|
||
api_key='synthetic-token', model='synthetic-model')
|
||
with mock.patch('urllib.request.urlopen', side_effect=urlopen), \
|
||
mock.patch('backend_client.device_id', return_value='synthetic-device'):
|
||
ai_chat._gateway_completion([{'role': 'user', 'content': prompt}], [], provider, purpose='guard')
|
||
self.assertEqual(sent[0]['customer_text'], prompt)
|
||
self.assertEqual(sent[0]['purpose'], 'guard')
|