ios: support safe assistant inline images
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
Run with Python + aiohttp, then xcodebuild test -scheme SybilAgentUI.
|
||||
"""
|
||||
import asyncio
|
||||
import base64
|
||||
import datetime
|
||||
import json
|
||||
import hashlib
|
||||
@@ -23,6 +24,8 @@ reply_tasks = set()
|
||||
attachments = {}
|
||||
uploads = {}
|
||||
attachment_capability = True
|
||||
assistant_image_capability = False
|
||||
fail_image_download = False
|
||||
next_upload_error = None
|
||||
blocked_proxy = False
|
||||
transcript_comparison = False
|
||||
@@ -95,6 +98,7 @@ async def reply(content):
|
||||
|
||||
async def route(request):
|
||||
global read_cursor, cursor, busy, attachment_capability, next_upload_error, blocked_proxy, transcript_comparison, push_available, auth_required
|
||||
global assistant_image_capability, fail_image_download
|
||||
path = request.path
|
||||
log.append(dict(method=request.method, path=path, query=dict(request.query)))
|
||||
if path == '/control':
|
||||
@@ -106,6 +110,7 @@ async def route(request):
|
||||
reply_tasks.clear()
|
||||
messages.clear(); requests.clear(); attempts.clear(); cursor = 0; read_cursor = 0; busy = False
|
||||
attachment_capability = True; next_upload_error = None; blocked_proxy = False; transcript_comparison = False
|
||||
assistant_image_capability = False; fail_image_download = False
|
||||
push_installations.clear(); push_retired.clear(); push_requests.clear(); log.clear()
|
||||
push_available = True; auth_required = False
|
||||
for i in range(body.get('count', 3)):
|
||||
@@ -120,6 +125,16 @@ async def route(request):
|
||||
transcript_comparison = True
|
||||
elif action == 'capability':
|
||||
attachment_capability = body.get('enabled', True)
|
||||
assistant_image_capability = body.get('assistantImages', False)
|
||||
elif action == 'assistant-image':
|
||||
# Synthetic output supplied by XCTest, never a workspace file or provider.
|
||||
data = base64.b64decode(body['png'], validate=True)
|
||||
descriptor = dict(id='fixture-output-image', kind='image', filename='fixture-landscape.png',
|
||||
mimeType='image/png', sizeBytes=len(data), sha256=hashlib.sha256(data).hexdigest())
|
||||
attachments[descriptor['id']] = dict(descriptor=descriptor, data=data)
|
||||
fail_image_download = body.get('failFirstDownload', False)
|
||||
row = message(body.get('content', ''), info={'agentAttachments': [descriptor]})
|
||||
await broadcast(dict(type='messages', **page([row])))
|
||||
elif action == 'upload-error':
|
||||
next_upload_error = body
|
||||
elif action == 'blocked-proxy':
|
||||
@@ -190,6 +205,8 @@ async def route(request):
|
||||
descriptor = dict(id='fixture-root', title='Sybil Agent', type='persistent-agent', model='fixture', protocol='pi-room.v1', websocket='/v1/agent/ws', activity=activity())
|
||||
if attachment_capability:
|
||||
descriptor['capabilities'] = {'attachments': CAPABILITY}
|
||||
if assistant_image_capability:
|
||||
descriptor.setdefault('capabilities', {})['assistantImages'] = dict(version=1)
|
||||
return web.json_response(descriptor)
|
||||
if path == '/api/v1/agent/push/config':
|
||||
if not push_available:
|
||||
@@ -263,6 +280,9 @@ async def route(request):
|
||||
uploads[upload_id] = (identity, attachment_id)
|
||||
return web.json_response({'attachment': descriptor}, status=201)
|
||||
if path.startswith('/api/v1/agent/attachments/') and path.endswith('/content'):
|
||||
if fail_image_download:
|
||||
fail_image_download = False
|
||||
return attachment_error(503, 'fixture_retry', 'Synthetic image unavailable. Retry the download.')
|
||||
attachment_id = path.removeprefix('/api/v1/agent/attachments/').removesuffix('/content')
|
||||
stored = attachments.get(attachment_id)
|
||||
if stored is None:
|
||||
|
||||
@@ -65,6 +65,59 @@ final class AgentUITests: XCTestCase {
|
||||
return try XCTUnwrap(json["attachment"] as? [String: Any])
|
||||
}
|
||||
|
||||
func testAssistantImageOnlyLiveRetryAndHistoryWithoutInputCapability() async throws {
|
||||
app.terminate()
|
||||
try await control(["action": "reset", "count": 0])
|
||||
try await control(["action": "capability", "enabled": false, "assistantImages": true])
|
||||
try await control(["action": "push-available", "enabled": false])
|
||||
app.launch(); openAgent()
|
||||
XCTAssertFalse(app.buttons["agent-attach"].exists)
|
||||
let image = UIGraphicsImageRenderer(size: CGSize(width: 320, height: 180)).image { context in
|
||||
UIColor.systemTeal.setFill(); context.fill(CGRect(x: 0, y: 0, width: 320, height: 180))
|
||||
UIColor.systemYellow.setFill(); context.cgContext.fillEllipse(in: CGRect(x: 225, y: 25, width: 50, height: 50))
|
||||
UIColor.systemIndigo.setFill()
|
||||
context.cgContext.move(to: CGPoint(x: 0, y: 180)); context.cgContext.addLine(to: CGPoint(x: 120, y: 55))
|
||||
context.cgContext.addLine(to: CGPoint(x: 250, y: 180)); context.cgContext.fillPath()
|
||||
}
|
||||
try await control(["action": "reply", "content": "\n\n<img src='http://127.0.0.1:8799/blocked-html.png' alt='HTML landscape'>"])
|
||||
try await control(["action": "assistant-image", "content": "", "png": try XCTUnwrap(image.pngData()).base64EncodedString(), "failFirstDownload": true])
|
||||
XCTAssertTrue(app.staticTexts["fixture-landscape.png"].waitForExistence(timeout: 8))
|
||||
XCTAssertTrue(app.buttons["Retry download"].waitForExistence(timeout: 8))
|
||||
let imageFrame = app.otherElements["agent-image-frame"]
|
||||
XCTAssertTrue(imageFrame.exists)
|
||||
let placeholder = imageFrame.frame
|
||||
XCTAssertGreaterThan(placeholder.width, 0)
|
||||
XCTAssertEqual(placeholder.height, 180, accuracy: 1)
|
||||
app.buttons["Retry download"].tap()
|
||||
let preview = app.images["agent-image-preview"]
|
||||
XCTAssertTrue(preview.waitForExistence(timeout: 8))
|
||||
XCTAssertEqual(preview.label, "Image: fixture-landscape.png")
|
||||
XCTAssertLessThanOrEqual(preview.frame.height, 180)
|
||||
XCTAssertEqual(imageFrame.frame.width, placeholder.width, accuracy: 1)
|
||||
XCTAssertEqual(imageFrame.frame.height, placeholder.height, accuracy: 1)
|
||||
let loadedSize = imageFrame.frame.size
|
||||
XCTAssertTrue(app.staticTexts["Markdown landscape"].exists)
|
||||
XCTAssertTrue(app.staticTexts["HTML landscape"].exists)
|
||||
XCTAssertFalse(app.buttons.matching(NSPredicate(format: "label BEGINSWITH %@", "Copy workspace path:")).firstMatch.exists)
|
||||
capture("agent-assistant-image-live")
|
||||
|
||||
// Previously saved output remains visible when both capabilities disappear.
|
||||
try await control(["action": "capability", "enabled": false, "assistantImages": false])
|
||||
app.terminate(); app.launch(); openAgent()
|
||||
XCTAssertTrue(preview.waitForExistence(timeout: 8))
|
||||
XCTAssertEqual(imageFrame.frame.width, placeholder.width, accuracy: 1)
|
||||
XCTAssertEqual(imageFrame.frame.height, placeholder.height, accuracy: 1)
|
||||
print("Assistant image frame sizes: placeholder=\(placeholder.size), live=\(loadedSize), history=\(imageFrame.frame.size)")
|
||||
XCTAssertEqual(app.staticTexts.matching(NSPredicate(format: "label == %@", "fixture-landscape.png")).count, 1)
|
||||
XCTAssertFalse(app.buttons["agent-attach"].exists)
|
||||
capture("agent-assistant-image-history")
|
||||
let state = try await control(["action": "state"])
|
||||
let log = try XCTUnwrap(state["log"] as? [[String: Any]])
|
||||
XCTAssertFalse(log.contains { ($0["path"] as? String)?.hasPrefix("/blocked-") == true })
|
||||
XCTAssertGreaterThanOrEqual(log.filter { ($0["path"] as? String) == "/api/v1/agent/attachments/fixture-output-image/content" }.count, 3)
|
||||
XCTAssertTrue((state["requests"] as? [String: Any])?.isEmpty == true)
|
||||
}
|
||||
|
||||
func testAttachmentHistoryDownloadAndReload() async throws {
|
||||
let original = Data("Synthetic original text. <script>plain text only</script>".utf8)
|
||||
let file = try await uploadOriginal(name: "fixture-original.txt", mime: "text/plain", bytes: original)
|
||||
|
||||
Reference in New Issue
Block a user