mirror of
https://github.com/block/buzz.git
synced 2026-10-28 21:11:48 +00:00
## Summary Solo compose now opens before agent discovery finishes, and chat creation avoids full channel-list reloads while overlapping independent reads. Durable retries, ownership checks, and relay confirmation remain intact. The agent picker uses the owner's saved inventory, excludes known archived identities, verifies profiles off the UI thread, and reuses avatar artwork. ### Related issue No matching issue found. Related: #8546 adds project selection to the same compose surface. ### Testing - Full mobile suite: 3,755 passed, 19 skipped; four additional artifact tests passed. - Analysis, formatting, file-size checks, and iOS Release build passed. - Regression tests fail when the old blocking paths are restored. - Installed on a physical iPhone with existing profile/community settings preserved. Owner tested the earlier creation/picker improvements; final compose timing awaits device feedback. No timing recording captured. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
174 lines
9.0 KiB
Swift
174 lines
9.0 KiB
Swift
import Flutter
|
|
import UIKit
|
|
|
|
@main
|
|
struct MeAgentAccessibilityTests {
|
|
@MainActor static func main() {
|
|
for locked in [false, true] {
|
|
for empty in [false, true] {
|
|
let savedName = "Saved agent"
|
|
var submitted: [String: String]?
|
|
let controller = NativeProfileTextEditorViewController(
|
|
title: "New chat", initialValue: "Chat", placeholder: "Chat name",
|
|
multiline: false, pageBackgroundColor: .systemGroupedBackground,
|
|
containerBackgroundColor: .secondarySystemGroupedBackground,
|
|
containerCornerRadius: 16, allowUnchangedSubmission: false,
|
|
channelDescription: nil, originalName: nil, originalDescription: nil,
|
|
canEditDetails: true, canEditCanvas: false, canvasContent: "",
|
|
canvasLoaded: false,
|
|
chatOptions: ["selectedAssistant": "agent", "savedAssistant": ["id": "agent", "name": savedName], "presentationId": "test", "assistantsLoading": empty, "fieldsLocked": locked, "assistants": empty ? [] : [["id": "agent", "name": "Agent"]]],
|
|
onCancel: {}, onSet: { submitted = $0 as? [String: String] }
|
|
)
|
|
controller.loadViewIfNeeded()
|
|
precondition(controller.numberOfSections(in: controller.tableView) == 3)
|
|
let projectCell = controller.tableView(controller.tableView, cellForRowAt: IndexPath(row: 0, section: 1))
|
|
let project = projectCell.accessoryView as! UIButton
|
|
precondition(project.accessibilityLabel == "Project")
|
|
precondition(project.accessibilityValue == "No project")
|
|
precondition(project.titleColor(for: .normal) == .label)
|
|
precondition(project.isEnabled == !locked)
|
|
precondition(project.showsMenuAsPrimaryAction && project.menu != nil)
|
|
let cell = controller.tableView(controller.tableView, cellForRowAt: IndexPath(row: 0, section: 2))
|
|
let content = cell.contentConfiguration as! UIListContentConfiguration
|
|
precondition(content.text == (empty ? (locked ? savedName : "Choose an agent") : "Agent"))
|
|
precondition((content.image != nil) == (locked || !empty))
|
|
let disabled = locked || empty
|
|
precondition(cell.accessibilityTraits.contains(.notEnabled) == disabled)
|
|
precondition(cell.accessibilityTraits.contains(.button) == !disabled)
|
|
precondition((cell.selectionStyle == .none) == disabled)
|
|
let canSubmit = locked || !empty
|
|
precondition(controller.navigationItem.rightBarButtonItem?.isEnabled == canSubmit)
|
|
// Invoke the action even when disabled to check its independent gate.
|
|
_ = controller.perform(NSSelectorFromString("setTapped"))
|
|
precondition(submitted?["assistant"] == (canSubmit ? "agent" : nil))
|
|
if empty && !locked {
|
|
controller.tableView.frame = CGRect(x: 0, y: 0, width: 390, height: 844)
|
|
controller.tableView.reloadData()
|
|
controller.tableView.layoutIfNeeded()
|
|
precondition(controller.tableView(controller.tableView, titleForFooterInSection: 1) == nil)
|
|
precondition(controller.tableView(controller.tableView, titleForFooterInSection: 2) == "Loading agents…")
|
|
controller.updateChatAssistants([
|
|
"presentationId": "test",
|
|
"assistants": [["id": "agent", "name": "Agent"]],
|
|
])
|
|
controller.tableView.layoutIfNeeded()
|
|
let updated = controller.tableView.cellForRow(at: IndexPath(row: 0, section: 2))!
|
|
precondition(updated.accessibilityTraits.contains(.button))
|
|
precondition(!updated.accessibilityTraits.contains(.notEnabled))
|
|
precondition(controller.tableView(controller.tableView, titleForFooterInSection: 2) == nil)
|
|
}
|
|
submitted = nil
|
|
controller.updateChatAssistants([
|
|
"presentationId": "test",
|
|
"assistants": empty ? [] : [["id": "agent", "name": "Agent"]],
|
|
])
|
|
let refreshedCell = controller.tableView(controller.tableView, cellForRowAt: IndexPath(row: 0, section: 2))
|
|
let refreshed = refreshedCell.contentConfiguration as! UIListContentConfiguration
|
|
precondition(refreshed.text == (empty ? (locked ? savedName : "Choose an agent") : "Agent"))
|
|
precondition(controller.navigationItem.rightBarButtonItem?.isEnabled == canSubmit)
|
|
_ = controller.perform(NSSelectorFromString("setTapped"))
|
|
precondition(submitted?["assistant"] == (canSubmit ? "agent" : nil))
|
|
}
|
|
}
|
|
avatarTests()
|
|
print("Me agent accessibility and bounded avatar loading passed")
|
|
}
|
|
}
|
|
|
|
private final class AvatarProtocol: URLProtocol {
|
|
static let lock = NSLock()
|
|
static var starts = 0
|
|
static var stops = 0
|
|
static var png = Data()
|
|
static func counts() -> (Int, Int) {
|
|
lock.lock(); defer { lock.unlock() }
|
|
return (starts, stops)
|
|
}
|
|
override class func canInit(with request: URLRequest) -> Bool { true }
|
|
override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }
|
|
override func startLoading() {
|
|
Self.lock.lock(); Self.starts += 1; Self.lock.unlock()
|
|
let path = request.url!.path
|
|
if path.hasPrefix("/hold") { return }
|
|
let headers = ["Content-Type": path == "/text" ? "text/html" : "image/png",
|
|
"Content-Length": path == "/large" ? "9000000" : "-1"]
|
|
client?.urlProtocol(self, didReceive: HTTPURLResponse(url: request.url!, statusCode: path == "/error" ? 404 : 200,
|
|
httpVersion: nil, headerFields: headers)!, cacheStoragePolicy: .notAllowed)
|
|
if path == "/chunked" {
|
|
for _ in 0..<3 { client?.urlProtocol(self, didLoad: Data(repeating: 0, count: 1024 * 1024)) }
|
|
} else {
|
|
client?.urlProtocol(self, didLoad: Self.png)
|
|
}
|
|
client?.urlProtocolDidFinishLoading(self)
|
|
}
|
|
override func stopLoading() {
|
|
Self.lock.lock(); Self.stops += 1; Self.lock.unlock()
|
|
}
|
|
}
|
|
|
|
@MainActor private func avatarTests() {
|
|
func pump(until predicate: () -> Bool) {
|
|
let deadline = Date().addingTimeInterval(4)
|
|
while !predicate() && Date() < deadline {
|
|
RunLoop.main.run(until: Date().addingTimeInterval(0.01))
|
|
}
|
|
precondition(predicate(), "Avatar operation timed out")
|
|
}
|
|
let config = URLSessionConfiguration.ephemeral
|
|
config.protocolClasses = [AvatarProtocol.self]
|
|
let loader = MeAvatarLoader(configuration: config)
|
|
let format = UIGraphicsImageRendererFormat()
|
|
format.scale = 1
|
|
AvatarProtocol.png = UIGraphicsImageRenderer(size: CGSize(width: 240, height: 240), format: format).pngData { _ in
|
|
UIColor.red.setFill(); UIRectFill(CGRect(x: 0, y: 0, width: 240, height: 240))
|
|
}
|
|
// No eager request on presenting even a thousand agents; only visible rows load.
|
|
let controller = MeAssistantListController(assistants: (0..<1000).map {
|
|
["id": "agent-\($0)", "name": "Agent \($0)", "avatarUrl": "https://avatar.test/hold/\($0)"]
|
|
}, selected: nil, pageColor: .white, rowColor: .white, avatars: loader, onSelect: { _, _ in })
|
|
controller.loadViewIfNeeded()
|
|
precondition(AvatarProtocol.counts().0 == 0)
|
|
for row in 0..<20 {
|
|
let path = IndexPath(row: row, section: 0)
|
|
let cell = controller.tableView(controller.tableView, cellForRowAt: path)
|
|
controller.tableView(controller.tableView, willDisplay: cell, forRowAt: path)
|
|
}
|
|
pump { AvatarProtocol.counts().0 == MeAvatarLoader.concurrency }
|
|
controller.viewDidDisappear(false)
|
|
pump { AvatarProtocol.counts().1 == MeAvatarLoader.concurrency }
|
|
precondition(AvatarProtocol.counts().0 == MeAvatarLoader.concurrency)
|
|
|
|
for path in ["large", "chunked", "error", "text", "ok"] {
|
|
var done = false
|
|
var result: UIImage?
|
|
loader.load(URL(string: "https://avatar.test/\(path)")!) { image in
|
|
result = image; done = true
|
|
}
|
|
pump { done }
|
|
precondition((result != nil) == (path == "ok"))
|
|
if let result { precondition(max(result.size.width, result.size.height) <= 120) }
|
|
}
|
|
let image = loader.image(for: URL(string: "https://avatar.test/ok"))
|
|
precondition(image != nil)
|
|
let rendered = MeAssistantListController.avatarImage(["name": "Agent"], photo: image)
|
|
precondition(rendered.size == CGSize(width: 40, height: 40))
|
|
let visibleLoader = MeAvatarLoader(configuration: config)
|
|
let visible = MeAssistantListController(assistants: [["id": "visible", "name": "Visible", "avatarUrl": "https://avatar.test/visible"]],
|
|
selected: nil, pageColor: .white, rowColor: .white, avatars: visibleLoader, onSelect: { _, _ in })
|
|
visible.loadViewIfNeeded()
|
|
visible.tableView.frame = CGRect(x: 0, y: 0, width: 320, height: 400)
|
|
visible.tableView.reloadData()
|
|
visible.tableView.layoutIfNeeded()
|
|
pump { visibleLoader.image(for: URL(string: "https://avatar.test/visible")) != nil }
|
|
let cell = visible.tableView.cellForRow(at: IndexPath(row: 0, section: 0))!
|
|
let content = cell.contentConfiguration as! UIListContentConfiguration
|
|
precondition(content.image?.pngData() == MeAssistantListController.avatarImage(["name": "Visible"], photo: image).pngData())
|
|
visible.viewDidDisappear(false)
|
|
// Source metadata is inspected before ImageIO is allowed to decode large pixels.
|
|
autoreleasepool {
|
|
let huge = UIGraphicsImageRenderer(size: CGSize(width: 4100, height: 4100), format: format).pngData { _ in }
|
|
precondition(MeAvatarLoader.thumbnail(huge) == nil)
|
|
}
|
|
loader.cancelAll()
|
|
}
|