From 5bb9f94a1e8bd5bd9f009db06172e38d0f3704fa Mon Sep 17 00:00:00 2001 From: Jintao Date: Tue, 4 Aug 2026 01:20:54 +0800 Subject: [PATCH 1/7] Parallelize minimum Xcode 16 CI --- .../actions/setup-minimum-xcode-16/action.yml | 64 +++++++ .github/workflows/ci.yml | 159 ++++++++---------- CHANGELOG.en.md | 11 +- CHANGELOG.md | 9 +- Docs/Testing.md | 11 +- Docs/en/Testing.md | 13 +- Tests/Scripts/ReleaseComplianceTests.rb | 48 +++++- 7 files changed, 214 insertions(+), 101 deletions(-) create mode 100644 .github/actions/setup-minimum-xcode-16/action.yml diff --git a/.github/actions/setup-minimum-xcode-16/action.yml b/.github/actions/setup-minimum-xcode-16/action.yml new file mode 100644 index 0000000..adfea98 --- /dev/null +++ b/.github/actions/setup-minimum-xcode-16/action.yml @@ -0,0 +1,64 @@ +name: Set up minimum Xcode 16 environment +description: Select Xcode 16.0 and install the pinned RootFS, XcodeGen, and iOS 18 Simulator runtime. + +runs: + using: composite + steps: + - name: Configure public package submodule checkout + shell: bash + run: | + git config --global \ + url."https://github.com/".insteadOf \ + "ssh://git@ssh.github.com:443/" + + - name: Verify minimum Apple toolchain + shell: bash + run: | + test "$(uname -m)" = "arm64" + test -d /Applications/Xcode_16.0.app/Contents/Developer + sudo xcode-select \ + --switch /Applications/Xcode_16.0.app/Contents/Developer + test "$(xcodebuild -version | sed -n '1p')" = "Xcode 16.0" + test "$(xcrun --sdk iphoneos --show-sdk-version)" = "18.0" + test "$(xcrun --sdk iphonesimulator --show-sdk-version)" = "18.0" + xcodebuild -version + swift --version + + - name: Download and verify pinned RootFS release archive + shell: bash + env: + ROOTFS_SHA256: "be0f3c133f78f28b023288459b33dc28fa253a6ef29f7123bc5f3892edf90ad4" + ROOTFS_BYTE_COUNT: "6581376" + ROOTFS_URL: "https://github.com/Lolendor/ish-arm64-pkg/releases/download/v0.3.3/fs.tar.gz" + run: | + archive="$RUNNER_TEMP/pocketroot-fs-v0.3.3.tar.gz" + curl --fail --location --retry 3 \ + --output "$archive" \ + "$ROOTFS_URL" + test "$(stat -f '%z' "$archive")" = "$ROOTFS_BYTE_COUNT" + echo "$ROOTFS_SHA256 $archive" | shasum -a 256 --check + echo "POCKETROOT_ROOTFS_ARCHIVE=$archive" >> "$GITHUB_ENV" + + - name: Install XcodeGen + shell: bash + env: + XCODEGEN_VERSION: "2.46.0" + XCODEGEN_SHA256: "4d9e34b62172d645eed6457cac13fc222569974098ef4ee9c3368bedf0196806" + run: | + archive="$RUNNER_TEMP/xcodegen.zip" + install_root="$RUNNER_TEMP/xcodegen-install" + curl --fail --location --retry 3 \ + --output "$archive" \ + "https://github.com/yonaskolb/XcodeGen/releases/download/$XCODEGEN_VERSION/xcodegen.zip" + echo "$XCODEGEN_SHA256 $archive" | shasum -a 256 --check + unzip -q "$archive" -d "$install_root" + echo "$install_root/xcodegen/bin" >> "$GITHUB_PATH" + + - name: Install iOS 18.0 Simulator runtime + shell: bash + run: | + xcrun simctl list >/dev/null + xcodebuild -downloadPlatform iOS + xcrun simctl list runtimes available + xcrun simctl list runtimes available \ + | grep -F "iOS 18.0" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c45905..1782f2d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -291,74 +291,23 @@ jobs: "$POCKETROOT_SPDX_SCHEMA" \ "$evidence/SBOM.spdx.json" - minimum-xcode-16: - name: Minimum Xcode 16.0 + minimum-xcode-16-runtime: + name: Minimum Xcode 16.0 / Native runtime runs-on: macos-15 - timeout-minutes: 90 + timeout-minutes: 35 steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Configure public package submodule checkout - run: | - git config --global \ - url."https://github.com/".insteadOf \ - "ssh://git@ssh.github.com:443/" - - - name: Verify minimum Apple toolchain - run: | - test "$(uname -m)" = "arm64" - test -d /Applications/Xcode_16.0.app/Contents/Developer - sudo xcode-select \ - --switch /Applications/Xcode_16.0.app/Contents/Developer - test "$(xcodebuild -version | sed -n '1p')" = "Xcode 16.0" - test "$(xcrun --sdk iphoneos --show-sdk-version)" = "18.0" - test "$(xcrun --sdk iphonesimulator --show-sdk-version)" = "18.0" - xcodebuild -version - swift --version - - - name: Download and verify pinned RootFS release archive - env: - ROOTFS_SHA256: "be0f3c133f78f28b023288459b33dc28fa253a6ef29f7123bc5f3892edf90ad4" - ROOTFS_BYTE_COUNT: "6581376" - ROOTFS_URL: "https://github.com/Lolendor/ish-arm64-pkg/releases/download/v0.3.3/fs.tar.gz" - run: | - archive="$RUNNER_TEMP/pocketroot-fs-v0.3.3.tar.gz" - curl --fail --location --retry 3 \ - --output "$archive" \ - "$ROOTFS_URL" - test "$(stat -f '%z' "$archive")" = "$ROOTFS_BYTE_COUNT" - echo "$ROOTFS_SHA256 $archive" | shasum -a 256 --check - echo "POCKETROOT_ROOTFS_ARCHIVE=$archive" >> "$GITHUB_ENV" + - name: Set up minimum Xcode 16 environment + uses: ./.github/actions/setup-minimum-xcode-16 - name: Validate real RootFS install with Xcode 16.0 run: | swift test \ --filter testPinnedReleaseArchiveWhenProvidedByEnvironment - - name: Install XcodeGen - env: - XCODEGEN_VERSION: "2.46.0" - XCODEGEN_SHA256: "4d9e34b62172d645eed6457cac13fc222569974098ef4ee9c3368bedf0196806" - run: | - archive="$RUNNER_TEMP/xcodegen.zip" - install_root="$RUNNER_TEMP/xcodegen-install" - curl --fail --location --retry 3 \ - --output "$archive" \ - "https://github.com/yonaskolb/XcodeGen/releases/download/$XCODEGEN_VERSION/xcodegen.zip" - echo "$XCODEGEN_SHA256 $archive" | shasum -a 256 --check - unzip -q "$archive" -d "$install_root" - echo "$install_root/xcodegen/bin" >> "$GITHUB_PATH" - - - name: Install iOS 18.0 Simulator runtime - run: | - xcrun simctl list >/dev/null - xcodebuild -downloadPlatform iOS - xcrun simctl list runtimes available - xcrun simctl list runtimes available \ - | grep -F "iOS 18.0" - - name: Final-link runtime with Xcode 16.0 run: ./Scripts/build-runtime-spike.sh @@ -370,52 +319,80 @@ jobs: POCKETROOT_SMOKE_STABILITY_INTERVAL_MILLISECONDS: "250" run: ./Scripts/run-runtime-smoke.sh - - name: Run public-SHA External Consumer UI acceptance + minimum-xcode-16-ui: + name: Minimum Xcode 16.0 / UI (${{ matrix.label }}) + runs-on: macos-15 + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + include: + - suite: external-consumer + label: External consumer + - suite: quick-start-iphone + label: Quick Start iPhone + - suite: quick-start-ipad + label: Quick Start iPad + - suite: host-iphone + label: Host App iPhone + - suite: host-ipad + label: Host App iPad + + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set up minimum Xcode 16 environment + uses: ./.github/actions/setup-minimum-xcode-16 + + - name: Run ${{ matrix.label }} UI smoke with Xcode 16.0 env: + POCKETROOT_UI_SUITE: ${{ matrix.suite }} + POCKETROOT_UI_FAILURE_ROOT: >- + ${{ runner.temp }}/pocketroot-ui-artifacts/${{ matrix.suite }} POCKETROOT_EXTERNAL_CONSUMER_REPOSITORY_URL: >- ${{ github.event.pull_request.head.repo.clone_url || format('{0}/{1}.git', github.server_url, github.repository) }} POCKETROOT_EXTERNAL_CONSUMER_REVISION: >- ${{ github.event.pull_request.head.sha || github.sha }} POCKETROOT_EXTERNAL_CONSUMER_REQUIRE_REMOTE: "1" - run: ./Scripts/run-external-consumer-ui-smoke.sh - - - name: Run Quick Start two-entry UI smoke with Xcode 16.0 - env: - POCKETROOT_UI_FAILURE_ARTIFACTS_DIR: >- - ${{ runner.temp }}/pocketroot-ui-artifacts/quick-start-iphone - run: ./Scripts/run-quick-start-ui-smoke.sh - - - name: Run Quick Start iPad UI smoke with Xcode 16.0 - env: - POCKETROOT_QUICK_START_UI_DEVICE_TYPE: >- - com.apple.CoreSimulator.SimDeviceType.iPad-10th-generation - POCKETROOT_QUICK_START_UI_DEVICE_NAME: >- - PocketRoot-Quick-Start-iPad-UI-Smoke - POCKETROOT_UI_FAILURE_ARTIFACTS_DIR: >- - ${{ runner.temp }}/pocketroot-ui-artifacts/quick-start-ipad - run: ./Scripts/run-quick-start-ui-smoke.sh - - - name: Run Host App PTY and Files UI smoke with Xcode 16.0 - env: - POCKETROOT_HOST_UI_FAILURE_ARTIFACTS_DIR: >- - ${{ runner.temp }}/pocketroot-ui-artifacts/host-iphone - run: ./Scripts/run-host-app-ui-smoke.sh - - - name: Run Host App iPad UI smoke with Xcode 16.0 - env: - POCKETROOT_HOST_UI_DEVICE_TYPE: >- - com.apple.CoreSimulator.SimDeviceType.iPad-10th-generation - POCKETROOT_HOST_UI_DEVICE_NAME: PocketRoot-Host-iPad-UI-Smoke - POCKETROOT_HOST_UI_FAILURE_ARTIFACTS_DIR: >- - ${{ runner.temp }}/pocketroot-ui-artifacts/host-ipad - run: ./Scripts/run-host-app-ui-smoke.sh + run: | + case "$POCKETROOT_UI_SUITE" in + external-consumer) + export POCKETROOT_UI_FAILURE_ARTIFACTS_DIR="$POCKETROOT_UI_FAILURE_ROOT" + ./Scripts/run-external-consumer-ui-smoke.sh + ;; + quick-start-iphone) + export POCKETROOT_UI_FAILURE_ARTIFACTS_DIR="$POCKETROOT_UI_FAILURE_ROOT" + ./Scripts/run-quick-start-ui-smoke.sh + ;; + quick-start-ipad) + export POCKETROOT_QUICK_START_UI_DEVICE_TYPE="com.apple.CoreSimulator.SimDeviceType.iPad-10th-generation" + export POCKETROOT_QUICK_START_UI_DEVICE_NAME="PocketRoot-Quick-Start-iPad-UI-Smoke" + export POCKETROOT_UI_FAILURE_ARTIFACTS_DIR="$POCKETROOT_UI_FAILURE_ROOT" + ./Scripts/run-quick-start-ui-smoke.sh + ;; + host-iphone) + export POCKETROOT_HOST_UI_FAILURE_ARTIFACTS_DIR="$POCKETROOT_UI_FAILURE_ROOT" + ./Scripts/run-host-app-ui-smoke.sh + ;; + host-ipad) + export POCKETROOT_HOST_UI_DEVICE_TYPE="com.apple.CoreSimulator.SimDeviceType.iPad-10th-generation" + export POCKETROOT_HOST_UI_DEVICE_NAME="PocketRoot-Host-iPad-UI-Smoke" + export POCKETROOT_HOST_UI_FAILURE_ARTIFACTS_DIR="$POCKETROOT_UI_FAILURE_ROOT" + ./Scripts/run-host-app-ui-smoke.sh + ;; + *) + echo "Unknown minimum-Xcode UI suite: $POCKETROOT_UI_SUITE" >&2 + exit 2 + ;; + esac - name: Upload UI failure diagnostics if: failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: pocketroot-ui-failure-${{ github.run_id }}-${{ github.run_attempt }} - path: ${{ runner.temp }}/pocketroot-ui-artifacts + name: pocketroot-ui-failure-${{ matrix.suite }}-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/pocketroot-ui-artifacts/${{ matrix.suite }} if-no-files-found: warn retention-days: 7 diff --git a/CHANGELOG.en.md b/CHANGELOG.en.md index cf3c2cb..d006d5f 100644 --- a/CHANGELOG.en.md +++ b/CHANGELOG.en.md @@ -6,7 +6,16 @@ All notable PocketRoot changes are recorded here. Semantic Versioning begins wit ## Unreleased -There are currently no changes outside the release candidate. +### Changed + +- Split the minimum-Xcode 16 gate into an independent native-runtime job and a + five-way `fail-fast: false` UI matrix for the public-SHA external consumer, + Quick Start iPhone/iPad, and Host App iPhone/iPad. Every isolated runner + re-verifies the RootFS, XcodeGen, Xcode 16, and iOS 18 runtime through one + repository-owned composite action without transferring an unreviewed App or + DerivedData; each lane has a collision-free failure artifact. Coverage is + unchanged while PR wall-clock time no longer accumulates every UI smoke + serially. ## 0.2.0 - Unreleased diff --git a/CHANGELOG.md b/CHANGELOG.md index e84f6c9..49e9e9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,14 @@ PocketRoot 的重要变化记录在这里,并从首个公开版本开始遵循 ## Unreleased -当前暂无未归入候选版本的变更。 +### Changed + +- 将最低 Xcode 16 门禁拆为独立 native runtime job 和五路 `fail-fast: false` UI + matrix,分别并行验证公开 SHA 外部消费者、Quick Start iPhone/iPad 与 Host App + iPhone/iPad。全部 job 复用仓库内 composite action,但各自在隔离 runner 上重新 + 校验 RootFS、XcodeGen、Xcode 16 与 iOS 18 runtime,不传递未审计 App 或 DerivedData; + 每路失败诊断使用独立 artifact 名称。测试覆盖不减少,PR 墙钟时间不再由全部 UI + smoke 串行累加。 ## 0.2.0 - Unreleased diff --git a/Docs/Testing.md b/Docs/Testing.md index beda5ae..a537bad 100644 --- a/Docs/Testing.md +++ b/Docs/Testing.md @@ -560,9 +560,14 @@ RootFS。这只是兼容性证据,不授权 RootFS 分发,也不改变正式 SPDX 2.3 SBOM,要求无 private framework、private entitlement、JIT entitlement、 `MAP_JIT` 或无效签名信号,再用同一固定 schema 校验;App 和证据均不上传。 -最低工具链 job 另外固定选择 Xcode 16.0 / iOS 18.0 SDK,验证真实 RootFS install、 -安装 iOS 18.0 Simulator runtime、完成 Simulator/device final-link,执行 17 项原生 -smoke,并分别在 iPhone 16 与 iPad(第 10 代)Simulator 运行最小 Quick Start 的 +最低工具链门禁拆为一个 native runtime job 和五路并行 UI matrix job。每个 job 都通过 +仓库内固定的 composite action 独立选择 Xcode 16.0 / iOS 18.0 SDK、校验并取得同一 +RootFS、安装同一 XcodeGen 与 iOS 18.0 Simulator runtime;job 之间不传递未审计的 +App、RootFS 或 DerivedData。Native job 验证真实 RootFS install、完成 +Simulator/device final-link 并执行 17 项原生 smoke。UI matrix 设置 +`fail-fast: false`,分别运行公开 SHA 外部消费者、iPhone/iPad Quick Start 和 +iPhone/iPad Host App,因此单路失败不会取消其他证据;每路使用不冲突的失败制品名。 +这些 UI job 在 iPhone 16 与 iPad(第 10 代)Simulator 运行最小 Quick Start 的 Files/Terminal 冷启动与 PTY-to-Files 文件闭环,以及 Host App 的 PTY、Files、 Workspace、系统 document picker 导入、share sheet 保存、guest 删除后再次导入并 复验内容的完整 UI 闭环。测试 fixture 只在显式 `-PocketRootUITesting` diff --git a/Docs/en/Testing.md b/Docs/en/Testing.md index 280a3a2..ee1d468 100644 --- a/Docs/en/Testing.md +++ b/Docs/en/Testing.md @@ -499,9 +499,16 @@ private-framework, private-entitlement, JIT-entitlement, `MAP_JIT`, or invalid signature signal, and validates the generated SBOM with the same schema. It uploads neither the App nor scan evidence. -The minimum-toolchain job explicitly selects Xcode 16.0 / iOS 18.0 SDK, -validates real RootFS installation, installs the iOS 18.0 Simulator runtime, -final-links Simulator/device Apps, runs the 17-check native smoke, and executes +The minimum-toolchain gate is split into one native-runtime job and a five-way +parallel UI matrix. Every job independently uses the repository-owned +composite action to select Xcode 16.0 / iOS 18.0 SDK, verify and obtain the +same RootFS, and install the same XcodeGen and iOS 18.0 Simulator runtime. Jobs +do not transfer an unreviewed App, RootFS, or DerivedData between runners. The +native job validates real RootFS installation, final-links Simulator/device +Apps, and runs the 17-check native smoke. The `fail-fast: false` UI matrix runs +the public-SHA external consumer, iPhone/iPad Quick Start, and iPhone/iPad Host +App independently, so one failure does not cancel the remaining evidence and +each lane has a collision-free diagnostic artifact name. These lanes execute the minimal Quick Start cold Files/Terminal and PTY-to-Files file closure plus the full Host App PTY, Files, Workspace, system document-picker import, share-sheet save, guest deletion, re-import, and content-verification UI diff --git a/Tests/Scripts/ReleaseComplianceTests.rb b/Tests/Scripts/ReleaseComplianceTests.rb index d44c10c..3db6639 100644 --- a/Tests/Scripts/ReleaseComplianceTests.rb +++ b/Tests/Scripts/ReleaseComplianceTests.rb @@ -1023,8 +1023,52 @@ def test_external_consumer_profile_resolves_public_products_and_lifecycle assert_includes runner, "POCKETROOT_EXTERNAL_CONSUMER_REVISION" assert_includes runner, "POCKETROOT_EXTERNAL_CONSUMER_REPOSITORY_URL" - assert_includes workflow, - "Run public-SHA External Consumer UI acceptance" + assert_includes workflow, "suite: external-consumer" + assert_includes workflow, "./Scripts/run-external-consumer-ui-smoke.sh" + end + + def test_minimum_xcode_ci_parallelizes_native_and_ui_coverage + workflow = REPOSITORY_ROOT.join(".github/workflows/ci.yml").binread + setup_action = + REPOSITORY_ROOT + .join(".github/actions/setup-minimum-xcode-16/action.yml") + .binread + + assert_match(/^ minimum-xcode-16-runtime:$/, workflow) + assert_match(/^ minimum-xcode-16-ui:$/, workflow) + refute_match(/^ minimum-xcode-16:$/, workflow) + assert_includes workflow, "fail-fast: false" + assert_equal( + 2, + workflow.scan("uses: ./.github/actions/setup-minimum-xcode-16").length + ) + %w[ + external-consumer + quick-start-iphone + quick-start-ipad + host-iphone + host-ipad + ].each do |suite| + assert_includes workflow, "suite: #{suite}" + end + assert_includes workflow, "./Scripts/run-runtime-smoke.sh" + assert_includes workflow, "./Scripts/run-external-consumer-ui-smoke.sh" + assert_includes workflow, "./Scripts/run-quick-start-ui-smoke.sh" + assert_includes workflow, "./Scripts/run-host-app-ui-smoke.sh" + assert_includes( + workflow, + "pocketroot-ui-failure-${{ matrix.suite }}-${{ github.run_id }}" + ) + + assert_includes setup_action, "/Applications/Xcode_16.0.app" + assert_includes setup_action, 'ROOTFS_BYTE_COUNT: "6581376"' + assert_includes( + setup_action, + 'ROOTFS_SHA256: "be0f3c133f78f28b023288459b33dc28fa253a6ef29f7123bc5f3892edf90ad4"' + ) + assert_includes setup_action, 'XCODEGEN_VERSION: "2.46.0"' + assert_includes setup_action, "xcodebuild -downloadPlatform iOS" + assert_includes setup_action, 'grep -F "iOS 18.0"' end def test_standalone_host_retains_runtime_across_scene_recreation From 2078226c64859df33574c5d78361d4620007a592 Mon Sep 17 00:00:00 2001 From: Jintao Date: Tue, 4 Aug 2026 02:06:58 +0800 Subject: [PATCH 2/7] Stabilize Host App folder UI smoke --- .github/workflows/ci.yml | 2 +- .../Release/experimental-v0.2.0/COMPOSITION.json | 2 +- .../Release/experimental-v0.2.0/SBOM.spdx.json | 2 +- Compliance/Release/experimental-v0.2.0/SHA256SUMS | 4 ++-- .../UITests/PocketRootHostAppUITests.swift | 15 ++++++++++++--- Tests/Scripts/ReleaseComplianceTests.rb | 3 +++ 6 files changed, 20 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1782f2d..e40589c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -322,7 +322,7 @@ jobs: minimum-xcode-16-ui: name: Minimum Xcode 16.0 / UI (${{ matrix.label }}) runs-on: macos-15 - timeout-minutes: 45 + timeout-minutes: 60 strategy: fail-fast: false matrix: diff --git a/Compliance/Release/experimental-v0.2.0/COMPOSITION.json b/Compliance/Release/experimental-v0.2.0/COMPOSITION.json index 5f01d94..e996491 100644 --- a/Compliance/Release/experimental-v0.2.0/COMPOSITION.json +++ b/Compliance/Release/experimental-v0.2.0/COMPOSITION.json @@ -120,7 +120,7 @@ "Examples/PocketRootDemo/Sources/PocketRootDemo/Shared/UI/PlaceholderViewController.swift": "48e0f6462228a66face2aef51f9bd39ec02923c4d1c01b6b34f920948998f2b8", "Examples/PocketRootHostApp/Sources/HostApp.swift": "0c36bd3104ddc5bebb9e23675a44a92e3ce4aec2c1461b3e182d86d1a75159c9", "Examples/PocketRootHostApp/Sources/Info.plist": "711b38923f11fdb4ceb7d28cb90a51674afec0a64786a20f9bd68e3f406f8d26", - "Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift": "bc99a66278cd3476f7b4e854d85ec975d839362a386e8b5a455496f63e14c491", + "Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift": "2bc9c122dc9f8f5373a04b93ddd649d266046eceb1b1f6cdc8eca928baab9b3f", "Examples/PocketRootQuickStartApp/Sources/Info.plist": "b3c0c96e89db99c72c9f020590b1c5cdc0c927fd3af4b6ae7865abfdc81049b8", "Examples/PocketRootQuickStartApp/Sources/QuickStartApp.swift": "1c77c9229281d0d1f9fbe3678bcc9995fff8c7d2189f4c6152daaa2999683fb6", "Examples/PocketRootQuickStartApp/UITests/PocketRootQuickStartAppUITests.swift": "828471c84386dc8ddba23ecadc3618ccd559d92084ab762f4ef0832c3e3d5f59", diff --git a/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json b/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json index 4bdaf49..bbc0f25 100644 --- a/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json +++ b/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json @@ -3,7 +3,7 @@ "dataLicense": "CC0-1.0", "SPDXID": "SPDXRef-DOCUMENT", "name": "PocketRoot experimental full-graph composition 0.2.0", - "documentNamespace": "https://github.com/jacklv-coder/PocketRoot/sbom/release/experimental-v0.2.0/ac8d5bb757a49cc8c7c154c43a223a39caf7d05ec81c3a315f1afe08513c26be", + "documentNamespace": "https://github.com/jacklv-coder/PocketRoot/sbom/release/experimental-v0.2.0/5ee38a7c20791247a9fdc8af4a5fb236ceb42485778ea61a1fdafe88fc8abc42", "creationInfo": { "created": "2026-08-03T00:00:00Z", "creators": [ diff --git a/Compliance/Release/experimental-v0.2.0/SHA256SUMS b/Compliance/Release/experimental-v0.2.0/SHA256SUMS index 9c6f265..c835acc 100644 --- a/Compliance/Release/experimental-v0.2.0/SHA256SUMS +++ b/Compliance/Release/experimental-v0.2.0/SHA256SUMS @@ -1,5 +1,5 @@ -ac8d5bb757a49cc8c7c154c43a223a39caf7d05ec81c3a315f1afe08513c26be COMPOSITION.json +5ee38a7c20791247a9fdc8af4a5fb236ceb42485778ea61a1fdafe88fc8abc42 COMPOSITION.json 9fe270ba11160051d13f99cfe4ce6ee03c52882b88339dd6e4c1b4923ae30755 READINESS.json 6d41126608b019b46332a7957889f41e3c3ec6c6e9e2b884dca5ffc6d405e943 README.md fab07621b5188930bfd2a38117e206815d94f755e667d65fc182ee55d14d2d40 RELEASE-CHECKLIST.md -0c88032bfa74974a774165e49a2ebf5f4fbfed6632436f1660c16209d7a7f773 SBOM.spdx.json +04303fedcb52e3bef88226cfb55c19b04961407b2bb50f4a8fdf46f76aa79d74 SBOM.spdx.json diff --git a/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift b/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift index 4478a3a..737e398 100644 --- a/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift +++ b/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift @@ -104,7 +104,10 @@ final class PocketRootHostAppUITests: XCTestCase { XCTAssertTrue(disclosure.waitForExistence(timeout: 10)) waitForEnabled(disclosure) disclosure.tap() - waitForEnabled(folder) + guard revealFileEntry(folder, in: app) else { + shutdownRuntime(in: app) + return + } folder.tap() waitForEnabled(actions) @@ -115,8 +118,14 @@ final class PocketRootHostAppUITests: XCTestCase { app.buttons["Create"].tap() waitForEnabled(actions) let childNavigationBar = app.navigationBars[folderName] - XCTAssertTrue(childNavigationBar.waitForExistence(timeout: 10)) - childNavigationBar.buttons.element(boundBy: 0).tap() + guard childNavigationBar.waitForExistence(timeout: 30) else { + XCTFail("child folder navigation bar to exist") + shutdownRuntime(in: app) + return + } + let backButton = childNavigationBar.buttons.element(boundBy: 0) + XCTAssertTrue(waitForHittable(backButton)) + backButton.tap() let nestedFile = app.descendants(matching: .any)[ "PocketRootFiles.entry./root/\(folderName)/\(nestedFileName)" diff --git a/Tests/Scripts/ReleaseComplianceTests.rb b/Tests/Scripts/ReleaseComplianceTests.rb index 3db6639..373119d 100644 --- a/Tests/Scripts/ReleaseComplianceTests.rb +++ b/Tests/Scripts/ReleaseComplianceTests.rb @@ -1037,6 +1037,9 @@ def test_minimum_xcode_ci_parallelizes_native_and_ui_coverage assert_match(/^ minimum-xcode-16-runtime:$/, workflow) assert_match(/^ minimum-xcode-16-ui:$/, workflow) refute_match(/^ minimum-xcode-16:$/, workflow) + ui_job_header = + workflow.split(/^ minimum-xcode-16-ui:\n/, 2).fetch(1).lines.first(5).join + assert_includes ui_job_header, "timeout-minutes: 60" assert_includes workflow, "fail-fast: false" assert_equal( 2, From facf40405e89b3801ec914852072e71d7b2ca1de Mon Sep 17 00:00:00 2001 From: Jintao Date: Tue, 4 Aug 2026 02:31:53 +0800 Subject: [PATCH 3/7] Recover vanished CI simulators --- CHANGELOG.en.md | 8 ++- CHANGELOG.md | 5 +- .../experimental-v0.2.0/COMPOSITION.json | 2 +- .../experimental-v0.2.0/SBOM.spdx.json | 2 +- .../Release/experimental-v0.2.0/SHA256SUMS | 4 +- Docs/Testing.md | 7 ++- Docs/en/Testing.md | 11 ++-- Scripts/generate-release-compliance.rb | 2 +- Scripts/run-ios-example-ui-smoke.sh | 55 +++++++++++++++++-- Tests/Scripts/ReleaseComplianceTests.rb | 4 +- Tests/Scripts/RuntimeSmokeScriptTests.sh | 34 +++++++++++- 11 files changed, 109 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.en.md b/CHANGELOG.en.md index d006d5f..28ddf60 100644 --- a/CHANGELOG.en.md +++ b/CHANGELOG.en.md @@ -13,9 +13,11 @@ All notable PocketRoot changes are recorded here. Semantic Versioning begins wit Quick Start iPhone/iPad, and Host App iPhone/iPad. Every isolated runner re-verifies the RootFS, XcodeGen, Xcode 16, and iOS 18 runtime through one repository-owned composite action without transferring an unreviewed App or - DerivedData; each lane has a collision-free failure artifact. Coverage is - unchanged while PR wall-clock time no longer accumulates every UI smoke - serially. + DerivedData; each lane has a collision-free failure artifact. A runner-owned + Simulator that explicitly disappears from Xcode destinations is recreated + with the same runtime/device type for one bounded retry; caller-owned devices + and assertion failures are never retried. Coverage is unchanged while PR + wall-clock time no longer accumulates every UI smoke serially. ## 0.2.0 - Unreleased diff --git a/CHANGELOG.md b/CHANGELOG.md index 49e9e9b..5a41cfe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,9 @@ PocketRoot 的重要变化记录在这里,并从首个公开版本开始遵循 matrix,分别并行验证公开 SHA 外部消费者、Quick Start iPhone/iPad 与 Host App iPhone/iPad。全部 job 复用仓库内 composite action,但各自在隔离 runner 上重新 校验 RootFS、XcodeGen、Xcode 16 与 iOS 18 runtime,不传递未审计 App 或 DerivedData; - 每路失败诊断使用独立 artifact 名称。测试覆盖不减少,PR 墙钟时间不再由全部 UI - smoke 串行累加。 + 每路失败诊断使用独立 artifact 名称。runner 自建 Simulator 明确从 Xcode destination + 消失时可按相同 runtime/device type 重建并有界重试一次;调用方设备和断言失败不重试。 + 测试覆盖不减少,PR 墙钟时间不再由全部 UI smoke 串行累加。 ## 0.2.0 - Unreleased diff --git a/Compliance/Release/experimental-v0.2.0/COMPOSITION.json b/Compliance/Release/experimental-v0.2.0/COMPOSITION.json index e996491..1f84f23 100644 --- a/Compliance/Release/experimental-v0.2.0/COMPOSITION.json +++ b/Compliance/Release/experimental-v0.2.0/COMPOSITION.json @@ -24,7 +24,7 @@ "Scripts/inject-demo-rootfs.sh": "3982b5382b0d1e13e0c8e8a5bb5404c5bad1dfc4d6e9cd23a39e3395a83087bb", "Scripts/scan-release-artifact.rb": "48f27ab280491864228393e1675fe3a2889cbd616f79e3cb16bae7efedf647c0", "Scripts/run-host-app-ui-smoke.sh": "c8b11b1bf3a467fd10ed2c4124a72396b0a803e98f155d1d709fab46b59eede1", - "Scripts/run-ios-example-ui-smoke.sh": "071fa3336479f1849eb3be3f1f6b6516025197a0cf30640e64cd902282d9ed4d", + "Scripts/run-ios-example-ui-smoke.sh": "47f2b34bec554a819c3b580ca045f4d31bdae2c7379470f5495f21dde58994dd", "Scripts/run-quick-start-ui-smoke.sh": "acf26d68fc37250e24911c2d23791a0d12ff06a624dedcefee0ed7905bc31af2", "Scripts/run-external-consumer-ui-smoke.sh": "d9544ede3ecbad8d42a114015fa77071d31aa84aafc1cbe0f0f001453b1a4d0d", "Scripts/run-host-app-device-ui-smoke.sh": "2f934265cb46145b27bb26e0d7e84acebc14290602e23cd1ab8f222aef68ed1f", diff --git a/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json b/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json index bbc0f25..a5e280a 100644 --- a/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json +++ b/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json @@ -3,7 +3,7 @@ "dataLicense": "CC0-1.0", "SPDXID": "SPDXRef-DOCUMENT", "name": "PocketRoot experimental full-graph composition 0.2.0", - "documentNamespace": "https://github.com/jacklv-coder/PocketRoot/sbom/release/experimental-v0.2.0/5ee38a7c20791247a9fdc8af4a5fb236ceb42485778ea61a1fdafe88fc8abc42", + "documentNamespace": "https://github.com/jacklv-coder/PocketRoot/sbom/release/experimental-v0.2.0/cef6204218376c737d154e0386bdbed28b7bcc90c87aec8f172e02a154a05110", "creationInfo": { "created": "2026-08-03T00:00:00Z", "creators": [ diff --git a/Compliance/Release/experimental-v0.2.0/SHA256SUMS b/Compliance/Release/experimental-v0.2.0/SHA256SUMS index c835acc..57fd6a1 100644 --- a/Compliance/Release/experimental-v0.2.0/SHA256SUMS +++ b/Compliance/Release/experimental-v0.2.0/SHA256SUMS @@ -1,5 +1,5 @@ -5ee38a7c20791247a9fdc8af4a5fb236ceb42485778ea61a1fdafe88fc8abc42 COMPOSITION.json +cef6204218376c737d154e0386bdbed28b7bcc90c87aec8f172e02a154a05110 COMPOSITION.json 9fe270ba11160051d13f99cfe4ce6ee03c52882b88339dd6e4c1b4923ae30755 READINESS.json 6d41126608b019b46332a7957889f41e3c3ec6c6e9e2b884dca5ffc6d405e943 README.md fab07621b5188930bfd2a38117e206815d94f755e667d65fc182ee55d14d2d40 RELEASE-CHECKLIST.md -04303fedcb52e3bef88226cfb55c19b04961407b2bb50f4a8fdf46f76aa79d74 SBOM.spdx.json +3d3b61a27db6de067e6610dac598032cbc1a839a6af0c3cb625b115e44a995b9 SBOM.spdx.json diff --git a/Docs/Testing.md b/Docs/Testing.md index a537bad..b7a655e 100644 --- a/Docs/Testing.md +++ b/Docs/Testing.md @@ -578,9 +578,10 @@ Workspace、系统 document picker 导入、share sheet 保存、guest 删除后 `POCKETROOT_KEEP_UI_RESULT=1` 可保留临时 DerivedData 与 `.xcresult`。两个 wrapper 复用 `run-ios-example-ui-smoke.sh` 的相同 RootFS、超时、xcresult 和安全清理边界。 当 Xcode 明确报告 Simulator test runner 未注册到 FrontBoard 时,通用 runner 会重启 -同一个由 runner 创建的临时 Simulator 并且只重试一次;调用方通过 -`POCKETROOT_UI_SMOKE_DEVICE` 提供的共享 Simulator 不会被自动关闭或重启。断言失败、 -测试超时和其他构建错误不会重试。 +同一个由 runner 创建的临时 Simulator;当 Xcode 明确报告目标设备已从可用 destination +消失时,会以相同 runtime 与 device type 重建该临时 Simulator。两种基础设施恢复合计 +最多重试一次;调用方通过 `POCKETROOT_UI_SMOKE_DEVICE` 提供的共享 Simulator 不会被 +自动关闭、重启或重建。断言失败、测试超时和其他构建错误不会重试。 可设置 `POCKETROOT_UI_INFRASTRUCTURE_RETRY_LIMIT=0` 关闭该恢复路径。若第二次仍失败, 失败 artifact 会同时包含首次/最终测试日志与可用的两次 `.xcresult`;即使重启本身 失败,也保留首次 `xcodebuild` 的诊断结果。 diff --git a/Docs/en/Testing.md b/Docs/en/Testing.md index ee1d468..95862aa 100644 --- a/Docs/en/Testing.md +++ b/Docs/en/Testing.md @@ -524,10 +524,13 @@ Use `POCKETROOT_QUICK_START_UI_DEVICE_TYPE` and cleanup boundaries. For failure diagnosis, `POCKETROOT_KEEP_UI_RESULT=1` retains temporary DerivedData and the `.xcresult`. When Xcode explicitly reports that the Simulator test runner was not registered with FrontBoard, the -shared runner restarts the same temporary Simulator and retries exactly once. -This recovery applies only to a Simulator created by the runner; a shared -Simulator supplied through `POCKETROOT_UI_SMOKE_DEVICE` is never automatically -shut down or restarted. Assertions, test timeouts, and other build failures are not retried. Set +shared runner restarts the same temporary Simulator. When Xcode explicitly +reports that the destination has disappeared, it recreates that temporary +Simulator with the same runtime and device type. These infrastructure paths +share one total retry. Recovery applies only to a Simulator created by the +runner; a shared Simulator supplied through `POCKETROOT_UI_SMOKE_DEVICE` is +never automatically shut down, restarted, or recreated. Assertions, test +timeouts, and other build failures are not retried. Set `POCKETROOT_UI_INFRASTRUCTURE_RETRY_LIMIT=0` to disable this recovery path. If the retry or restart also fails, the failure artifact retains the first and final logs plus both available `.xcresult` bundles. This Simulator evidence diff --git a/Scripts/generate-release-compliance.rb b/Scripts/generate-release-compliance.rb index 6c099a9..1c3acbc 100644 --- a/Scripts/generate-release-compliance.rb +++ b/Scripts/generate-release-compliance.rb @@ -281,7 +281,7 @@ module PocketRootReleaseCompliance "Scripts/run-host-app-ui-smoke.sh" => "c8b11b1bf3a467fd10ed2c4124a72396b0a803e98f155d1d709fab46b59eede1", "Scripts/run-ios-example-ui-smoke.sh" => - "071fa3336479f1849eb3be3f1f6b6516025197a0cf30640e64cd902282d9ed4d", + "47f2b34bec554a819c3b580ca045f4d31bdae2c7379470f5495f21dde58994dd", "Scripts/run-quick-start-ui-smoke.sh" => "acf26d68fc37250e24911c2d23791a0d12ff06a624dedcefee0ed7905bc31af2", "Scripts/run-external-consumer-ui-smoke.sh" => diff --git a/Scripts/run-ios-example-ui-smoke.sh b/Scripts/run-ios-example-ui-smoke.sh index 8dd045d..a99f229 100755 --- a/Scripts/run-ios-example-ui-smoke.sh +++ b/Scripts/run-ios-example-ui-smoke.sh @@ -31,6 +31,8 @@ FIRST_ATTEMPT_RESULT_BUNDLE_PATH="$DERIVED_DATA_ROOT/$TEST_BUNDLE-attempt-1.xcre RETRY_SHUTDOWN_EXIT_CODE="" RETRY_RESTART_EXIT_CODE="" RETRY_RESTART_FAILURE_STAGE="" +RETRY_FAILURE_KIND="" +FIRST_ATTEMPT_DEVICE_UDID="" cleanup() { if [[ "$CREATED_DEVICE" == "true" && @@ -130,28 +132,66 @@ run_ui_tests() { return "${PIPESTATUS[0]}" } -is_retryable_simulator_launch_failure() { - grep -Fq 'Simulator device failed to launch' "$TEST_LOG_PATH" && - grep -Fq 'is unknown to FrontBoard' "$TEST_LOG_PATH" +retryable_simulator_failure_kind() { + if grep -Fq 'Simulator device failed to launch' "$TEST_LOG_PATH" && + grep -Fq 'is unknown to FrontBoard' "$TEST_LOG_PATH"; then + printf 'frontboard-launch\n' + return 0 + fi + if grep -Fq 'Unable to find a device matching the provided destination specifier' \ + "$TEST_LOG_PATH" && + grep -Fq 'no available devices matched the request' "$TEST_LOG_PATH"; then + printf 'missing-destination\n' + return 0 + fi + return 1 } set +e run_ui_tests test_exit_code="$?" set -e +if [[ "$test_exit_code" -ne 0 ]]; then + RETRY_FAILURE_KIND="$(retryable_simulator_failure_kind || true)" +fi if [[ "$test_exit_code" -ne 0 && "$INFRASTRUCTURE_RETRY_LIMIT" -eq 1 && - "$CREATED_DEVICE" == "true" ]] && - is_retryable_simulator_launch_failure; then + "$CREATED_DEVICE" == "true" && + -n "$RETRY_FAILURE_KIND" ]]; then + FIRST_ATTEMPT_DEVICE_UDID="$DEVICE_UDID" mv "$TEST_LOG_PATH" "$FIRST_ATTEMPT_LOG_PATH" if [[ -d "$RESULT_BUNDLE_PATH" ]]; then mv "$RESULT_BUNDLE_PATH" "$FIRST_ATTEMPT_RESULT_BUNDLE_PATH" fi - echo "$ARTIFACT_LABEL UI test runner was not registered with FrontBoard; restarting the Simulator and retrying once." >&2 + if [[ "$RETRY_FAILURE_KIND" == "missing-destination" ]]; then + echo "$ARTIFACT_LABEL Simulator destination disappeared; recreating the runner-owned Simulator and retrying once." >&2 + else + echo "$ARTIFACT_LABEL UI test runner was not registered with FrontBoard; restarting the Simulator and retrying once." >&2 + fi set +e xcrun simctl shutdown "$DEVICE_UDID" >/dev/null 2>&1 RETRY_SHUTDOWN_EXIT_CODE="$?" + if [[ "$RETRY_FAILURE_KIND" == "missing-destination" ]]; then + xcrun simctl delete "$DEVICE_UDID" >/dev/null 2>&1 + replacement_device="$(xcrun simctl create \ + "$SIMULATOR_NAME" \ + "$SIMULATOR_DEVICE_TYPE" \ + "$RUNTIME_ID")" + RETRY_RESTART_EXIT_CODE="$?" + if [[ "$RETRY_RESTART_EXIT_CODE" -eq 0 && + -n "$replacement_device" ]]; then + DEVICE_UDID="$replacement_device" + else + RETRY_RESTART_FAILURE_STAGE="create" + if [[ "$RETRY_RESTART_EXIT_CODE" -eq 0 ]]; then + RETRY_RESTART_EXIT_CODE=1 + fi + fi + else + RETRY_RESTART_EXIT_CODE=0 + fi + if [[ "$RETRY_RESTART_EXIT_CODE" -eq 0 ]]; then xcrun simctl boot "$DEVICE_UDID" >/dev/null 2>&1 RETRY_RESTART_EXIT_CODE="$?" if [[ "$RETRY_RESTART_EXIT_CODE" -ne 0 ]]; then @@ -163,6 +203,7 @@ if [[ "$test_exit_code" -ne 0 && RETRY_RESTART_FAILURE_STAGE="bootstatus" fi fi + fi set -e if [[ "$RETRY_RESTART_EXIT_CODE" -eq 0 ]]; then RETRY_RESTART_FAILURE_STAGE="" @@ -203,6 +244,8 @@ if [[ "$test_exit_code" -ne 0 ]]; then echo "default_allowance=$DEFAULT_TEST_ALLOWANCE" echo "maximum_allowance=$MAXIMUM_TEST_ALLOWANCE" echo "infrastructure_retry_limit=$INFRASTRUCTURE_RETRY_LIMIT" + echo "retry_failure_kind=${RETRY_FAILURE_KIND:-}" + echo "first_attempt_device=$FIRST_ATTEMPT_DEVICE_UDID" echo "retry_shutdown_exit_code=$RETRY_SHUTDOWN_EXIT_CODE" echo "retry_restart_exit_code=$RETRY_RESTART_EXIT_CODE" echo "retry_restart_failure_stage=$RETRY_RESTART_FAILURE_STAGE" diff --git a/Tests/Scripts/ReleaseComplianceTests.rb b/Tests/Scripts/ReleaseComplianceTests.rb index 373119d..2b8695b 100644 --- a/Tests/Scripts/ReleaseComplianceTests.rb +++ b/Tests/Scripts/ReleaseComplianceTests.rb @@ -1229,8 +1229,10 @@ def test_standalone_host_has_real_pty_and_files_ui_smoke assert_includes generic_runner, "POCKETROOT_UI_SKIP_TESTING" assert_includes generic_runner, "POCKETROOT_UI_FAILURE_ARTIFACTS_DIR" assert_includes generic_runner, "POCKETROOT_UI_INFRASTRUCTURE_RETRY_LIMIT" - assert_includes generic_runner, "is_retryable_simulator_launch_failure" + assert_includes generic_runner, "retryable_simulator_failure_kind" assert_includes generic_runner, "is unknown to FrontBoard" + assert_includes generic_runner, "no available devices matched the request" + assert_includes generic_runner, "replacement_device" assert_includes generic_runner, '"$CREATED_DEVICE" == "true"' assert_includes generic_runner, "xcodebuild-test-attempt-1.log" assert_includes generic_runner, "-attempt-1.xcresult" diff --git a/Tests/Scripts/RuntimeSmokeScriptTests.sh b/Tests/Scripts/RuntimeSmokeScriptTests.sh index 96578bc..3f07a07 100755 --- a/Tests/Scripts/RuntimeSmokeScriptTests.sh +++ b/Tests/Scripts/RuntimeSmokeScriptTests.sh @@ -84,8 +84,9 @@ if ! grep -Fq -- 'POCKETROOT_KEEP_UI_RESULT' "$GENERIC_UI_RUNNER" \ || ! grep -Fq -- 'POCKETROOT_UI_SKIP_TESTING' "$GENERIC_UI_RUNNER" \ || ! grep -Fq -- 'POCKETROOT_UI_FAILURE_ARTIFACTS_DIR' "$GENERIC_UI_RUNNER" \ || ! grep -Fq -- 'POCKETROOT_UI_INFRASTRUCTURE_RETRY_LIMIT' "$GENERIC_UI_RUNNER" \ - || ! grep -Fq -- 'is_retryable_simulator_launch_failure' "$GENERIC_UI_RUNNER" \ + || ! grep -Fq -- 'retryable_simulator_failure_kind' "$GENERIC_UI_RUNNER" \ || ! grep -Fq -- "is unknown to FrontBoard" "$GENERIC_UI_RUNNER" \ + || ! grep -Fq -- 'no available devices matched the request' "$GENERIC_UI_RUNNER" \ || ! grep -Fq -- 'retrying once' "$GENERIC_UI_RUNNER" \ || ! grep -Fq -- 'xcodebuild-test-attempt-1.log' "$GENERIC_UI_RUNNER" \ || ! grep -Fq -- '-attempt-1.xcresult' "$GENERIC_UI_RUNNER" \ @@ -185,6 +186,11 @@ if [[ "${POCKETROOT_MOCK_XCODEBUILD_MODE:-retry}" == "retry" && "$calls" -eq 1 ] echo 'Application "com.example.UITests.xctrunner" is unknown to FrontBoard.' exit 65 fi +if [[ "${POCKETROOT_MOCK_XCODEBUILD_MODE:-retry}" == "missing-destination" && "$calls" -eq 1 ]]; then + echo "Unable to find a device matching the provided destination specifier:" + echo "The requested device could not be found because no available devices matched the request." + exit 70 +fi if [[ "${POCKETROOT_MOCK_XCODEBUILD_MODE:-retry}" == "assertion" ]]; then echo "Testing failed: expected value did not match" exit 65 @@ -214,6 +220,32 @@ if [[ "$(cat "$GENERIC_RUNNER_XCODEBUILD_CALLS")" != "2" ]] \ exit 1 fi +printf '0\n' > "$GENERIC_RUNNER_XCODEBUILD_CALLS" +: > "$GENERIC_RUNNER_XCRUN_CALLS" +: > "$GENERIC_RUNNER_BOOTSTATUS_CALLS" +: > "$GENERIC_RUNNER_BOOT_CALLS" +MISSING_DESTINATION_OUTPUT="$GENERIC_RUNNER_TEST_ROOT/missing-destination-output.txt" +PATH="$GENERIC_RUNNER_MOCK_BIN:$PATH" \ +POCKETROOT_UI_APP_DIR="$GENERIC_RUNNER_APP" \ +POCKETROOT_UI_PROJECT_NAME="MockApp" \ +POCKETROOT_UI_SCHEME="MockApp" \ +POCKETROOT_UI_TEST_BUNDLE="MockAppUITests" \ +POCKETROOT_CLONED_SOURCE_PACKAGES_DIR="$GENERIC_RUNNER_TEST_ROOT/packages" \ +POCKETROOT_MOCK_XCODEBUILD_CALLS="$GENERIC_RUNNER_XCODEBUILD_CALLS" \ +POCKETROOT_MOCK_XCRUN_CALLS="$GENERIC_RUNNER_XCRUN_CALLS" \ +POCKETROOT_MOCK_BOOTSTATUS_CALLS="$GENERIC_RUNNER_BOOTSTATUS_CALLS" \ +POCKETROOT_MOCK_BOOT_CALLS="$GENERIC_RUNNER_BOOT_CALLS" \ +POCKETROOT_MOCK_XCODEBUILD_MODE="missing-destination" \ + "$GENERIC_UI_RUNNER" "$GENERIC_RUNNER_ARCHIVE" \ + > "$MISSING_DESTINATION_OUTPUT" 2>&1 + +if [[ "$(cat "$GENERIC_RUNNER_XCODEBUILD_CALLS")" != "2" ]] \ + || [[ "$(grep -Fc -- 'simctl create' "$GENERIC_RUNNER_XCRUN_CALLS")" != "2" ]] \ + || ! grep -Fq -- 'destination disappeared' "$MISSING_DESTINATION_OUTPUT"; then + echo "Shared example UI runner did not recreate a disappeared temporary Simulator." >&2 + exit 1 +fi + printf '0\n' > "$GENERIC_RUNNER_XCODEBUILD_CALLS" : > "$GENERIC_RUNNER_XCRUN_CALLS" : > "$GENERIC_RUNNER_BOOT_CALLS" From dc68b785a3f3eb9ab794d6c677f53462ccd0c5e7 Mon Sep 17 00:00:00 2001 From: Jintao Date: Tue, 4 Aug 2026 03:32:19 +0800 Subject: [PATCH 4/7] Stabilize system document picker smoke --- CHANGELOG.en.md | 8 ++++++-- CHANGELOG.md | 6 ++++-- .../Release/experimental-v0.2.0/COMPOSITION.json | 2 +- Compliance/Release/experimental-v0.2.0/SBOM.spdx.json | 2 +- Compliance/Release/experimental-v0.2.0/SHA256SUMS | 4 ++-- .../UITests/PocketRootHostAppUITests.swift | 10 +++++++++- Tests/Scripts/ReleaseComplianceTests.rb | 8 ++++++-- 7 files changed, 29 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.en.md b/CHANGELOG.en.md index 28ddf60..e73cdad 100644 --- a/CHANGELOG.en.md +++ b/CHANGELOG.en.md @@ -84,8 +84,12 @@ All notable PocketRoot changes are recorded here. Semantic Versioning begins wit independent Simulators and `xcodebuild` invocations, retaining phase checkpoints, `.xcresult`, test output, and PocketRoot Simulator logs on failure while keeping a ten-minute hard limit per test. The system-file - round trip now waits within one 30-second window for either the local device - location or a restored Host folder. The shared UI runner restarts and retries + round trip now waits within one 60-second window for either the local device + location or a restored Host folder, covering the nearly minute-long system + picker recovery observed after a preceding share sheet and App relaunch. A + failed wait stops the helper immediately instead of recording a premature + assertion and then completing the round trip in that same picker. The shared + UI runner restarts and retries exactly once only when its own temporary Simulator test runner was not registered with FrontBoard; caller-supplied shared Simulators are not restarted, and assertion, timeout, and other build failures still fail diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a41cfe..34e2ecb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,8 +61,10 @@ PocketRoot 的重要变化记录在这里,并从首个公开版本开始遵循 - 将 Host App 的 Files/Workspace 与 PTY lifecycle UI smoke 拆分到独立模拟器和 `xcodebuild` 调用;失败时记录阶段检查点,并保留 `.xcresult`、测试输出和 PocketRoot 模拟器日志供 CI 下载分析,同时维持每项测试 10 分钟硬上限; - 系统文件 round-trip 测试也会在同一个 30 秒窗口内等待本机位置或已恢复的 - Host 目录,兼容文件选择器保存上次目录及慢速模拟器的合法状态。通用 UI runner + 系统文件 round-trip 测试也会在同一个 60 秒窗口内等待本机位置或已恢复的 + Host 目录,兼容文件选择器保存上次目录、前序 share sheet/App 重启后近一分钟的 + 系统页面恢复及慢速模拟器的合法状态。等待失败会立即停止辅助流程,避免先记录 + 过早断言、随后又在同一 picker 中完成 round-trip。通用 UI runner 只对 test runner 未注册到 FrontBoard 的模拟器基础设施错误重启自己创建的临时设备 并重试一次,调用方指定的共享 Simulator 不会被重启;测试 断言、超时和其他构建错误仍立即失败,重试或重启失败时同时保留首次与最终日志及 diff --git a/Compliance/Release/experimental-v0.2.0/COMPOSITION.json b/Compliance/Release/experimental-v0.2.0/COMPOSITION.json index 1f84f23..4e3cda8 100644 --- a/Compliance/Release/experimental-v0.2.0/COMPOSITION.json +++ b/Compliance/Release/experimental-v0.2.0/COMPOSITION.json @@ -120,7 +120,7 @@ "Examples/PocketRootDemo/Sources/PocketRootDemo/Shared/UI/PlaceholderViewController.swift": "48e0f6462228a66face2aef51f9bd39ec02923c4d1c01b6b34f920948998f2b8", "Examples/PocketRootHostApp/Sources/HostApp.swift": "0c36bd3104ddc5bebb9e23675a44a92e3ce4aec2c1461b3e182d86d1a75159c9", "Examples/PocketRootHostApp/Sources/Info.plist": "711b38923f11fdb4ceb7d28cb90a51674afec0a64786a20f9bd68e3f406f8d26", - "Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift": "2bc9c122dc9f8f5373a04b93ddd649d266046eceb1b1f6cdc8eca928baab9b3f", + "Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift": "8786b8d88241c1f3d37915f35d33a0660b4c2ca383a3276c567f1ac8be92479b", "Examples/PocketRootQuickStartApp/Sources/Info.plist": "b3c0c96e89db99c72c9f020590b1c5cdc0c927fd3af4b6ae7865abfdc81049b8", "Examples/PocketRootQuickStartApp/Sources/QuickStartApp.swift": "1c77c9229281d0d1f9fbe3678bcc9995fff8c7d2189f4c6152daaa2999683fb6", "Examples/PocketRootQuickStartApp/UITests/PocketRootQuickStartAppUITests.swift": "828471c84386dc8ddba23ecadc3618ccd559d92084ab762f4ef0832c3e3d5f59", diff --git a/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json b/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json index a5e280a..078a863 100644 --- a/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json +++ b/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json @@ -3,7 +3,7 @@ "dataLicense": "CC0-1.0", "SPDXID": "SPDXRef-DOCUMENT", "name": "PocketRoot experimental full-graph composition 0.2.0", - "documentNamespace": "https://github.com/jacklv-coder/PocketRoot/sbom/release/experimental-v0.2.0/cef6204218376c737d154e0386bdbed28b7bcc90c87aec8f172e02a154a05110", + "documentNamespace": "https://github.com/jacklv-coder/PocketRoot/sbom/release/experimental-v0.2.0/f17aad06f9d1ba3c3c9ec7f60f99f7180987edf6221331036356a4a69e699950", "creationInfo": { "created": "2026-08-03T00:00:00Z", "creators": [ diff --git a/Compliance/Release/experimental-v0.2.0/SHA256SUMS b/Compliance/Release/experimental-v0.2.0/SHA256SUMS index 57fd6a1..945f1ce 100644 --- a/Compliance/Release/experimental-v0.2.0/SHA256SUMS +++ b/Compliance/Release/experimental-v0.2.0/SHA256SUMS @@ -1,5 +1,5 @@ -cef6204218376c737d154e0386bdbed28b7bcc90c87aec8f172e02a154a05110 COMPOSITION.json +f17aad06f9d1ba3c3c9ec7f60f99f7180987edf6221331036356a4a69e699950 COMPOSITION.json 9fe270ba11160051d13f99cfe4ce6ee03c52882b88339dd6e4c1b4923ae30755 READINESS.json 6d41126608b019b46332a7957889f41e3c3ec6c6e9e2b884dca5ffc6d405e943 README.md fab07621b5188930bfd2a38117e206815d94f755e667d65fc182ee55d14d2d40 RELEASE-CHECKLIST.md -3d3b61a27db6de067e6610dac598032cbc1a839a6af0c3cb625b115e44a995b9 SBOM.spdx.json +e721d2b277c00e620b1ecb591fc8daa1ba9f48b075b8c5024b3812d8f2c94dcf SBOM.spdx.json diff --git a/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift b/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift index 737e398..6b43db1 100644 --- a/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift +++ b/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift @@ -990,7 +990,15 @@ final class PocketRootHostAppUITests: XCTestCase { hostDocumentLabels + localLocationLabels ) ).firstMatch - XCTAssertTrue(pickerLanding.waitForExistence(timeout: 30)) + // On the minimum Xcode 16 runner, the third presentation can take + // almost a minute to restore the picker after the preceding share + // sheet and App relaunch. The picker did eventually expose the local + // location and the round trip completed, so keep one bounded wait + // instead of recording a premature failure and continuing to tap. + guard pickerLanding.waitForExistence(timeout: 60) else { + XCTFail("document picker to expose a local or host destination") + return + } if currentHostDocuments.exists { return } diff --git a/Tests/Scripts/ReleaseComplianceTests.rb b/Tests/Scripts/ReleaseComplianceTests.rb index 2b8695b..3ac3df1 100644 --- a/Tests/Scripts/ReleaseComplianceTests.rb +++ b/Tests/Scripts/ReleaseComplianceTests.rb @@ -1182,7 +1182,11 @@ def test_standalone_host_has_real_pty_and_files_ui_smoke ui_test, '"FullDocumentManagerViewControllerNavigationBar"' ) - assert_includes ui_test, "pickerLanding.waitForExistence(timeout: 30)" + assert_includes ui_test, "pickerLanding.waitForExistence(timeout: 60)" + assert_includes( + ui_test, + 'XCTFail("document picker to expose a local or host destination")' + ) assert_includes ui_test, "for _ in 0..<2" assert_includes ui_test, "hostDestination.waitForExistence(timeout: 10)" assert_includes ui_test, "openedHostDestination" @@ -1194,7 +1198,7 @@ def test_standalone_host_has_real_pty_and_files_ui_smoke 'NSPredicate(format: "label BEGINSWITH %@", "PocketRoot Host")' ) assert_operator( - ui_test.index("pickerLanding.waitForExistence(timeout: 30)"), + ui_test.index("pickerLanding.waitForExistence(timeout: 60)"), :<, ui_test.index("localLocation.waitForExistence(timeout: 30)") ) From 9fe09f8200c225cc42027ffeef9c524f5e13465c Mon Sep 17 00:00:00 2001 From: Jintao Date: Tue, 4 Aug 2026 04:53:32 +0800 Subject: [PATCH 5/7] Stabilize document picker readiness --- CHANGELOG.en.md | 13 +- CHANGELOG.md | 9 +- .../experimental-v0.2.0/COMPOSITION.json | 2 +- .../experimental-v0.2.0/SBOM.spdx.json | 2 +- .../Release/experimental-v0.2.0/SHA256SUMS | 4 +- .../UITests/PocketRootHostAppUITests.swift | 118 +++++++++++------- Tests/Scripts/ReleaseComplianceTests.rb | 22 +++- 7 files changed, 109 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.en.md b/CHANGELOG.en.md index e73cdad..744bd2b 100644 --- a/CHANGELOG.en.md +++ b/CHANGELOG.en.md @@ -84,11 +84,14 @@ All notable PocketRoot changes are recorded here. Semantic Versioning begins wit independent Simulators and `xcodebuild` invocations, retaining phase checkpoints, `.xcresult`, test output, and PocketRoot Simulator logs on failure while keeping a ten-minute hard limit per test. The system-file - round trip now waits within one 60-second window for either the local device - location or a restored Host folder, covering the nearly minute-long system - picker recovery observed after a preceding share sheet and App relaunch. A - failed wait stops the helper immediately instead of recording a premature - assertion and then completing the round trip in that same picker. The shared + round trip now uses one 60-second state loop for the Host fixture, a local + location, or an interactive `Browse` transition instead of mistaking an early + navigation bar for readiness. If the picker is still on Recents, it taps the + `Browse` tab, which can enter the Xcode 16 accessibility tree late; a picker + already in the Host folder does not return until the fixture file is actually + visible. The helper + returns an explicit success state so a failed navigation ends the test case + without cascading taps and secondary failures. The shared UI runner restarts and retries exactly once only when its own temporary Simulator test runner was not registered with FrontBoard; caller-supplied shared Simulators are not diff --git a/CHANGELOG.md b/CHANGELOG.md index 34e2ecb..b1ebda7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,10 +61,11 @@ PocketRoot 的重要变化记录在这里,并从首个公开版本开始遵循 - 将 Host App 的 Files/Workspace 与 PTY lifecycle UI smoke 拆分到独立模拟器和 `xcodebuild` 调用;失败时记录阶段检查点,并保留 `.xcresult`、测试输出和 PocketRoot 模拟器日志供 CI 下载分析,同时维持每项测试 10 分钟硬上限; - 系统文件 round-trip 测试也会在同一个 60 秒窗口内等待本机位置或已恢复的 - Host 目录,兼容文件选择器保存上次目录、前序 share sheet/App 重启后近一分钟的 - 系统页面恢复及慢速模拟器的合法状态。等待失败会立即停止辅助流程,避免先记录 - 过早断言、随后又在同一 picker 中完成 round-trip。通用 UI runner + 系统文件 round-trip 测试也会在一个 60 秒状态循环中等待 Host fixture、本机位置 + 或可交互的 `Browse/浏览`,而不会把提前出现的 picker 导航栏误判为就绪; + 当 picker 停在 Recents 时,点击可能延迟进入 accessibility tree 的 Browse, + 而已经位于 Host 目录时会等到 fixture 文件真正可见才返回。辅助函数返回 + 明确的成功状态,失败后立即停止当前用例,避免后续点击产生连锁误报。通用 UI runner 只对 test runner 未注册到 FrontBoard 的模拟器基础设施错误重启自己创建的临时设备 并重试一次,调用方指定的共享 Simulator 不会被重启;测试 断言、超时和其他构建错误仍立即失败,重试或重启失败时同时保留首次与最终日志及 diff --git a/Compliance/Release/experimental-v0.2.0/COMPOSITION.json b/Compliance/Release/experimental-v0.2.0/COMPOSITION.json index 4e3cda8..2c0c852 100644 --- a/Compliance/Release/experimental-v0.2.0/COMPOSITION.json +++ b/Compliance/Release/experimental-v0.2.0/COMPOSITION.json @@ -120,7 +120,7 @@ "Examples/PocketRootDemo/Sources/PocketRootDemo/Shared/UI/PlaceholderViewController.swift": "48e0f6462228a66face2aef51f9bd39ec02923c4d1c01b6b34f920948998f2b8", "Examples/PocketRootHostApp/Sources/HostApp.swift": "0c36bd3104ddc5bebb9e23675a44a92e3ce4aec2c1461b3e182d86d1a75159c9", "Examples/PocketRootHostApp/Sources/Info.plist": "711b38923f11fdb4ceb7d28cb90a51674afec0a64786a20f9bd68e3f406f8d26", - "Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift": "8786b8d88241c1f3d37915f35d33a0660b4c2ca383a3276c567f1ac8be92479b", + "Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift": "9b033fdd03d811c69f64e8f98bddaf470077e6cc6083c658b3b874c73c7dfbe5", "Examples/PocketRootQuickStartApp/Sources/Info.plist": "b3c0c96e89db99c72c9f020590b1c5cdc0c927fd3af4b6ae7865abfdc81049b8", "Examples/PocketRootQuickStartApp/Sources/QuickStartApp.swift": "1c77c9229281d0d1f9fbe3678bcc9995fff8c7d2189f4c6152daaa2999683fb6", "Examples/PocketRootQuickStartApp/UITests/PocketRootQuickStartAppUITests.swift": "828471c84386dc8ddba23ecadc3618ccd559d92084ab762f4ef0832c3e3d5f59", diff --git a/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json b/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json index 078a863..c86fd71 100644 --- a/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json +++ b/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json @@ -3,7 +3,7 @@ "dataLicense": "CC0-1.0", "SPDXID": "SPDXRef-DOCUMENT", "name": "PocketRoot experimental full-graph composition 0.2.0", - "documentNamespace": "https://github.com/jacklv-coder/PocketRoot/sbom/release/experimental-v0.2.0/f17aad06f9d1ba3c3c9ec7f60f99f7180987edf6221331036356a4a69e699950", + "documentNamespace": "https://github.com/jacklv-coder/PocketRoot/sbom/release/experimental-v0.2.0/319dd0806954ae0eda60b2b13f7f2442f461081a554439df8a7b83633f5d04ac", "creationInfo": { "created": "2026-08-03T00:00:00Z", "creators": [ diff --git a/Compliance/Release/experimental-v0.2.0/SHA256SUMS b/Compliance/Release/experimental-v0.2.0/SHA256SUMS index 945f1ce..97d5742 100644 --- a/Compliance/Release/experimental-v0.2.0/SHA256SUMS +++ b/Compliance/Release/experimental-v0.2.0/SHA256SUMS @@ -1,5 +1,5 @@ -f17aad06f9d1ba3c3c9ec7f60f99f7180987edf6221331036356a4a69e699950 COMPOSITION.json +319dd0806954ae0eda60b2b13f7f2442f461081a554439df8a7b83633f5d04ac COMPOSITION.json 9fe270ba11160051d13f99cfe4ce6ee03c52882b88339dd6e4c1b4923ae30755 READINESS.json 6d41126608b019b46332a7957889f41e3c3ec6c6e9e2b884dca5ffc6d405e943 README.md fab07621b5188930bfd2a38117e206815d94f755e667d65fc182ee55d14d2d40 RELEASE-CHECKLIST.md -e721d2b277c00e620b1ecb591fc8daa1ba9f48b075b8c5024b3812d8f2c94dcf SBOM.spdx.json +4e2c161a1355234f23513b6532a4dd7acc23344628b18144a6a52e69eee451d2 SBOM.spdx.json diff --git a/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift b/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift index 6b43db1..33618c0 100644 --- a/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift +++ b/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift @@ -215,7 +215,9 @@ final class PocketRootHostAppUITests: XCTestCase { } importFile.tap() - openHostDocuments(in: app) + guard openHostDocuments(in: app) else { + return + } let fixture = app.cells[ "\(Self.systemImportFixtureDisplayName), txt" ] @@ -252,7 +254,9 @@ final class PocketRootHostAppUITests: XCTestCase { withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5) ).tap() - openHostDocuments(in: app) + guard openHostDocuments(in: app) else { + return + } let save = app.buttons.matching( NSPredicate(format: "label IN %@", ["Save", "存储"]) ).firstMatch @@ -301,7 +305,9 @@ final class PocketRootHostAppUITests: XCTestCase { return } importFile.tap() - openHostDocuments(in: app) + guard openHostDocuments(in: app) else { + return + } let exported = app.cells[ "\(Self.systemImportFixtureDisplayName), txt" ] @@ -922,23 +928,10 @@ final class PocketRootHostAppUITests: XCTestCase { ) } - private func openHostDocuments(in app: XCUIApplication) { + private func openHostDocuments(in app: XCUIApplication) -> Bool { let browse = app.buttons.matching( NSPredicate(format: "label IN %@", ["Browse", "浏览"]) ).firstMatch - if browse.waitForExistence(timeout: 10), - let frames = waitForInteractionFrames( - of: browse, - in: app, - timeout: 3 - ) - { - tapFrame( - frames.elementFrame, - in: frames.appFrame, - using: app - ) - } // The iOS document picker preserves its last visited directory. On a // later import or export in this test it can reopen directly inside @@ -981,26 +974,60 @@ final class PocketRootHostAppUITests: XCTestCase { localLocationLabels ) ).firstMatch - let localLocation = sidebarLocalLocation.waitForExistence(timeout: 5) - ? sidebarLocalLocation - : fallbackLocalLocation - let pickerLanding = app.descendants(matching: .any).matching( - NSPredicate( - format: "label IN %@", - hostDocumentLabels + localLocationLabels - ) - ).firstMatch - // On the minimum Xcode 16 runner, the third presentation can take - // almost a minute to restore the picker after the preceding share - // sheet and App relaunch. The picker did eventually expose the local - // location and the round trip completed, so keep one bounded wait - // instead of recording a premature failure and continuing to tap. - guard pickerLanding.waitForExistence(timeout: 60) else { - XCTFail("document picker to expose a local or host destination") - return + let hostFixture = app.cells[ + "\(Self.systemImportFixtureDisplayName), txt" + ] + // The minimum Xcode 16 runner can present the document manager on + // Recents before its Browse tab enters the accessibility tree, while a + // restored Host location can also expose its navigation bar before its + // file list. Spend one deadline waiting for either the fixture itself, + // a local location, or a usable Browse transition. + let pickerDeadline = Date().addingTimeInterval(60) + var localLocation: XCUIElement? + var didTapBrowse = false + while Date() < pickerDeadline { + if currentHostDocuments.exists { + if hostFixture.exists { + return true + } + // An iPad sidebar can keep the local location visible while + // the restored Host file list is still loading. Once the Host + // navigation state is present, stay there instead of letting + // the sidebar take us back out of the destination. + RunLoop.current.run( + until: Date().addingTimeInterval(0.2) + ) + continue + } + if sidebarLocalLocation.exists { + localLocation = sidebarLocalLocation + break + } + if fallbackLocalLocation.exists { + localLocation = fallbackLocalLocation + break + } + if !didTapBrowse, browse.exists { + guard let frames = waitForInteractionFrames( + of: browse, + in: app, + timeout: 10 + ) else { + XCTFail("document picker Browse to become hittable") + return false + } + tapFrame( + frames.elementFrame, + in: frames.appFrame, + using: app + ) + didTapBrowse = true + } + RunLoop.current.run(until: Date().addingTimeInterval(0.2)) } - if currentHostDocuments.exists { - return + guard let localLocation else { + XCTFail("document picker to expose a usable Host or local destination") + return false } let hostDocuments = app.cells.matching( @@ -1043,17 +1070,20 @@ final class PocketRootHostAppUITests: XCTestCase { "local document location to reveal the host container" ) guard openedHostDestination else { - return + return false } - if currentHostDocuments.exists { - return + if !currentHostDocuments.exists { + XCTAssertTrue(hostDocuments.exists) + guard hostDocuments.exists else { + return false + } + hostDocuments.tap() } - - XCTAssertTrue(hostDocuments.exists) - guard hostDocuments.exists else { - return + guard hostFixture.waitForExistence(timeout: 30) else { + XCTFail("Host Documents fixture to become visible") + return false } - hostDocuments.tap() + return true } private func dismissShareSheetIfNeeded( diff --git a/Tests/Scripts/ReleaseComplianceTests.rb b/Tests/Scripts/ReleaseComplianceTests.rb index 3ac3df1..94dfe45 100644 --- a/Tests/Scripts/ReleaseComplianceTests.rb +++ b/Tests/Scripts/ReleaseComplianceTests.rb @@ -1182,11 +1182,20 @@ def test_standalone_host_has_real_pty_and_files_ui_smoke ui_test, '"FullDocumentManagerViewControllerNavigationBar"' ) - assert_includes ui_test, "pickerLanding.waitForExistence(timeout: 60)" + assert_includes ui_test, "let pickerDeadline = Date().addingTimeInterval(60)" assert_includes( ui_test, - 'XCTFail("document picker to expose a local or host destination")' + 'XCTFail("document picker to expose a usable Host or local destination")' ) + assert_includes ui_test, "while Date() < pickerDeadline" + assert_includes ui_test, "if currentHostDocuments.exists {" + assert_includes ui_test, "if hostFixture.exists {" + assert_includes ui_test, "continue" + assert_includes ui_test, "var didTapBrowse = false" + assert_includes ui_test, 'XCTFail("document picker Browse to become hittable")' + assert_includes ui_test, "hostFixture.waitForExistence(timeout: 30)" + assert_includes ui_test, 'XCTFail("Host Documents fixture to become visible")' + assert_equal 3, ui_test.scan("guard openHostDocuments(in: app) else").length assert_includes ui_test, "for _ in 0..<2" assert_includes ui_test, "hostDestination.waitForExistence(timeout: 10)" assert_includes ui_test, "openedHostDestination" @@ -1198,9 +1207,14 @@ def test_standalone_host_has_real_pty_and_files_ui_smoke 'NSPredicate(format: "label BEGINSWITH %@", "PocketRoot Host")' ) assert_operator( - ui_test.index("pickerLanding.waitForExistence(timeout: 60)"), + ui_test.index("let pickerDeadline = Date().addingTimeInterval(60)"), :<, - ui_test.index("localLocation.waitForExistence(timeout: 30)") + ui_test.index("hostFixture.waitForExistence(timeout: 30)") + ) + assert_operator( + ui_test.index("if currentHostDocuments.exists {"), + :<, + ui_test.index("if sidebarLocalLocation.exists {") ) assert_includes ui_test, "testPTYLifecycleAndShutdown" assert_includes ui_test, "dismissKeyboardOnboardingIfPresent(in: app)" From 983637366ec20f033bf7f42754d58c8b321cfb11 Mon Sep 17 00:00:00 2001 From: Jintao Date: Tue, 4 Aug 2026 05:48:17 +0800 Subject: [PATCH 6/7] Stabilize Xcode 16 CI recovery --- .../actions/setup-minimum-xcode-16/action.yml | 7 +++ .github/workflows/ci.yml | 10 ++++ CHANGELOG.en.md | 11 ++-- CHANGELOG.md | 9 ++-- .../experimental-v0.2.0/COMPOSITION.json | 4 +- .../experimental-v0.2.0/SBOM.spdx.json | 2 +- .../Release/experimental-v0.2.0/SHA256SUMS | 4 +- Docs/Testing.md | 5 +- Docs/en/Testing.md | 10 ++-- .../UITests/PocketRootHostAppUITests.swift | 53 ++++++++++++++----- Scripts/generate-release-compliance.rb | 2 +- Scripts/run-ios-example-ui-smoke.sh | 2 +- Tests/Scripts/ReleaseComplianceTests.rb | 32 +++++++++++ Tests/Scripts/RuntimeSmokeScriptTests.sh | 2 +- 14 files changed, 120 insertions(+), 33 deletions(-) diff --git a/.github/actions/setup-minimum-xcode-16/action.yml b/.github/actions/setup-minimum-xcode-16/action.yml index adfea98..8a89d62 100644 --- a/.github/actions/setup-minimum-xcode-16/action.yml +++ b/.github/actions/setup-minimum-xcode-16/action.yml @@ -1,6 +1,12 @@ name: Set up minimum Xcode 16 environment description: Select Xcode 16.0 and install the pinned RootFS, XcodeGen, and iOS 18 Simulator runtime. +inputs: + install-simulator-runtime: + description: Install and verify the iOS 18.0 Simulator runtime during setup. + required: false + default: "true" + runs: using: composite steps: @@ -55,6 +61,7 @@ runs: echo "$install_root/xcodegen/bin" >> "$GITHUB_PATH" - name: Install iOS 18.0 Simulator runtime + if: ${{ inputs.install-simulator-runtime == 'true' }} shell: bash run: | xcrun simctl list >/dev/null diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e40589c..acf73e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -302,12 +302,22 @@ jobs: - name: Set up minimum Xcode 16 environment uses: ./.github/actions/setup-minimum-xcode-16 + with: + install-simulator-runtime: "false" - name: Validate real RootFS install with Xcode 16.0 run: | swift test \ --filter testPinnedReleaseArchiveWhenProvidedByEnvironment + - name: Install iOS 18.0 Simulator runtime + run: | + xcrun simctl list >/dev/null + xcodebuild -downloadPlatform iOS + xcrun simctl list runtimes available + xcrun simctl list runtimes available \ + | grep -F "iOS 18.0" + - name: Final-link runtime with Xcode 16.0 run: ./Scripts/build-runtime-spike.sh diff --git a/CHANGELOG.en.md b/CHANGELOG.en.md index 744bd2b..eb589a3 100644 --- a/CHANGELOG.en.md +++ b/CHANGELOG.en.md @@ -89,15 +89,18 @@ All notable PocketRoot changes are recorded here. Semantic Versioning begins wit navigation bar for readiness. If the picker is still on Recents, it taps the `Browse` tab, which can enter the Xcode 16 accessibility tree late; a picker already in the Host folder does not return until the fixture file is actually - visible. The helper - returns an explicit success state so a failed navigation ends the test case - without cascading taps and secondary failures. The shared + visible. Fixture selection uses a verified current frame and retries once if + iOS 18.0 leaves the picker open after reporting a synthesized tap. The helper + returns an explicit success state so a failed navigation or selection ends + the test case without cascading taps and secondary failures. The shared UI runner restarts and retries exactly once only when its own temporary Simulator test runner was not registered with FrontBoard; caller-supplied shared Simulators are not restarted, and assertion, timeout, and other build failures still fail immediately. If retry or restart fails, both attempt logs and available - `.xcresult` bundles are retained. +- The native Xcode 16 lane validates the real RootFS install before downloading + the multi-gigabyte Simulator runtime, preserving the disk headroom enforced + by the installer while UI lanes retain the shared one-step setup. ## 0.1.0 - 2026-07-31 diff --git a/CHANGELOG.md b/CHANGELOG.md index b1ebda7..4c3ceb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,12 +64,15 @@ PocketRoot 的重要变化记录在这里,并从首个公开版本开始遵循 系统文件 round-trip 测试也会在一个 60 秒状态循环中等待 Host fixture、本机位置 或可交互的 `Browse/浏览`,而不会把提前出现的 picker 导航栏误判为就绪; 当 picker 停在 Recents 时,点击可能延迟进入 accessibility tree 的 Browse, - 而已经位于 Host 目录时会等到 fixture 文件真正可见才返回。辅助函数返回 - 明确的成功状态,失败后立即停止当前用例,避免后续点击产生连锁误报。通用 UI runner + 而已经位于 Host 目录时会等到 fixture 文件真正可见才返回。fixture 选择使用当前 + 已验证 frame;若 iOS 18.0 在报告合成 tap 后仍保留 picker,则有界重试一次。辅助函数 + 返回明确的成功状态,导航或选择失败后立即停止当前用例,避免后续点击产生连锁误报。 + 通用 UI runner 只对 test runner 未注册到 FrontBoard 的模拟器基础设施错误重启自己创建的临时设备 并重试一次,调用方指定的共享 Simulator 不会被重启;测试 断言、超时和其他构建错误仍立即失败,重试或重启失败时同时保留首次与最终日志及 - 可用的两次 `.xcresult`。 +- Xcode 16 native lane 会先验证真实 RootFS 安装,再下载数 GB 的 Simulator runtime, + 为安装器的磁盘余量门禁保留空间;UI lanes 继续使用统一的一步 setup。 ## 0.1.0 - 2026-07-31 diff --git a/Compliance/Release/experimental-v0.2.0/COMPOSITION.json b/Compliance/Release/experimental-v0.2.0/COMPOSITION.json index 2c0c852..b0bf2a3 100644 --- a/Compliance/Release/experimental-v0.2.0/COMPOSITION.json +++ b/Compliance/Release/experimental-v0.2.0/COMPOSITION.json @@ -24,7 +24,7 @@ "Scripts/inject-demo-rootfs.sh": "3982b5382b0d1e13e0c8e8a5bb5404c5bad1dfc4d6e9cd23a39e3395a83087bb", "Scripts/scan-release-artifact.rb": "48f27ab280491864228393e1675fe3a2889cbd616f79e3cb16bae7efedf647c0", "Scripts/run-host-app-ui-smoke.sh": "c8b11b1bf3a467fd10ed2c4124a72396b0a803e98f155d1d709fab46b59eede1", - "Scripts/run-ios-example-ui-smoke.sh": "47f2b34bec554a819c3b580ca045f4d31bdae2c7379470f5495f21dde58994dd", + "Scripts/run-ios-example-ui-smoke.sh": "147fa58dc255e28fd63b0a7c62ed747b2215df652df3a0bdf5585eee4b49d395", "Scripts/run-quick-start-ui-smoke.sh": "acf26d68fc37250e24911c2d23791a0d12ff06a624dedcefee0ed7905bc31af2", "Scripts/run-external-consumer-ui-smoke.sh": "d9544ede3ecbad8d42a114015fa77071d31aa84aafc1cbe0f0f001453b1a4d0d", "Scripts/run-host-app-device-ui-smoke.sh": "2f934265cb46145b27bb26e0d7e84acebc14290602e23cd1ab8f222aef68ed1f", @@ -120,7 +120,7 @@ "Examples/PocketRootDemo/Sources/PocketRootDemo/Shared/UI/PlaceholderViewController.swift": "48e0f6462228a66face2aef51f9bd39ec02923c4d1c01b6b34f920948998f2b8", "Examples/PocketRootHostApp/Sources/HostApp.swift": "0c36bd3104ddc5bebb9e23675a44a92e3ce4aec2c1461b3e182d86d1a75159c9", "Examples/PocketRootHostApp/Sources/Info.plist": "711b38923f11fdb4ceb7d28cb90a51674afec0a64786a20f9bd68e3f406f8d26", - "Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift": "9b033fdd03d811c69f64e8f98bddaf470077e6cc6083c658b3b874c73c7dfbe5", + "Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift": "a7de763ee3e30510051645cbfac7b73167bcbca4b0cb1332004dcfeda8d487d2", "Examples/PocketRootQuickStartApp/Sources/Info.plist": "b3c0c96e89db99c72c9f020590b1c5cdc0c927fd3af4b6ae7865abfdc81049b8", "Examples/PocketRootQuickStartApp/Sources/QuickStartApp.swift": "1c77c9229281d0d1f9fbe3678bcc9995fff8c7d2189f4c6152daaa2999683fb6", "Examples/PocketRootQuickStartApp/UITests/PocketRootQuickStartAppUITests.swift": "828471c84386dc8ddba23ecadc3618ccd559d92084ab762f4ef0832c3e3d5f59", diff --git a/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json b/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json index c86fd71..5fa9edf 100644 --- a/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json +++ b/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json @@ -3,7 +3,7 @@ "dataLicense": "CC0-1.0", "SPDXID": "SPDXRef-DOCUMENT", "name": "PocketRoot experimental full-graph composition 0.2.0", - "documentNamespace": "https://github.com/jacklv-coder/PocketRoot/sbom/release/experimental-v0.2.0/319dd0806954ae0eda60b2b13f7f2442f461081a554439df8a7b83633f5d04ac", + "documentNamespace": "https://github.com/jacklv-coder/PocketRoot/sbom/release/experimental-v0.2.0/226b1537aab6f0e10e274801df7448fd5535519feaecb4e4ead72afcd8f9c17b", "creationInfo": { "created": "2026-08-03T00:00:00Z", "creators": [ diff --git a/Compliance/Release/experimental-v0.2.0/SHA256SUMS b/Compliance/Release/experimental-v0.2.0/SHA256SUMS index 97d5742..71888e9 100644 --- a/Compliance/Release/experimental-v0.2.0/SHA256SUMS +++ b/Compliance/Release/experimental-v0.2.0/SHA256SUMS @@ -1,5 +1,5 @@ -319dd0806954ae0eda60b2b13f7f2442f461081a554439df8a7b83633f5d04ac COMPOSITION.json +226b1537aab6f0e10e274801df7448fd5535519feaecb4e4ead72afcd8f9c17b COMPOSITION.json 9fe270ba11160051d13f99cfe4ce6ee03c52882b88339dd6e4c1b4923ae30755 READINESS.json 6d41126608b019b46332a7957889f41e3c3ec6c6e9e2b884dca5ffc6d405e943 README.md fab07621b5188930bfd2a38117e206815d94f755e667d65fc182ee55d14d2d40 RELEASE-CHECKLIST.md -4e2c161a1355234f23513b6532a4dd7acc23344628b18144a6a52e69eee451d2 SBOM.spdx.json +dd4f166c4e6a18bb7c76ff99f76ccee376354abe2efa17e35396333e434afeb1 SBOM.spdx.json diff --git a/Docs/Testing.md b/Docs/Testing.md index b7a655e..431e574 100644 --- a/Docs/Testing.md +++ b/Docs/Testing.md @@ -562,8 +562,9 @@ RootFS。这只是兼容性证据,不授权 RootFS 分发,也不改变正式 最低工具链门禁拆为一个 native runtime job 和五路并行 UI matrix job。每个 job 都通过 仓库内固定的 composite action 独立选择 Xcode 16.0 / iOS 18.0 SDK、校验并取得同一 -RootFS、安装同一 XcodeGen 与 iOS 18.0 Simulator runtime;job 之间不传递未审计的 -App、RootFS 或 DerivedData。Native job 验证真实 RootFS install、完成 +RootFS、安装同一 XcodeGen,并取得同一 iOS 18.0 Simulator runtime;job 之间不传递 +未审计的 App、RootFS 或 DerivedData。Native job 在下载数 GB 的 Simulator runtime +之前验证真实 RootFS install,为安装器的磁盘余量门禁保留空间,然后完成 Simulator/device final-link 并执行 17 项原生 smoke。UI matrix 设置 `fail-fast: false`,分别运行公开 SHA 外部消费者、iPhone/iPad Quick Start 和 iPhone/iPad Host App,因此单路失败不会取消其他证据;每路使用不冲突的失败制品名。 diff --git a/Docs/en/Testing.md b/Docs/en/Testing.md index 95862aa..4db2ccd 100644 --- a/Docs/en/Testing.md +++ b/Docs/en/Testing.md @@ -502,10 +502,12 @@ uploads neither the App nor scan evidence. The minimum-toolchain gate is split into one native-runtime job and a five-way parallel UI matrix. Every job independently uses the repository-owned composite action to select Xcode 16.0 / iOS 18.0 SDK, verify and obtain the -same RootFS, and install the same XcodeGen and iOS 18.0 Simulator runtime. Jobs -do not transfer an unreviewed App, RootFS, or DerivedData between runners. The -native job validates real RootFS installation, final-links Simulator/device -Apps, and runs the 17-check native smoke. The `fail-fast: false` UI matrix runs +same RootFS, install the same XcodeGen, and obtain the same iOS 18.0 Simulator +runtime. Jobs do not transfer an unreviewed App, RootFS, or DerivedData between +runners. The native job validates real RootFS installation before downloading +the multi-gigabyte Simulator runtime, preserving the installer's required disk +headroom, then final-links Simulator/device Apps and runs the 17-check native +smoke. The `fail-fast: false` UI matrix runs the public-SHA external consumer, iPhone/iPad Quick Start, and iPhone/iPad Host App independently, so one failure does not cancel the remaining evidence and each lane has a collision-free diagnostic artifact name. These lanes execute diff --git a/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift b/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift index 33618c0..f01b052 100644 --- a/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift +++ b/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift @@ -218,16 +218,13 @@ final class PocketRootHostAppUITests: XCTestCase { guard openHostDocuments(in: app) else { return } - let fixture = app.cells[ - "\(Self.systemImportFixtureDisplayName), txt" - ] - XCTAssertTrue(fixture.waitForExistence(timeout: 30)) - fixture.tap() + guard importHostFixture(in: app) else { + return + } let imported = app.descendants(matching: .any)[ "PocketRootFiles.entry./root/\(Self.systemImportFixtureName)" ] - XCTAssertTrue(imported.waitForExistence(timeout: 30)) guard revealFileEntry(imported, in: app) else { return } @@ -308,16 +305,13 @@ final class PocketRootHostAppUITests: XCTestCase { guard openHostDocuments(in: app) else { return } - let exported = app.cells[ - "\(Self.systemImportFixtureDisplayName), txt" - ] - XCTAssertTrue(exported.waitForExistence(timeout: 30)) - exported.tap() + guard importHostFixture(in: app) else { + return + } let reimported = app.descendants(matching: .any)[ "PocketRootFiles.entry./root/\(Self.systemImportFixtureName)" ] - XCTAssertTrue(reimported.waitForExistence(timeout: 30)) guard revealFileEntry(reimported, in: app) else { return } @@ -1086,6 +1080,41 @@ final class PocketRootHostAppUITests: XCTestCase { return true } + private func importHostFixture(in app: XCUIApplication) -> Bool { + let fixture = app.cells[ + "\(Self.systemImportFixtureDisplayName), txt" + ] + let imported = app.descendants(matching: .any)[ + "PocketRootFiles.entry./root/\(Self.systemImportFixtureName)" + ] + + // Xcode 16 can synthesize a successful tap while the iOS 18.0 + // document picker keeps the file cell open and never completes the + // selection. Re-query the current frame and retry once only while the + // guest file is still absent. + for _ in 0..<2 { + guard let frames = waitForInteractionFrames( + of: fixture, + in: app, + timeout: 10 + ) else { + XCTFail("Host Documents fixture to expose an interaction frame") + return false + } + tapFrame( + frames.elementFrame, + in: frames.appFrame, + using: app + ) + if imported.waitForExistence(timeout: 15) { + return true + } + } + + XCTFail("Host Documents fixture selection to import the guest file") + return false + } + private func dismissShareSheetIfNeeded( in app: XCUIApplication ) -> Bool { diff --git a/Scripts/generate-release-compliance.rb b/Scripts/generate-release-compliance.rb index 1c3acbc..7b50e58 100644 --- a/Scripts/generate-release-compliance.rb +++ b/Scripts/generate-release-compliance.rb @@ -281,7 +281,7 @@ module PocketRootReleaseCompliance "Scripts/run-host-app-ui-smoke.sh" => "c8b11b1bf3a467fd10ed2c4124a72396b0a803e98f155d1d709fab46b59eede1", "Scripts/run-ios-example-ui-smoke.sh" => - "47f2b34bec554a819c3b580ca045f4d31bdae2c7379470f5495f21dde58994dd", + "147fa58dc255e28fd63b0a7c62ed747b2215df652df3a0bdf5585eee4b49d395", "Scripts/run-quick-start-ui-smoke.sh" => "acf26d68fc37250e24911c2d23791a0d12ff06a624dedcefee0ed7905bc31af2", "Scripts/run-external-consumer-ui-smoke.sh" => diff --git a/Scripts/run-ios-example-ui-smoke.sh b/Scripts/run-ios-example-ui-smoke.sh index a99f229..edee8d7 100755 --- a/Scripts/run-ios-example-ui-smoke.sh +++ b/Scripts/run-ios-example-ui-smoke.sh @@ -138,7 +138,7 @@ retryable_simulator_failure_kind() { printf 'frontboard-launch\n' return 0 fi - if grep -Fq 'Unable to find a device matching the provided destination specifier' \ + if grep -Fq 'Unable to find a destination matching the provided destination specifier' \ "$TEST_LOG_PATH" && grep -Fq 'no available devices matched the request' "$TEST_LOG_PATH"; then printf 'missing-destination\n' diff --git a/Tests/Scripts/ReleaseComplianceTests.rb b/Tests/Scripts/ReleaseComplianceTests.rb index 94dfe45..33c483a 100644 --- a/Tests/Scripts/ReleaseComplianceTests.rb +++ b/Tests/Scripts/ReleaseComplianceTests.rb @@ -1039,6 +1039,12 @@ def test_minimum_xcode_ci_parallelizes_native_and_ui_coverage refute_match(/^ minimum-xcode-16:$/, workflow) ui_job_header = workflow.split(/^ minimum-xcode-16-ui:\n/, 2).fetch(1).lines.first(5).join + runtime_job = + workflow + .split(/^ minimum-xcode-16-runtime:\n/, 2) + .fetch(1) + .split(/^ minimum-xcode-16-ui:\n/, 2) + .first assert_includes ui_job_header, "timeout-minutes: 60" assert_includes workflow, "fail-fast: false" assert_equal( @@ -1070,8 +1076,24 @@ def test_minimum_xcode_ci_parallelizes_native_and_ui_coverage 'ROOTFS_SHA256: "be0f3c133f78f28b023288459b33dc28fa253a6ef29f7123bc5f3892edf90ad4"' ) assert_includes setup_action, 'XCODEGEN_VERSION: "2.46.0"' + assert_includes setup_action, "install-simulator-runtime:" + assert_includes( + setup_action, + "if: ${{ inputs.install-simulator-runtime == 'true' }}" + ) assert_includes setup_action, "xcodebuild -downloadPlatform iOS" assert_includes setup_action, 'grep -F "iOS 18.0"' + assert_includes runtime_job, 'install-simulator-runtime: "false"' + assert_operator( + runtime_job.index("Validate real RootFS install with Xcode 16.0"), + :<, + runtime_job.index("Install iOS 18.0 Simulator runtime") + ) + assert_operator( + runtime_job.index("Install iOS 18.0 Simulator runtime"), + :<, + runtime_job.index("Final-link runtime with Xcode 16.0") + ) end def test_standalone_host_retains_runtime_across_scene_recreation @@ -1196,6 +1218,16 @@ def test_standalone_host_has_real_pty_and_files_ui_smoke assert_includes ui_test, "hostFixture.waitForExistence(timeout: 30)" assert_includes ui_test, 'XCTFail("Host Documents fixture to become visible")' assert_equal 3, ui_test.scan("guard openHostDocuments(in: app) else").length + assert_equal 2, ui_test.scan("guard importHostFixture(in: app) else").length + assert_includes ui_test, "private func importHostFixture(" + assert_includes ui_test, "for _ in 0..<2" + assert_includes ui_test, "if imported.waitForExistence(timeout: 15)" + assert_includes( + ui_test, + 'XCTFail("Host Documents fixture selection to import the guest file")' + ) + refute_includes ui_test, "fixture.tap()" + refute_includes ui_test, "exported.tap()" assert_includes ui_test, "for _ in 0..<2" assert_includes ui_test, "hostDestination.waitForExistence(timeout: 10)" assert_includes ui_test, "openedHostDestination" diff --git a/Tests/Scripts/RuntimeSmokeScriptTests.sh b/Tests/Scripts/RuntimeSmokeScriptTests.sh index 3f07a07..d7a2589 100755 --- a/Tests/Scripts/RuntimeSmokeScriptTests.sh +++ b/Tests/Scripts/RuntimeSmokeScriptTests.sh @@ -187,7 +187,7 @@ if [[ "${POCKETROOT_MOCK_XCODEBUILD_MODE:-retry}" == "retry" && "$calls" -eq 1 ] exit 65 fi if [[ "${POCKETROOT_MOCK_XCODEBUILD_MODE:-retry}" == "missing-destination" && "$calls" -eq 1 ]]; then - echo "Unable to find a device matching the provided destination specifier:" + echo "Unable to find a destination matching the provided destination specifier:" echo "The requested device could not be found because no available devices matched the request." exit 70 fi From 1d023a2231990693d1be97d3252640a05921a0d0 Mon Sep 17 00:00:00 2001 From: Jintao Date: Tue, 4 Aug 2026 06:48:21 +0800 Subject: [PATCH 7/7] Stabilize PTY size probe --- CHANGELOG.en.md | 7 ++++++- CHANGELOG.md | 3 +++ .../experimental-v0.2.0/COMPOSITION.json | 2 +- .../experimental-v0.2.0/SBOM.spdx.json | 2 +- .../Release/experimental-v0.2.0/SHA256SUMS | 4 ++-- .../UITests/PocketRootHostAppUITests.swift | 20 ++++++++++++++++--- Tests/Scripts/ReleaseComplianceTests.rb | 13 ++++++++++++ 7 files changed, 43 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.en.md b/CHANGELOG.en.md index eb589a3..5298dde 100644 --- a/CHANGELOG.en.md +++ b/CHANGELOG.en.md @@ -92,12 +92,17 @@ All notable PocketRoot changes are recorded here. Semantic Versioning begins wit visible. Fixture selection uses a verified current frame and retries once if iOS 18.0 leaves the picker open after reporting a synthesized tap. The helper returns an explicit success state so a failed navigation or selection ends - the test case without cascading taps and secondary failures. The shared + the test case without cascading taps and secondary failures. The PTY size + probe likewise refocuses and resends only once when the first synthesized + command's unique marker remains absent for 15 seconds; the final 30-second + assertion still exposes a real terminal-input or resize synchronization + failure. The shared UI runner restarts and retries exactly once only when its own temporary Simulator test runner was not registered with FrontBoard; caller-supplied shared Simulators are not restarted, and assertion, timeout, and other build failures still fail immediately. If retry or restart fails, both attempt logs and available + `.xcresult` bundles are retained. - The native Xcode 16 lane validates the real RootFS install before downloading the multi-gigabyte Simulator runtime, preserving the disk headroom enforced by the installer while UI lanes retain the shared one-step setup. diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c3ceb0..61f64d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,10 +67,13 @@ PocketRoot 的重要变化记录在这里,并从首个公开版本开始遵循 而已经位于 Host 目录时会等到 fixture 文件真正可见才返回。fixture 选择使用当前 已验证 frame;若 iOS 18.0 在报告合成 tap 后仍保留 picker,则有界重试一次。辅助函数 返回明确的成功状态,导航或选择失败后立即停止当前用例,避免后续点击产生连锁误报。 + PTY 尺寸探针同样只在首条合成输入的唯一标记 15 秒内未出现时重新聚焦并补发一次, + 最终 30 秒断言仍会暴露真实的终端输入或尺寸同步故障。 通用 UI runner 只对 test runner 未注册到 FrontBoard 的模拟器基础设施错误重启自己创建的临时设备 并重试一次,调用方指定的共享 Simulator 不会被重启;测试 断言、超时和其他构建错误仍立即失败,重试或重启失败时同时保留首次与最终日志及 + 可用的两次 `.xcresult`。 - Xcode 16 native lane 会先验证真实 RootFS 安装,再下载数 GB 的 Simulator runtime, 为安装器的磁盘余量门禁保留空间;UI lanes 继续使用统一的一步 setup。 diff --git a/Compliance/Release/experimental-v0.2.0/COMPOSITION.json b/Compliance/Release/experimental-v0.2.0/COMPOSITION.json index b0bf2a3..1f6c41c 100644 --- a/Compliance/Release/experimental-v0.2.0/COMPOSITION.json +++ b/Compliance/Release/experimental-v0.2.0/COMPOSITION.json @@ -120,7 +120,7 @@ "Examples/PocketRootDemo/Sources/PocketRootDemo/Shared/UI/PlaceholderViewController.swift": "48e0f6462228a66face2aef51f9bd39ec02923c4d1c01b6b34f920948998f2b8", "Examples/PocketRootHostApp/Sources/HostApp.swift": "0c36bd3104ddc5bebb9e23675a44a92e3ce4aec2c1461b3e182d86d1a75159c9", "Examples/PocketRootHostApp/Sources/Info.plist": "711b38923f11fdb4ceb7d28cb90a51674afec0a64786a20f9bd68e3f406f8d26", - "Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift": "a7de763ee3e30510051645cbfac7b73167bcbca4b0cb1332004dcfeda8d487d2", + "Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift": "cfa59f0489f94b14b242ffa1aa9f75e0eeda99c74ca487318e0ea096bbef8d61", "Examples/PocketRootQuickStartApp/Sources/Info.plist": "b3c0c96e89db99c72c9f020590b1c5cdc0c927fd3af4b6ae7865abfdc81049b8", "Examples/PocketRootQuickStartApp/Sources/QuickStartApp.swift": "1c77c9229281d0d1f9fbe3678bcc9995fff8c7d2189f4c6152daaa2999683fb6", "Examples/PocketRootQuickStartApp/UITests/PocketRootQuickStartAppUITests.swift": "828471c84386dc8ddba23ecadc3618ccd559d92084ab762f4ef0832c3e3d5f59", diff --git a/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json b/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json index 5fa9edf..e95a18f 100644 --- a/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json +++ b/Compliance/Release/experimental-v0.2.0/SBOM.spdx.json @@ -3,7 +3,7 @@ "dataLicense": "CC0-1.0", "SPDXID": "SPDXRef-DOCUMENT", "name": "PocketRoot experimental full-graph composition 0.2.0", - "documentNamespace": "https://github.com/jacklv-coder/PocketRoot/sbom/release/experimental-v0.2.0/226b1537aab6f0e10e274801df7448fd5535519feaecb4e4ead72afcd8f9c17b", + "documentNamespace": "https://github.com/jacklv-coder/PocketRoot/sbom/release/experimental-v0.2.0/44f6d771dd3c6d83d7f5ee81ad4ffe83a50ef3efdafa24a26b1e8a70fc5f53d3", "creationInfo": { "created": "2026-08-03T00:00:00Z", "creators": [ diff --git a/Compliance/Release/experimental-v0.2.0/SHA256SUMS b/Compliance/Release/experimental-v0.2.0/SHA256SUMS index 71888e9..1ec07e0 100644 --- a/Compliance/Release/experimental-v0.2.0/SHA256SUMS +++ b/Compliance/Release/experimental-v0.2.0/SHA256SUMS @@ -1,5 +1,5 @@ -226b1537aab6f0e10e274801df7448fd5535519feaecb4e4ead72afcd8f9c17b COMPOSITION.json +44f6d771dd3c6d83d7f5ee81ad4ffe83a50ef3efdafa24a26b1e8a70fc5f53d3 COMPOSITION.json 9fe270ba11160051d13f99cfe4ce6ee03c52882b88339dd6e4c1b4923ae30755 READINESS.json 6d41126608b019b46332a7957889f41e3c3ec6c6e9e2b884dca5ffc6d405e943 README.md fab07621b5188930bfd2a38117e206815d94f755e667d65fc182ee55d14d2d40 RELEASE-CHECKLIST.md -dd4f166c4e6a18bb7c76ff99f76ccee376354abe2efa17e35396333e434afeb1 SBOM.spdx.json +c56b8eec15f2112bcd38b8f1d8f0c56da52967538284e12cd749422565076b8f SBOM.spdx.json diff --git a/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift b/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift index f01b052..839ab62 100644 --- a/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift +++ b/Examples/PocketRootHostApp/UITests/PocketRootHostAppUITests.swift @@ -1369,8 +1369,7 @@ final class PocketRootHostAppUITests: XCTestCase { _ terminal: XCUIElement, marker: String ) -> (rows: Int, columns: Int) { - terminal.typeText("printf '\(marker)'; stty size\n") - + let command = "printf '\(marker)'; stty size\n" var result: (rows: Int, columns: Int)? let expression = try! NSRegularExpression( pattern: NSRegularExpression.escapedPattern(for: marker) @@ -1397,7 +1396,22 @@ final class PocketRootHostAppUITests: XCTestCase { result = (rows, columns) return true } - wait(for: predicate, evaluatedWith: terminal, timeout: 30) + terminal.typeText(command) + if !waitWithoutAssertion( + for: predicate, + evaluatedWith: terminal, + timeout: 15 + ) { + terminal.tap() + terminal.typeText(command) + } + guard wait( + for: predicate, + evaluatedWith: terminal, + timeout: 30 + ) else { + return (0, 0) + } return result ?? (0, 0) } diff --git a/Tests/Scripts/ReleaseComplianceTests.rb b/Tests/Scripts/ReleaseComplianceTests.rb index 33c483a..b2a6f59 100644 --- a/Tests/Scripts/ReleaseComplianceTests.rb +++ b/Tests/Scripts/ReleaseComplianceTests.rb @@ -1249,6 +1249,19 @@ def test_standalone_host_has_real_pty_and_files_ui_smoke ui_test.index("if sidebarLocalLocation.exists {") ) assert_includes ui_test, "testPTYLifecycleAndShutdown" + query_terminal_size = ui_test[ + /private func queryTerminalSize\(.*?\n \}\n\n private enum WidthChangeDirection/m + ] + refute_nil query_terminal_size + assert_includes( + query_terminal_size, + %q{let command = "printf '\(marker)'; stty size\n"} + ) + assert_includes query_terminal_size, "if !waitWithoutAssertion(" + assert_includes query_terminal_size, "timeout: 15" + assert_includes query_terminal_size, "terminal.tap()" + assert_equal 2, query_terminal_size.scan("terminal.typeText(command)").length + assert_includes query_terminal_size, "guard wait(" assert_includes ui_test, "dismissKeyboardOnboardingIfPresent(in: app)" assert_includes ui_test, "let interruptFrames = waitForInteractionFrames(" assert_includes ui_test, "interruptFrames.elementFrame"