Skip to content

Commit cc9df8a

Browse files
thymikeeclaude
andcommitted
refactor(ios-runner): restrict the tap-witness synthesized type route to the bare submit key
The daemon sends ordinary `type` text in append mode and only the bare "\n" with no text-entry mode, so the tap-witness synthesis gate now admits only that shape. The ordinary-text commit wait it guarded never ran in production and is deleted with its append-only outcome function and tests; the replacement commit wait keeps every helper it shares. Runner tests now send the daemon's request shapes: ordinary text in append mode asserting the final field value, and a separate bare "\n" test asserting the submission route. The touched command-level tests move to UnitTests/. Closes #2800 Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
1 parent 5be2568 commit cc9df8a

12 files changed

Lines changed: 293 additions & 501 deletions

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

Lines changed: 0 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -345,76 +345,6 @@ extension RunnerTests {
345345
#endif
346346

347347
#if os(iOS)
348-
func testTypeWithoutResolvedInputReturnsTypedFailureBeforeDispatchingText() throws {
349-
let command = try runnerCommandFixture(
350-
#"{"command":"type","commandId":"type-without-focus","text":"hello"}"#
351-
)
352-
353-
let response = executeTypeCommand(
354-
activeApp: XCUIApplication(bundleIdentifier: "com.example.agentdevice.missing-input"),
355-
command: command
356-
)
357-
358-
XCTAssertFalse(response.ok)
359-
XCTAssertEqual(response.error?.code, "TEXT_INPUT_NOT_FOCUSED")
360-
XCTAssertEqual(
361-
response.error?.hint,
362-
"Focus a visible text input, then retry type or fill. If the input is not exposed by accessibility, use a coordinate focus command before typing."
363-
)
364-
}
365-
366-
func testBareTypeUsesTappedInputWhenSoftwareKeyboardIsHidden() throws {
367-
// The fixture uses a real text responder with an empty input view to model hardware-keyboard input.
368-
app.launchArguments = ["--agent-device-text-entry-regression"]
369-
app.launch()
370-
defer {
371-
invalidateCachedTarget(reason: "unit_test_cleanup")
372-
app.terminate()
373-
}
374-
XCTAssertTrue(app.waitForExistence(timeout: appExistenceTimeout))
375-
376-
let textField = app.textFields["agent-device-hardware-keyboard-input"]
377-
XCTAssertTrue(textField.waitForExistence(timeout: appExistenceTimeout))
378-
let frame = textField.frame
379-
XCTAssertFalse(frame.isEmpty)
380-
381-
let tapCommand = try runnerCommandFixture(
382-
#"{"command":"tap","commandId":"tap-hardware-keyboard-input","selectorKey":"id","selectorValue":"agent-device-hardware-keyboard-input"}"#
383-
)
384-
let tapResponse = try executeOnMainPrepared(command: tapCommand, activeApp: app)
385-
XCTAssertTrue(tapResponse.ok, String(describing: tapResponse.error))
386-
// A precondition, not a product claim. The fixture's empty `inputView` is what keeps the
387-
// keyboard down, but nothing in this bundle owns the simulator's own keyboard settings, so an
388-
// ambient flip that raised one here would be an environment fact — and reporting it as a
389-
// failed assertion is what made this read as a product regression on unrelated PRs (#1874).
390-
try XCTSkipIf(
391-
isKeyboardVisible(app: app),
392-
"software keyboard is up: this simulator cannot exercise the hidden-keyboard responder path"
393-
)
394-
395-
let failureCountBefore = currentXCTestFailureCount()
396-
let typeCommand = try runnerCommandFixture(
397-
#"{"command":"type","commandId":"type-hardware-keyboard","text":"hardware-keyboard"}"#
398-
)
399-
let typeResponse = executeTypeCommand(activeApp: app, command: typeCommand)
400-
401-
XCTAssertTrue(typeResponse.ok, String(describing: typeResponse.error))
402-
XCTAssertFalse(didRecordXCTestFailure(since: failureCountBefore))
403-
XCTAssertEqual(typeResponse.data?.textEntryRoute, "synthesized-first-responder")
404-
XCTAssertEqual(String(describing: textField.value ?? ""), "hardware-keyboard")
405-
406-
let secondFailureCountBefore = currentXCTestFailureCount()
407-
let secondTypeCommand = try runnerCommandFixture(
408-
#"{"command":"type","commandId":"type-hardware-keyboard-again","text":"-again"}"#
409-
)
410-
let secondTypeResponse = executeTypeCommand(activeApp: app, command: secondTypeCommand)
411-
412-
XCTAssertFalse(secondTypeResponse.ok)
413-
XCTAssertEqual(secondTypeResponse.error?.code, "TEXT_INPUT_NOT_FOCUSED")
414-
XCTAssertFalse(didRecordXCTestFailure(since: secondFailureCountBefore))
415-
XCTAssertEqual(String(describing: textField.value ?? ""), "hardware-keyboard")
416-
}
417-
418348
// `waitForTextEntryReadiness`'s hardware-keyboard fallback returns early only on confirmed
419349
// focus (#1874), and `keyboardFocusConfirmed` reads that from the app-wide focus predicate this
420350
// bundle otherwise refuses to trust. Two XCTest facts it rests on, neither a repository
@@ -464,38 +394,6 @@ extension RunnerTests {
464394
"focus held by another element must read as a refusal, never as this element's focus"
465395
)
466396
}
467-
468-
func testBareDelayedTypeFailsWhenTappedInputDisappearsMidCommand() throws {
469-
app.launchArguments = [
470-
"--agent-device-text-entry-regression",
471-
"--agent-device-text-entry-disappear-after-input",
472-
]
473-
app.launch()
474-
defer {
475-
invalidateCachedTarget(reason: "unit_test_cleanup")
476-
app.terminate()
477-
}
478-
XCTAssertTrue(app.waitForExistence(timeout: appExistenceTimeout))
479-
480-
let textField = app.textFields["agent-device-hardware-keyboard-input"]
481-
XCTAssertTrue(textField.waitForExistence(timeout: appExistenceTimeout))
482-
let tapCommand = try runnerCommandFixture(
483-
#"{"command":"tap","commandId":"tap-disappearing-input","selectorKey":"id","selectorValue":"agent-device-hardware-keyboard-input"}"#
484-
)
485-
let tapResponse = try executeOnMainPrepared(command: tapCommand, activeApp: app)
486-
XCTAssertTrue(tapResponse.ok, String(describing: tapResponse.error))
487-
488-
let failureCountBefore = currentXCTestFailureCount()
489-
let typeCommand = try runnerCommandFixture(
490-
#"{"command":"type","commandId":"type-disappearing-input","text":"ab","delayMs":50}"#
491-
)
492-
let typeResponse = executeTypeCommand(activeApp: app, command: typeCommand)
493-
494-
XCTAssertFalse(didRecordXCTestFailure(since: failureCountBefore))
495-
XCTAssertFalse(typeResponse.ok)
496-
XCTAssertEqual(typeResponse.error?.code, "TEXT_INPUT_NOT_FOCUSED")
497-
XCTAssertFalse(textField.exists)
498-
}
499397
#endif
500398

