fix: tune iOS runner response retention (#665)

* fix: tune iOS runner response retention

* fix: require explicit runner response retention
This commit is contained in:
Michał Pierzchała
2026-06-01 22:24:04 -05:00
committed by GitHub
parent 3ae2472d44
commit 7e30a83057
@@ -1,4 +1,5 @@
import Foundation
import XCTest
enum RunnerCommandLifecycleState: String {
case notAccepted
@@ -50,7 +51,7 @@ final class RunnerCommandJournal {
command: command,
state: response.ok ? .completed : .failed,
responseOk: response.ok,
responseJson: encodeResponseJson(response),
responseJson: encodeResponseJson(command: command, response: response),
error: response.error
)
}
@@ -131,10 +132,151 @@ final class RunnerCommandJournal {
return trimmed.isEmpty ? nil : trimmed
}
private func encodeResponseJson(_ response: Response) -> String? {
guard response.data?.nodes == nil else { return nil }
private func encodeResponseJson(command: Command, response: Response) -> String? {
guard shouldRetainResponseJson(command: command) else { return nil }
guard let data = try? JSONEncoder().encode(response) else { return nil }
guard data.count <= maxResponseJsonBytes else { return nil }
return String(data: data, encoding: .utf8)
}
private func shouldRetainResponseJson(command: Command) -> Bool {
switch command.command {
case .snapshot, .screenshot:
return false
case .tap, .mouseClick, .tapSeries, .longPress, .interactionFrame, .drag, .dragSeries,
.remotePress, .type, .swipe, .findText, .querySelector, .readText, .back, .backInApp,
.backSystem, .home, .rotate, .appSwitcher, .keyboardDismiss, .keyboardReturn, .alert,
.pinch, .rotateGesture, .transformGesture, .recordStart, .recordStop, .status, .uptime,
.shutdown:
return true
}
}
}
extension RunnerTests {
func testCommandJournalRetentionPolicy() throws {
let journal = RunnerCommandJournal()
let uptime = runnerJournalCommand("uptime", id: "small-scalar")
journal.accept(command: uptime)
journal.finish(
command: uptime,
response: Response(ok: true, data: DataPayload(currentUptimeMs: 12.5))
)
let scalarStatus = journal.status(commandId: "small-scalar")
XCTAssertEqual(scalarStatus.lifecycleState, RunnerCommandLifecycleState.completed.rawValue)
XCTAssertEqual(scalarStatus.lifecycleResponseOk, true)
XCTAssertNotNil(scalarStatus.lifecycleResponseJson)
let scalarResponse = try decodeRunnerJournalResponse(scalarStatus.lifecycleResponseJson)
XCTAssertEqual(scalarResponse.data?.currentUptimeMs, 12.5)
let querySelector = runnerJournalCommand("querySelector", id: "small-object")
journal.accept(command: querySelector)
journal.finish(
command: querySelector,
response: Response(ok: true, data: DataPayload(found: true, nodes: [runnerJournalNode()]))
)
let objectStatus = journal.status(commandId: "small-object")
XCTAssertNotNil(objectStatus.lifecycleResponseJson)
let objectResponse = try decodeRunnerJournalResponse(objectStatus.lifecycleResponseJson)
XCTAssertEqual(objectResponse.data?.found, true)
XCTAssertEqual(objectResponse.data?.nodes?.count, 1)
let snapshot = runnerJournalCommand("snapshot", id: "snapshot-tree")
journal.accept(command: snapshot)
journal.finish(
command: snapshot,
response: Response(ok: true, data: DataPayload(nodes: [runnerJournalNode()], truncated: false))
)
let snapshotStatus = journal.status(commandId: "snapshot-tree")
XCTAssertEqual(snapshotStatus.lifecycleState, RunnerCommandLifecycleState.completed.rawValue)
XCTAssertEqual(snapshotStatus.lifecycleResponseOk, true)
XCTAssertNil(snapshotStatus.lifecycleResponseJson)
let screenshot = runnerJournalCommand("screenshot", id: "screenshot-artifact")
journal.accept(command: screenshot)
journal.finish(
command: screenshot,
response: Response(ok: true, data: DataPayload(message: "tmp/screenshot-1.png"))
)
let screenshotStatus = journal.status(commandId: "screenshot-artifact")
XCTAssertEqual(screenshotStatus.lifecycleState, RunnerCommandLifecycleState.completed.rawValue)
XCTAssertEqual(screenshotStatus.lifecycleResponseOk, true)
XCTAssertNil(screenshotStatus.lifecycleResponseJson)
let largeRead = runnerJournalCommand("readText", id: "large-read")
journal.accept(command: largeRead)
journal.finish(
command: largeRead,
response: Response(ok: true, data: DataPayload(text: String(repeating: "x", count: 17 * 1024)))
)
let largeReadStatus = journal.status(commandId: "large-read")
XCTAssertEqual(largeReadStatus.lifecycleState, RunnerCommandLifecycleState.completed.rawValue)
XCTAssertEqual(largeReadStatus.lifecycleResponseOk, true)
XCTAssertNil(largeReadStatus.lifecycleResponseJson)
}
func testCommandJournalKeepsErrorMetadataWhenResponseJsonIsDropped() {
let journal = RunnerCommandJournal()
let snapshot = runnerJournalCommand("snapshot", id: "snapshot-error")
let hint = "Try a smaller read such as snapshot -s <visible label or id> -d 8."
journal.accept(command: snapshot)
journal.finish(
command: snapshot,
response: Response(
ok: false,
error: ErrorPayload(
code: "IOS_AX_SNAPSHOT_FAILED",
message: "iOS XCTest snapshot failed while serializing the accessibility tree.",
hint: hint
)
)
)
let status = journal.status(commandId: "snapshot-error")
XCTAssertEqual(status.lifecycleState, RunnerCommandLifecycleState.failed.rawValue)
XCTAssertEqual(status.lifecycleResponseOk, false)
XCTAssertNil(status.lifecycleResponseJson)
XCTAssertEqual(status.lifecycleErrorCode, "IOS_AX_SNAPSHOT_FAILED")
XCTAssertEqual(
status.lifecycleErrorMessage,
"iOS XCTest snapshot failed while serializing the accessibility tree."
)
XCTAssertEqual(status.lifecycleErrorHint, hint)
}
private func runnerJournalCommand(_ command: String, id: String) -> Command {
let json = #"{"command":"\#(command)","commandId":"\#(id)"}"#
return try! JSONDecoder().decode(Command.self, from: Data(json.utf8))
}
private func runnerJournalNode() -> SnapshotNode {
SnapshotNode(
index: 0,
type: "button",
label: "Continue",
identifier: "continue",
value: nil,
rect: SnapshotRect(x: 10, y: 20, width: 100, height: 44),
enabled: true,
focused: nil,
selected: nil,
hittable: true,
depth: 0,
parentIndex: nil,
hiddenContentAbove: nil,
hiddenContentBelow: nil
)
}
private func decodeRunnerJournalResponse(_ responseJson: String?) throws -> Response {
let responseJson = try XCTUnwrap(responseJson)
return try JSONDecoder().decode(Response.self, from: Data(responseJson.utf8))
}
}