Skip to content

Commit 6428c54

Browse files
authored
test(ios-runner): pin production-built runner requests in a golden table TS and Swift both verify (#2900)
* test(apple-runner): pin production-built runner requests in a golden table Add contracts/fixtures/runner-requests.json: one entry per production request site, captured at its real entry point. Three producer tests own the entries: the platform-apple request sites, the runner-internal sites (real HTTP bodies from the fake runner), and the two recording modules. runner-contract.test.ts asserts every RunnerCommand has an entry, names are sorted and unique, and the drive code builds no request itself. Refs #2881 * test(apple-runner): verify the runner request golden table in Swift Every fixture request decodes as Command and re-encodes with the same key set, every CommandType case has a request, and every stored field of Command, SequenceStep and RunnerGesturePlan appears in some request. CommandType becomes CaseIterable for the case check. Refs #2881 * chore(gates): keep runner-requests.json entries on one line each Refs #2881 * test(apple-runner): pin the macOS desktopScroll pixels request * test(apple-runner): share the runner-requests fixture check with root tests Export the fixture reader as @agent-device/platform-apple/runner/requests-fixtures so root producers pin requests through the same commandId normalization, and derive the request-literal guard's sources from the producers the fixture declares. Refs #2881 * test(apple-runner): drive root recording requests from a dedicated golden file The request-literal guard scans every fixture producer, so root behavior tests that doubled as producers had to weaken their exact request assertions. Move the root recording drives into src/__tests__/screen-recording-runner-requests.test.ts, restore the original literals, and pin the recordStop sent without an app bundle. Producers must now be *runner-requests.test.ts files that call assertProducedRunnerRequests(import.meta.filename, ...), so a comment no longer satisfies the check. The runner-internal drive moves to runner/__tests__/runner-requests.test.ts to follow that rule. Refs #2881
1 parent 0fb1577 commit 6428c54

15 files changed

Lines changed: 833 additions & 64 deletions

‎.oxfmtrc.json‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
"node_modules/**",
1111
"**/*.md",
1212
"scripts/maestro-conformance/corpus/**",
13-
"fallow-baselines/**"
13+
"fallow-baselines/**",
14+
// One entry per line, so each pinned runner request changes as one reviewable diff line.
15+
"contracts/fixtures/runner-requests.json"
1416
]
1517
}

‎apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import AgentDeviceSnapshotPresentation
22

33
// MARK: - Wire Models
44