501399
func testInjectedTapRecordedFailureGateIsTapOnlyAndCountGated() {

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

Lines changed: 17 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import XCTest
22

3-
// The synthesized text-entry commit wait, end to end: the deadline that bounds it, the two waits
4-
// that run it, the observation/pacing they poll through, and the value-free cadence line that path
5-
// is allowed to log. Split from RunnerTests+SynthesizedTextEntry.swift, which keeps the
6-
// private-XCTest synthesis boundary and the route policies; the pure outcome functions these wrap
7-
// stay there next to the rules they encode. Everything that touches the polled field value now
8-
// lives in this one file, which is the surface apple-runner-log-redaction.test.ts guards.
3+
// The synthesized replacement commit wait, end to end: the deadline that bounds it, the wait that
4+
// runs it, the observation/pacing it polls through, and the value-free cadence line that path is
5+
// allowed to log. RunnerTests+SynthesizedTextEntry.swift keeps the private-XCTest synthesis
6+
// boundary, the route policies, and the pure outcome function this wraps. Everything that touches
7+
// the polled field value lives in this one file, which is the surface
8+
// apple-runner-log-redaction.test.ts guards.
99
extension RunnerTests {
1010
/// One commit wait's running deadline.
1111
///
@@ -57,54 +57,17 @@ extension RunnerTests {
5757
}
5858
}
5959

60-
/// Blocks until the synthesized bare-type text is observable in the target field, so `type`
61-
/// cannot report ok while trailing characters are still uncommitted on a slow simulator.
62-
///
63-
/// Observation only. A stalled prefix cannot be told apart from a suffix still queued in the
64-
/// event stream, so re-synthesizing the difference risks committing it twice after the command
65-
/// already reported success (#1676 rejected exactly that repair). Reporting `.notObserved` is
66-
/// what the caller does instead: the partial value is the agent's to resolve, and a named
67-
/// failure beats a success that misdescribes the field. Text carrying a submit key is skipped
68-
/// outright: the app may clear or rewrite the field on submit, so `textBefore + typedText` is
69-
/// not the value to wait for.
70-
func awaitSynthesizedFirstResponderCommit(
71-
app: XCUIApplication,
72-
target: TextEntryTarget,
73-
textBefore: String?,
74-
typedText: String
75-
) -> SynthesizedTextCommitOutcome {
76-
guard let textBefore, !typedText.contains("\n"), !typedText.contains("\r") else {
77-
return .unobservable
78-
}
79-
let expectedText = textBefore + typedText
80-
let waitStartedAt = Date()
81-
NSLog("[DEBUG-1874] wait start expectedLen=%ld route=append", expectedText.count)
82-
let ingredients = synthesizedCommitPollingIngredients(app: app, target: target, expectedText: expectedText)
83-
let outcome = Self.awaitSynthesizedCommitOutcome(
84-
expectedText: expectedText,
85-
placeholder: ingredients.placeholder,
86-
now: { Date() },
87-
observe: ingredients.observe,
88-
waitForNextObservation: ingredients.waitForNextObservation
89-
)
90-
NSLog(
91-
"[DEBUG-1874] wait outcome=%@ elapsedMs=%.0f route=append",
92-
String(describing: outcome),
93-
waitStartedAt.timeIntervalSinceNow * -1000
94-
)
95-
return outcome
96-
}
97-
9860
/// Blocks until the synthesized replacement text (`fill`) is observable in the target field, so
9961
/// `fill` cannot report ok while the select-all-and-retype it posted is still uncommitted — or
10062
/// silently wrong — on a slow or channel-penalized simulator (this route runs only when the
101-
/// XCTest channel is already penalized, and never resolves an `XCUIElement`, so it previously had
102-
/// no verification at all).
63+
/// XCTest channel is already penalized, and never resolves an `XCUIElement`).
10364
///
104-
/// Unlike the append route, the expected value is the final text itself — replacement mode
105-
/// clears the field first, so there is no `textBefore` prefix to account for — and unlike the
106-
/// append route, a settled mismatch is always reported rather than trusted: see
107-
/// `awaitSynthesizedReplacementCommitOutcome`'s doc comment.
65+
/// Observation only. A stalled prefix cannot be told apart from a suffix still queued in the
66+
/// event stream, so re-synthesizing the difference risks committing it twice after the command
67+
/// already reported success. The expected value is the
68+
/// final text itself, and a settled mismatch is always reported: see
69+
/// `awaitSynthesizedReplacementCommitOutcome`'s doc comment. Text carrying a submit key is
70+
/// skipped outright: the app may clear or rewrite the field on submit.
10871
func awaitSynthesizedReplacementCommit(
10972
app: XCUIApplication,
11073
target: TextEntryTarget,
@@ -160,16 +123,9 @@ extension RunnerTests {
160123
)
161124
}
162125

163-
/// The placeholder/observe/pacing ingredients shared by the append route
164-
/// (`awaitSynthesizedFirstResponderCommit`) and the replacement route
165-
/// (`awaitSynthesizedReplacementCommit`). What must NOT be shared is which outcome function
166-
/// consumes them: see `awaitSynthesizedReplacementCommitOutcome`'s doc comment for why append
167-
/// mode's "trust a diverged value" rule is wrong for replacement mode. Each caller therefore
168-
/// calls its own named outcome function directly, with real argument labels — deliberately not
169-
/// a stored closure/function-value parameter here, which would erase those labels at the call
170-
/// site and make the observe closure unrecognizable to the static content-redaction check in
171-
/// `apple-runner-log-redaction.test.ts` (`extractObserveClosure` locates the labeled closure
172-
/// literal by its text; a closure passed as a plain function value carries no such label).
126+
/// The placeholder/observe/pacing ingredients the commit wait polls through. The observe closure
127+
/// stays a labeled closure literal here: the static content-redaction check in
128+
/// `apple-runner-log-redaction.test.ts` (`extractObserveClosure`) locates it by that text.
173129
private func synthesizedCommitPollingIngredients(
174130
app: XCUIApplication,
175131
target: TextEntryTarget,
@@ -189,7 +145,7 @@ extension RunnerTests {
189145
treatingPlaceholderAsEmpty: true
190146
)
191147
// Cadence evidence stays value-free: the polled value is user content typed through
192-
// `type`/`fill` and must never reach runner.log. Lengths and the expected-prefix walk
148+
// `fill` and must never reach runner.log. Lengths and the expected-prefix walk
193149
// are enough to distinguish throttling (prefix grows slowly) from a wedge (it freezes).
194150
Self.logCommitCadence(
195151
elapsedMs: Int(waitStartedAt.timeIntervalSinceNow * -1000),

0 commit comments

Comments
 (0)