Files
callstack__agent-device/.github/workflows/1874-diagnose.yml
Michał Pierzchała 437465f37b ci(1874): declare the diagnose lane and read its iterations honestly (#2059)
The loop that #1874 is investigated with could not tell the truth about itself.
It classified every non-`passed` iteration as a stall, which after #2035 gave the
looped test an XCTSkipIf meant an environment flip would report a 100% stall
rate; it captured cadence only for failures, though an absorbed episode now
passes; and it read its logs with shell pipelines whose exit status means "did
this match", so an iteration that legitimately matched nothing killed the job
before it could be summarized.

scripts/diagnose-1874-iteration.ts reads one iteration: xcodebuild's own verdict,
the `type-all` duration, and the cadence worth keeping. A nonzero exit outranks a
green measured test — in `pair` mode the neighbour or the runner can fail while
the measured test passes — and a run that produced no verdict is named as ours
rather than counted as a stall. The workflow gains the #1781 lane declaration it
never had. Its kill criterion names #2080, which the loop can now serve rather
than merely claim to: the looped test is a dispatch input, so the fill route that
#2080 traces loops the same way. One test pins the contract the script cannot
check about itself — that the workflow hands it the status xcodebuild returned
rather than a literal.

Closes #1874.

Both filed symptoms are resolved. `smoke:form-input` was root-caused and fixed in
#2035: the fixture's placeholder was identical to the value every suite filled,
so `fill` could never be verified on the penalized route — deterministic, not a
flake, and only visible under load because that route is gated on a penalized
XCTest channel. The targeted XCTest is mitigated by the progress-aware commit
budget, with 200 consecutive green loop iterations across two dispatches.

The issue's remaining question — why the input pipeline throttles — is answered
by the second dispatch, and the premise was wrong: it does not. Posting 17
characters took 484 ms and the commit was observed on the first poll, inside an
iteration whose `type-all` measured 14334 ms. The ~12.6 s went to accessibility
round-trips before any character was posted, which is #1105's path, not the
input pipeline's.
2026-08-28 08:33:46 +02:00

133 lines
5.1 KiB
YAML

name: 1874 Diagnose
# Under the lane rule in #1781:
#
# - Catches: iOS text-entry commit episodes on CI hardware — the dispatched test looped on both
# arches, with and without the neighbour test that shares the simulator. An absorbed episode
# surfaces as a slow pass, so the cadence is kept for passes too.
# - Evidence: the 2026-08-21 dispatch passed 50/50 and still caught an episode (`type-all`
# 2487 ms across 5 polls) that no PR lane would report; it does not reproduce locally.
# - Cost: manual dispatch and the `diagnose/1874-commit-stall` branch only — never a PR or `main`.
# One dispatch is four macOS slots, each a runner build plus `iterations` iterations of
# simulator time.
# - Kill criterion: delete when #2080 closes. It is the last open text-entry question this loop
# is the CI reproduction path for, and nothing else reads these artifacts.
on:
workflow_dispatch:
inputs:
iterations:
description: 'Loop iterations per job'
required: false
default: '25'
test:
description: 'RunnerTests method to loop'
required: false
default: testBareTypeUsesTappedInputWhenSoftwareKeyboardIsHidden
push:
branches:
- diagnose/1874-commit-stall
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
stall-loop:
name: ${{ matrix.arch }} ${{ matrix.mode }} stall loop
runs-on: macos-26
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
mode: [isolated, pair]
arch: [x86_64, arm64]
env:
IOS_RUNTIME_VERSION: '26.2'
AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH: ${{ github.workspace }}/.tmp/ios-runner-derived
AGENT_DEVICE_XCUITEST_INCLUDE_UNIT_TESTS: '1'
AGENT_DEVICE_XCUITEST_ARCHS: ${{ matrix.arch }}
steps:
- uses: actions/checkout@v4
- name: Setup toolchain
uses: ./.github/actions/setup-node-pnpm
- name: Restore and build iOS XCTest runner
uses: ./.github/actions/setup-apple-runner-build
with:
derived-path: ${{ env.AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH }}
cache-key-prefix: ios-runner-prebuilt
cache-key-suffix: -ios-${{ env.IOS_RUNTIME_VERSION }}-1874-${{ matrix.arch }}
gate: swift-runner-ios
xcuitest-platform: ios
xcuitest-destination: generic/platform=iOS Simulator
- name: Boot iOS test simulator
id: ios-simulator
uses: ./.github/actions/boot-ios-test-simulator
with:
runtime-version: ${{ env.IOS_RUNTIME_VERSION }}
preferred-device-name: iPhone 17 Pro
- name: Stall loop
env:
ITERATIONS_INPUT: ${{ github.event.inputs.iterations }}
TEST_INPUT: ${{ github.event.inputs.test }}
run: |
set -euo pipefail
XCTESTRUN_PATH="$(find "$AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH/Build/Products" -maxdepth 1 -name '*.xctestrun' -print -quit)"
test -n "$XCTESTRUN_PATH"
UDID="${{ steps.ios-simulator.outputs.simulator-udid }}"
ITER="${ITERATIONS_INPUT:-25}"
TEST_NAME="${TEST_INPUT:-testBareTypeUsesTappedInputWhenSoftwareKeyboardIsHidden}"
mkdir -p .tmp
if [ "${{ matrix.mode }}" = "pair" ]; then
EXTRA_ONLY_TESTING=(-only-testing:AgentDeviceRunnerUITests/RunnerTests/testBareDelayedTypeFailsWhenTappedInputDisappearsMidCommand)
else
EXTRA_ONLY_TESTING=()
fi
sys_prof="$(system_profiler SPHardwareDataType 2>/dev/null | grep -E 'Chip|Cores|Memory' || true)"
echo "HOST: $sys_prof" | tee stall-summary.txt
pass=0; skipped=0; failed=0; noresult=0; runfailed=0
for i in $(seq 1 "$ITER"); do
LOG=".tmp/stall-run-$i.log"
set +e
xcodebuild test-without-building \
-xctestrun "$XCTESTRUN_PATH" \
-destination "platform=iOS Simulator,id=$UDID" \
-only-testing:"AgentDeviceRunnerUITests/RunnerTests/$TEST_NAME" \
${EXTRA_ONLY_TESTING[@]+"${EXTRA_ONLY_TESTING[@]}"} \
> "$LOG" 2>&1
rc=$?
set -e
verdict="$(node --experimental-strip-types scripts/diagnose-1874-iteration.ts \
"$LOG" "$TEST_NAME" "$i" "$rc")"
case "$verdict" in
passed) pass=$((pass+1)) ;;
skipped) skipped=$((skipped+1)) ;;
no-result) noresult=$((noresult+1)) ;;
run-failed) runfailed=$((runfailed+1)) ;;
*) failed=$((failed+1)) ;;
esac
done
echo "RESULT arch=${{ matrix.arch }} mode=${{ matrix.mode }}: $pass passed, $skipped skipped, $failed failed, $noresult no-result, $runfailed run-failed of $ITER" | tee -a stall-summary.txt
- name: Upload stall evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: stall-evidence-${{ matrix.arch }}-${{ matrix.mode }}
include-hidden-files: true
path: |
stall-summary.txt
.tmp/stall-evidence-*.log
if-no-files-found: ignore