5-
enum CommandType: String, Codable {
5+
enum CommandType: String, Codable, CaseIterable {
66
case tap
77
case mouseClick
88
case longPress

‎apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+ModelsTests.swift‎

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,115 @@ extension RunnerTests {
3232
func runnerCommandFixture(_ json: String) throws -> Command {
3333
try JSONDecoder().decode(Command.self, from: Data(json.utf8))
3434
}
35+
36+
func testProductionRunnerRequestsDecodeWithoutDroppingAKey() throws {
37+
for (name, request) in try productionRunnerRequests() {
38+
let command = try decodeProductionRunnerRequest(request, name)
39+
XCTAssertEqual(command.command.rawValue, request["command"] as? String, name)
40+
let reencoded = try JSONSerialization.jsonObject(with: JSONEncoder().encode(command))
41+
XCTAssertEqual(runnerRequestKeyPaths(reencoded), runnerRequestKeyPaths(request), name)
42+
}
43+
}
44+
45+
func testEveryRunnerCommandTypeHasAProductionRequest() throws {
46+
let produced = Set(try productionRunnerRequests().compactMap { $0.request["command"] as? String })
47+
let orphaned = CommandType.allCases.map(\.rawValue).filter { !produced.contains($0) }
48+
XCTAssertEqual(orphaned, [], "CommandType cases with no production request")
49+
}
50+
51+
func testEveryRunnerRequestFieldHasAProductionRequest() throws {
52+
let entries = try productionRunnerRequests()
53+
let requests = entries.map(\.request)
54+
let steps = requests.flatMap { $0["steps"] as? [[String: Any]] ?? [] }
55+
let plans = requests.compactMap { $0["gesturePlan"] as? [String: Any] }
56+
let pointers = plans.flatMap { $0["pointers"] as? [[String: Any]] ?? [] }
57+
let samples = pointers.flatMap { $0["samples"] as? [[String: Any]] ?? [] }
58+
let commands = try entries.map { try decodeProductionRunnerRequest($0.request, $0.name) }
59+
let command = try XCTUnwrap(commands.first, "no production request")
60+
let plan = try XCTUnwrap(
61+
commands.compactMap(\.gesturePlan).first,
62+
"no production request carries a gesturePlan"
63+
)
64+
let step = try JSONDecoder().decode(SequenceStep.self, from: Data(#"{"kind":"tap"}"#.utf8))
65+
let sample = try XCTUnwrap(plan.pointers.first?.samples.first)
66+
assertEveryStoredField(of: command, appearsIn: requests, "Command")
67+
assertEveryStoredField(of: step, appearsIn: steps, "SequenceStep")
68+
assertEveryStoredField(of: plan, appearsIn: plans, "RunnerGesturePlan")
69+
assertEveryStoredField(
70+
of: plan.viewport,
71+
appearsIn: plans.compactMap { $0["viewport"] as? [String: Any] },
72+
"RunnerGestureViewport"
73+
)
74+
assertEveryStoredField(of: plan.pointers[0], appearsIn: pointers, "RunnerGesturePointer")
75+
assertEveryStoredField(of: sample, appearsIn: samples, "RunnerGestureSample")
76+
assertEveryStoredField(
77+
of: sample.point,
78+
appearsIn: samples.compactMap { $0["point"] as? [String: Any] },
79+
"RunnerGesturePoint"
80+
)
81+
}
82+
83+
private func productionRunnerRequests() throws -> [(name: String, request: [String: Any])] {
84+
let fixtureURL = URL(fileURLWithPath: #filePath)
85+
.deletingLastPathComponent()
86+
.deletingLastPathComponent()
87+
.deletingLastPathComponent()
88+
.deletingLastPathComponent()
89+
.deletingLastPathComponent()
90+
.deletingLastPathComponent()
91+
.appendingPathComponent("contracts/fixtures/runner-requests.json")
92+
let entries = try XCTUnwrap(
93+
JSONSerialization.jsonObject(with: Data(contentsOf: fixtureURL)) as? [[String: Any]]
94+
)
95+
return try entries.map { entry in
96+
(
97+
name: try XCTUnwrap(entry["name"] as? String),
98+
request: try XCTUnwrap(entry["request"] as? [String: Any], "\(entry["name"] ?? "?")")
99+
)
100+
}
101+
}
102+
103+
private func decodeProductionRunnerRequest(_ request: [String: Any], _ name: String) throws
104+
-> Command
105+
{
106+
do {
107+
return try JSONDecoder().decode(
108+
Command.self,
109+
from: JSONSerialization.data(withJSONObject: request)
110+
)
111+
} catch {
112+
XCTFail("\(name) does not decode as Command: \(error)")
113+
throw error
114+
}
115+
}
116+
117+
private func runnerRequestKeyPaths(_ value: Any, _ prefix: String = "") -> Set<String> {
118+
if let object = value as? [String: Any] {
119+
return object.reduce(into: Set<String>()) { paths, field in
120+
paths.insert(prefix + field.key)
121+
paths.formUnion(runnerRequestKeyPaths(field.value, "\(prefix)\(field.key)."))
122+
}
123+
}
124+
if let array = value as? [Any] {
125+
return array.reduce(into: Set<String>()) { paths, element in
126+
paths.formUnion(runnerRequestKeyPaths(element, "\(prefix)[]."))
127+
}
128+
}
129+
return []
130+
}
131+
132+
private func assertEveryStoredField(
133+
of value: Any,
134+
appearsIn objects: [[String: Any]],
135+
_ level: String
136+
) {
137+
let fields = Set(Mirror(reflecting: value).children.compactMap(\.label))
138+
let produced = Set(objects.flatMap(\.keys))
139+
XCTAssertEqual(
140+
fields.subtracting(produced).sorted(),
141+
[],
142+
"\(level) fields with no production request"
143+
)
144+
}
35145
}
36146
#endif

‎apple/runner/RUNNER_PROTOCOL.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ The daemon probes `http://127.0.0.1:<port>/command` for simulator and desktop fl
1414
## Request Shape
1515

1616
Every request includes a `command` field. Additional fields depend on the command family.
17+
The request vocabulary is `contracts/fixtures/runner-requests.json`: the requests production builds.
1718

1819
Examples:
1920

0 commit comments

Comments
 (0)