Files
sansyn-pc-extension/tests/test_reference_generation_contract.py
T

122 lines
7.1 KiB
Python
Raw Normal View History

import base64
import json
import sys
import unittest
from email import policy
from email.parser import BytesParser
from pathlib import Path
from unittest.mock import MagicMock, patch
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'server'))
import server as app
import ai_client
class ReferenceContractTest(unittest.TestCase):
def test_original_analysis_survives_rewritten_report_roles(self):
report = {
'facts': {'images': {'main': ['https://example.test/1.png', 'https://example.test/2.png']}},
'evidenceLedger': [
{'id': f'IMG_MAIN_{i:04d}', 'type': 'image', 'value': f'https://example.test/{i}.png', 'meta': {'group': 'main'}}
for i in (1, 2)
],
'experienceSolution': {'visualCommerce': {'items': [
{'imageEvidenceId': 'IMG_MAIN_0002', 'imageRole': '使用场景', 'visualSignals': ['四季通用']}
], 'evidenceDetail': []}},
'visualDecision': {
'imageRoles': [
{'imageEvidenceId': 'IMG_MAIN_0002', 'imageRole': '材质触感', 'visualSignals': ['面料细节'], 'businessMeaning': '面料特写'}
],
'evidenceDetail': [
{'imageEvidenceId': 'IMG_MAIN_0002', 'composition': '面料特写背景,中央信息卡片', 'textInfo': '面料细节'}
],
},
}
observation = report['visualDecision']['evidenceDetail'][0]
observation['textInfo'] = '面料细节' + '完整参考文字' * 60
observation['composition'] += '保持原始布局' * 100
observation['textLayout'] = {'position': '中央', 'alignment': 'left', 'lines': 3}
observation['displayRelations'] = {'visibleParts':[{'id':'a','description':'上层可见区域'},{'id':'b','description':'回折区域'}],
'relations':[{'type':'连接','from':'a','to':'b','observation':'左侧连续回折,不是独立叠放'}]}
role = report['visualDecision']['imageRoles'][0]
role['visualSignals'] = ['面料细节'] + ['完整分析' * 30] * 6
slot = app.build_generation_slots(report, {})[1]
self.assertEqual('材质触感', slot['referenceAnalysis']['theme'])
self.assertEqual(observation['textInfo'], slot['referenceTextInfo'])
self.assertEqual(observation['composition'], slot['referenceAnalysis']['composition'])
self.assertEqual(observation, slot['referenceAnalysis']['imageObservation'])
self.assertEqual(role, slot['referenceAnalysis']['imageRoleAnalysis'])
self.assertEqual(role['visualSignals'], slot['referenceAnalysis']['signals'])
self.assertEqual(observation['displayRelations'],slot['referenceAnalysis']['displayRelations'])
prompt = app.build_image_prompt(report, {}, slot=slot, variant_index=1,
product_reference_mode='uploaded_identity_collected_reference', match_reference_shooting=True)
self.assertIn('原始图片主题:材质触感', prompt)
self.assertIn('面料特写背景,中央信息卡片', prompt)
self.assertNotIn('唯一任务:', prompt)
self.assertIn(observation['textInfo'], prompt)
self.assertIn(observation['composition'], prompt)
self.assertIn('左侧连续回折,不是独立叠放',prompt)
def test_reference_download_uses_certificate_context_and_exact_bytes(self):
image = b'\x89PNG\r\n\x1a\nimage-content'
with patch.object(app, '_download_image', return_value=(image, 'image/png')):
data_url = app._reference_data_url('https://example.test/reference.png')
self.assertEqual(image, base64.b64decode(data_url.split(',', 1)[1]))
response = MagicMock()
response.__enter__.return_value = response
response.read.return_value = image
response.headers.get.return_value = 'image/png'
with patch.object(app, '_ssl_context', return_value='trusted-context'), \
patch.object(app, 'load_config', return_value={}), \
patch.object(app.urllib.request, 'urlopen', return_value=response) as open_url:
self.assertEqual(image, app._download_image('https://example.test/reference.png')[0])
self.assertEqual('trusted-context', open_url.call_args.kwargs['context'])
def test_apiyi_request_contains_ordered_image_files_and_complete_prompt(self):
images = [b'\x89PNG\r\n\x1a\nproduct-image', b'\x89PNG\r\n\x1a\ncollected-image']
refs = ['data:image/png;base64,' + base64.b64encode(data).decode() for data in images]
prompt = '图1提供产品身份;图2为展示母版。\n保持折叠状态和文字排版。'
config = {'api_base': 'https://api.apiyi.com/v1', 'api_key': 'test-key',
'image_model': 'gpt-image-2', 'image_path': '/images/generations',
'timeout': 10, 'retries': 0}
response = MagicMock()
response.__enter__.return_value = response
response.read.return_value = json.dumps({'data': [{'b64_json': 'result'}]}).encode()
with patch.object(ai_client, 'image_channel', return_value=config), \
patch.object(ai_client, '_ssl_context', return_value='trusted-context'), \
patch.object(ai_client.urllib.request, 'urlopen', return_value=response) as open_url:
result = ai_client.image_generate(prompt, model='gpt-image-2', size='1024x1024', quality='high', reference_images=refs)
self.assertEqual('result', result['data'][0]['b64_json'])
request = open_url.call_args.args[0]
self.assertEqual('https://api.apiyi.com/v1/images/edits', request.full_url)
content_type = request.get_header('Content-type')
message = BytesParser(policy=policy.default).parsebytes(
('Content-Type: ' + content_type + '\r\nMIME-Version: 1.0\r\n\r\n').encode() + request.data)
fields = {}
files = []
for part in message.iter_parts():
name = part.get_param('name', header='content-disposition')
data = part.get_payload(decode=True)
if name == 'image[]':
files.append(data)
else:
fields[name] = data.decode('utf-8')
self.assertEqual(images, files)
self.assertEqual(prompt, fields['prompt'])
self.assertEqual({'model': 'gpt-image-2', 'size': '1024x1024', 'quality': 'high', 'n': '1'},
{key: value for key, value in fields.items() if key != 'prompt'})
def test_invalid_reference_never_sends_request(self):
config = {'api_base': 'https://api.apiyi.com/v1', 'api_key': 'test-key',
'image_model': 'gpt-image-2', 'image_path': '/images/generations',
'timeout': 10, 'retries': 0}
with patch.object(ai_client, 'image_channel', return_value=config), \
patch.object(ai_client.urllib.request, 'urlopen') as open_url:
with self.assertRaises(RuntimeError):
ai_client.image_generate('保留展示状态', reference_images=['data:image/png;base64,aW52YWxpZA=='])
open_url.assert_not_called()
if __name__ == '__main__':
unittest.main()