diff --git a/.github/workflows/ci-guards.yml b/.github/workflows/ci-guards.yml index bf992f20def2..1ddbe9fa6f98 100644 --- a/.github/workflows/ci-guards.yml +++ b/.github/workflows/ci-guards.yml @@ -782,6 +782,7 @@ jobs: python3 tests/test_e2e_warm_derived_data.py python3 tests/test_seed_derived_data.py python3 tests/test_ci_product_publication.py + python3 tests/test_ci_cli_product_routing.py - name: Validate Python R2 appcast upload guard if: ${{ matrix.group == 'release-tooling' }} diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml index 1acd338ab30c..7d10433999fe 100644 --- a/.github/workflows/ci-macos.yml +++ b/.github/workflows/ci-macos.yml @@ -6,6 +6,10 @@ on: macos: required: true type: string + cli: + required: false + default: "false" + type: string full_suite: required: true type: string @@ -108,7 +112,7 @@ jobs: # build products and run test-without-building, so a compiler failure # cannot fan out across every macOS worker and a successful build is not # repeated six times. - if: ${{ inputs.macos == 'true' && inputs.compile_admitted != 'true' }} + if: ${{ (inputs.macos == 'true' || inputs.cli == 'true') && (inputs.compile_admitted != 'true' || inputs.full_suite == 'true' || inputs.cli == 'true') }} # Pull requests and main's full-suite dispatch (ci-main-full-suite.yml) # compile on the pool and Xcode seed-derived-data.yml builds with, so both # can adopt its DerivedData seed below. Merge groups and dispatches on @@ -739,7 +743,7 @@ jobs: CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_cli_contract_help.py CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_cli_config_doctor.py - # Compile-only forks have no artifact consumers and cannot seed reuse: + # Compile-only forks without targeted CLI tests have no artifact consumers and cannot seed reuse: # both the reuse selector and broker require an in-repository producer. # Keep unknown metadata, every full suite, and in-org reuse producers. - name: Start product publication timer @@ -749,6 +753,7 @@ jobs: id: publish-products env: PRODUCT_FULL_SUITE: ${{ inputs.full_suite }} + PRODUCT_CLI: ${{ inputs.cli }} PRODUCT_UNIT_SUITE: ${{ inputs.unit_suite }} PRODUCT_EVENT: ${{ github.event_name }} PRODUCT_HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }} @@ -763,6 +768,7 @@ jobs: # unused and must still be packaged and uploaded. unused_fork_product = ( os.environ.get("PRODUCT_FULL_SUITE") == "false" + and os.environ.get("PRODUCT_CLI") == "false" and os.environ.get("PRODUCT_UNIT_SUITE") != "true" and os.environ.get("PRODUCT_EVENT") == "pull_request" and bool(head) and bool(repository) @@ -2207,6 +2213,245 @@ jobs: ;; esac + cli-product-tests: + permissions: + contents: read + actions: read + # The R2 artifact transport mints an OIDC token for its broker. + id-token: write + needs: + - macos-compile-admission + # Targeted CLI routing consumes the shared compiled product even when prior + # compile admission exists; it does not enable the app-host/full-suite lanes. + # Same !cancelled() reasoning as app-host-unit-tests: the implicit success() + # gate would count a legitimately skipped linux job in the transitive needs + # chain as a reason to skip every macOS job. + if: ${{ !cancelled() && needs.macos-compile-admission.result == 'success' && (inputs.cli == 'true' || (inputs.macos == 'true' && inputs.full_suite == 'true')) }} + name: CLI product tests + # cmuxCLITests has no app host, so it needs no testmanagerd control session + # and no GUI console session. It still needs the compiled product, so it + # reuses macos-compile-admission's artifact like the app-host shards do, + # on the pool and Xcode that built it: the test bundle only loads under + # the Xcode that linked it. + runs-on: ${{ needs.macos-compile-admission.outputs.runner }} + timeout-minutes: 40 + env: + CMUX_NODE_PRODUCT_CACHE_ROOT: ${{ vars.CMUX_NODE_PRODUCT_CACHE_ROOT }} + CMUX_NODE_PRODUCT_CACHE_MAX_BYTES: ${{ vars.CMUX_NODE_PRODUCT_CACHE_MAX_BYTES }} + CMUX_NODE_PRODUCT_CACHE_WAIT_SECONDS: ${{ vars.CMUX_NODE_PRODUCT_CACHE_WAIT_SECONDS }} + CMUX_ARTIFACT_PEER_URLS: ${{ vars.CMUX_ARTIFACT_PEER_URLS }} + CMUX_ARTIFACT_PEER_TOKEN_FILE: ${{ vars.CMUX_ARTIFACT_PEER_TOKEN_FILE }} + CMUX_CI_XCODE_APP: ${{ needs.macos-compile-admission.outputs.xcode_app }} + CMUX_CI_REQUIRED_MACOS_SDK_MAJOR: "26" + CMUX_SKIP_ZIG_BUILD: "1" + SWIFT_BACKTRACE: "interactive=no,timeout=0s,symbolicate=off,color=no" + steps: + - name: Verify GitHub-hosted route + env: + RUNNER_ENVIRONMENT: ${{ runner.environment }} + REQUESTED_RUNNER: ${{ needs.macos-compile-admission.outputs.runner }} + RUNNER_CONTEXT_NAME: ${{ runner.name }} + run: | + set -euo pipefail + case "$REQUESTED_RUNNER" in + macos-*) + if [ "$RUNNER_ENVIRONMENT" != "github-hosted" ]; then + echo "::error::$REQUESTED_RUNNER resolved outside GitHub-hosted capacity: $RUNNER_CONTEXT_NAME" + exit 1 + fi + ;; + esac + + - name: Clear stale git locks (self-hosted reused workspace) + shell: bash + run: | + ws="${GITHUB_WORKSPACE:-$PWD}" + rm -f "$ws/.git/index.lock" 2>/dev/null || true + if [ -d "$ws/.git/modules" ]; then + find "$ws/.git/modules" -type f -name "*.lock" -delete 2>/dev/null || true + fi + + # No submodules: this lane never compiles Ghostty or the app, it only + # loads an already-built bundle and spawns the already-built CLI. + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Select Xcode + run: | + set -euo pipefail + ./scripts/select-ci-xcode.sh + + - name: Prepare isolated DerivedData + run: | + set -euo pipefail + DERIVED_DATA_PATH="$RUNNER_TEMP/cmux-derived-data-cli-tests-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" + rm -rf "$DERIVED_DATA_PATH" + mkdir -p "$DERIVED_DATA_PATH" + echo "CMUX_DERIVED_DATA_PATH=$DERIVED_DATA_PATH" >> "$GITHUB_ENV" + + - name: Try node-local compiled product cache + id: node-products + continue-on-error: true + env: + ARTIFACT_ID: ${{ needs.macos-compile-admission.outputs.artifact_id }} + ARTIFACT_PROVIDER_DIGEST: ${{ needs.macos-compile-admission.outputs.artifact_digest }} + EXPECTED_SHA256: ${{ needs.macos-compile-admission.outputs.sha256 }} + CMUX_PRODUCT_CONTRACT: ${{ needs.macos-compile-admission.outputs.product_contract }} + CMUX_PRODUCT_SOURCE_REVISION: ${{ needs.macos-compile-admission.outputs.source_revision }} + CMUX_PRODUCT_PRODUCER_RUN_ID: ${{ needs.macos-compile-admission.outputs.producer_run_id }} + CMUX_PRODUCT_PRODUCER_RUN_ATTEMPT: ${{ needs.macos-compile-admission.outputs.producer_run_attempt }} + CMUX_NODE_PRODUCT_CACHE_FALLBACK_SOURCE: ${{ vars.CMUX_ARTIFACT_PEER_URLS != '' && 'peer' || (vars.CI_ARTIFACT_R2_URL != '' && 'r2' || 'github') }} + run: python3 scripts/ci/node_product_cache.py acquire "$RUNNER_TEMP/app-host-products" + + - name: Try trusted fleet peer artifact source + id: peer-products + if: steps.node-products.outputs.hit != 'true' + continue-on-error: true + env: + ARTIFACT_ID: ${{ needs.macos-compile-admission.outputs.artifact_id }} + ARTIFACT_PROVIDER_DIGEST: ${{ needs.macos-compile-admission.outputs.artifact_digest }} + EXPECTED_SHA256: ${{ needs.macos-compile-admission.outputs.sha256 }} + CMUX_PRODUCT_CONTRACT: ${{ needs.macos-compile-admission.outputs.product_contract }} + CMUX_PRODUCT_SOURCE_REVISION: ${{ needs.macos-compile-admission.outputs.source_revision }} + CMUX_PRODUCT_PRODUCER_RUN_ID: ${{ needs.macos-compile-admission.outputs.producer_run_id }} + CMUX_PRODUCT_PRODUCER_RUN_ATTEMPT: ${{ needs.macos-compile-admission.outputs.producer_run_attempt }} + run: python3 scripts/ci/peer_product_source.py fetch "$RUNNER_TEMP/app-host-products" + + # R2 is an optional remote broker. With no configured endpoint, a peer + # miss falls straight through to the canonical GitHub artifact. + - name: Try shared R2 artifact transport + id: r2-products + if: steps.node-products.outputs.hit != 'true' && steps.peer-products.outputs.hit != 'true' && vars.CI_ARTIFACT_R2_URL != '' + continue-on-error: true + env: + GH_TOKEN: ${{ github.token }} + CI_ARTIFACT_R2_URL: ${{ vars.CI_ARTIFACT_R2_URL }} + ARTIFACT_ID: ${{ needs.macos-compile-admission.outputs.artifact_id }} + ARTIFACT_PROVIDER_DIGEST: ${{ needs.macos-compile-admission.outputs.artifact_digest }} + run: python3 scripts/ci/restore-r2-artifact.py + + # Same exact-ID GitHub artifact over parallel range requests; the + # single-stream download below stays as the fallback. + - name: Try parallel GitHub artifact transport + id: parallel-products + if: steps.node-products.outputs.hit != 'true' && steps.peer-products.outputs.hit != 'true' && steps.r2-products.outputs.hit != 'true' + continue-on-error: true + env: + GH_TOKEN: ${{ github.token }} + ARTIFACT_ID: ${{ needs.macos-compile-admission.outputs.artifact_id }} + ARTIFACT_PROVIDER_DIGEST: ${{ needs.macos-compile-admission.outputs.artifact_digest }} + run: python3 scripts/ci/parallel_artifact_download.py + + - name: Download compiled test product + if: steps.node-products.outputs.hit != 'true' && steps.peer-products.outputs.hit != 'true' && steps.r2-products.outputs.hit != 'true' && steps.parallel-products.outputs.hit != 'true' + uses: ./.github/actions/download-test-product + with: + artifact-id: ${{ needs.macos-compile-admission.outputs.artifact_id }} + path: ${{ runner.temp }}/app-host-products + + - name: Restore compiled test product + id: restore-products + env: + ARTIFACT_ID: ${{ needs.macos-compile-admission.outputs.artifact_id }} + ARTIFACT_PROVIDER_DIGEST: ${{ needs.macos-compile-admission.outputs.artifact_digest }} + EXPECTED_SHA256: ${{ needs.macos-compile-admission.outputs.sha256 }} + CMUX_PRODUCT_CONTRACT: ${{ needs.macos-compile-admission.outputs.product_contract }} + CMUX_PRODUCT_SOURCE_REVISION: ${{ needs.macos-compile-admission.outputs.source_revision }} + CMUX_PRODUCT_PRODUCER_RUN_ID: ${{ needs.macos-compile-admission.outputs.producer_run_id }} + CMUX_PRODUCT_PRODUCER_RUN_ATTEMPT: ${{ needs.macos-compile-admission.outputs.producer_run_attempt }} + CMUX_NODE_PRODUCT_CACHE_HIT: ${{ steps.node-products.outputs.hit }} + CMUX_NODE_PRODUCT_CACHE_LOOKUP_SECONDS: ${{ steps.node-products.outputs.lookup_seconds }} + CMUX_PEER_PRODUCT_HIT: ${{ steps.peer-products.outputs.hit }} + CMUX_PEER_PRODUCT_LOOKUP_SECONDS: ${{ steps.peer-products.outputs.lookup_seconds }} + CMUX_PEER_PRODUCT_TRANSFER_SECONDS: ${{ steps.peer-products.outputs.transfer_seconds }} + CMUX_PEER_PRODUCT_BYTES: ${{ steps.peer-products.outputs.bytes_transferred }} + CMUX_R2_PRODUCT_HIT: ${{ steps.r2-products.outputs.hit }} + CMUX_PARALLEL_PRODUCT_HIT: ${{ steps.parallel-products.outputs.hit }} + CMUX_PARALLEL_PRODUCT_TRANSFER_SECONDS: ${{ steps.parallel-products.outputs.transfer_seconds }} + CMUX_ARTIFACT_R2_RESULT: ${{ steps.r2-products.outputs.r2_result }} + run: scripts/ci/restore-app-host-test-product.sh + + - name: Finalize node-local compiled product cache + if: always() + continue-on-error: true + env: + GH_TOKEN: ${{ github.token }} + ARTIFACT_ID: ${{ needs.macos-compile-admission.outputs.artifact_id }} + ARTIFACT_PROVIDER_DIGEST: ${{ needs.macos-compile-admission.outputs.artifact_digest }} + EXPECTED_SHA256: ${{ needs.macos-compile-admission.outputs.sha256 }} + CMUX_PRODUCT_CONTRACT: ${{ needs.macos-compile-admission.outputs.product_contract }} + CMUX_PRODUCT_SOURCE_REVISION: ${{ needs.macos-compile-admission.outputs.source_revision }} + CMUX_PRODUCT_PRODUCER_RUN_ID: ${{ needs.macos-compile-admission.outputs.producer_run_id }} + CMUX_PRODUCT_PRODUCER_RUN_ATTEMPT: ${{ needs.macos-compile-admission.outputs.producer_run_attempt }} + CMUX_NODE_PRODUCT_CACHE_TOKEN: ${{ steps.node-products.outputs.token }} + CMUX_NODE_PRODUCT_CACHE_LEASE: ${{ steps.node-products.outputs.lease }} + CMUX_NODE_PRODUCT_SOURCE_CLASS: ${{ steps.peer-products.outputs.hit == 'true' && 'peer' || (steps.r2-products.outputs.hit == 'true' && 'r2' || 'github') }} + CMUX_PRODUCT_RESTORE_SUCCEEDED: ${{ steps.restore-products.outcome == 'success' }} + run: python3 scripts/ci/node_product_cache.py finalize "$RUNNER_TEMP/app-host-products/app-host-products.tar.gz" + + - name: Resolve the CLI under test + run: | + set -euo pipefail + products="$CMUX_DERIVED_DATA_PATH/Build/Products/Debug" + cli="$products/cmux" + if [ ! -x "$cli" ]; then + cli="$(find "$products" -path "*.app/Contents/Resources/bin/cmux" -perm -111 -print -quit)" + fi + if [ -z "${cli:-}" ] || [ ! -x "$cli" ]; then + echo "::error::no cmux CLI in the restored product" >&2 + exit 1 + fi + echo "CMUX_CLI_PATH=$cli" >> "$GITHUB_ENV" + echo "Testing $cli" + + - name: Isolate HOME + run: | + set -euo pipefail + cli_home="$RUNNER_TEMP/cmux-cli-tests-home" + rm -rf "$cli_home" + mkdir -p "$cli_home/.config" + echo "CMUX_CLI_TESTS_HOME=$cli_home" >> "$GITHUB_ENV" + + - name: Run CLI product tests + run: | + set -euo pipefail + test_log="$RUNNER_TEMP/cmux-cli-product-tests.log" + # xcodebuild forwards only TEST_RUNNER_-prefixed variables into the + # test process, so a bare HOME in the job environment reaches + # xcodebuild and stops there. CFFIXED_USER_HOME has to travel with + # it: Foundation resolves NSHomeDirectory() through getpwuid unless + # that is set, so HOME alone still leaves every fixture reading the + # runner's real home between reused self-hosted runs. CMUX_CLI_PATH + # needs the prefix too, or the tests fall back to searching the + # products directory and may run a different binary than the one + # resolved above. + env \ + "TEST_RUNNER_HOME=$CMUX_CLI_TESTS_HOME" \ + "TEST_RUNNER_CFFIXED_USER_HOME=$CMUX_CLI_TESTS_HOME" \ + "TEST_RUNNER_XDG_CONFIG_HOME=$CMUX_CLI_TESTS_HOME/.config" \ + "TEST_RUNNER_SSH_AUTH_SOCK=" \ + "TEST_RUNNER_CMUX_CLI_PATH=$CMUX_CLI_PATH" \ + bash scripts/ci/run-and-capture.sh "$test_log" \ + xcodebuild test-without-building \ + -xctestrun "$CMUX_CLI_TESTS_XCTESTRUN" \ + -destination "platform=macOS" \ + -test-timeouts-enabled YES \ + -default-test-execution-time-allowance 180 \ + -maximum-test-execution-time-allowance 300 \ + -only-testing:cmuxCLITests + scripts/ci/require_selected_test_execution.sh "$test_log" "cmuxCLITests" + + - name: Upload CLI product test log + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: cli-product-tests-log-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/cmux-cli-product-tests.log + if-no-files-found: ignore + retention-days: 7 + swift-package-tests: # Stays on the macos-15 pool on every event, including pull requests. The # "Select helper Xcode" step below pins CMUX_CI_REQUIRED_MACOS_SDK_MAJOR=15 @@ -3533,6 +3778,7 @@ jobs: needs: - macos-compile-admission - app-host-unit-tests + - cli-product-tests - swift-package-tests - tests-build-and-lag - release-admission @@ -3558,17 +3804,19 @@ jobs: # same reading the macos-compile-admission job condition uses. if inputs.get("compile_admitted") == "": inputs["compile_admitted"] = "false" + inputs.setdefault("cli", "false") # A caller that predates the swift_packages input leaves it unset or # empty, which reads as "the package lane was not separately routed". if inputs.get("swift_packages", "") == "": inputs["swift_packages"] = "false" - for route in ("macos", "full_suite", "compile_admitted", "release_build", "swift_packages"): + for route in ("macos", "cli", "full_suite", "compile_admitted", "release_build", "swift_packages"): if inputs.get(route) not in {"true", "false"}: print(f"invalid route {route}={inputs.get(route)!r}", file=sys.stderr) sys.exit(1) macos = inputs["macos"] == "true" full_suite = inputs["full_suite"] == "true" + cli = inputs["cli"] == "true" # The package lane is routed from changed paths, so it is required # whenever the router selected it even under the compile-only suite. swift_packages = inputs["swift_packages"] == "true" @@ -3581,8 +3829,9 @@ jobs: # separate worker, so admission's success is what proves the tests. in_admission = inputs.get("unit_in_admission", "") == "true" required = { - "macos-compile-admission": macos and (inputs["compile_admitted"] != "true" or in_admission), + "macos-compile-admission": (macos or cli) and (inputs["compile_admitted"] != "true" or full_suite or cli or in_admission), "app-host-unit-tests": macos and (full_suite or unit_suite) and not in_admission, + "cli-product-tests": cli or (macos and full_suite), "swift-package-tests": (macos and full_suite) or swift_packages, "tests-build-and-lag": macos and full_suite, "release-admission": macos and full_suite and inputs["release_build"] == "true", diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b5142797b29..0c3cfe7945e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1025,7 +1025,7 @@ jobs: - browser - guards - web - if: ${{ !cancelled() && github.event_name == 'pull_request' && needs.changes.result == 'success' && needs.static-preflight.result == 'success' && needs.changes.outputs.macos != 'false' && (needs.changes.outputs.full_suite == 'true' || needs.changes.outputs.compile_admitted != 'true') }} + if: ${{ !cancelled() && github.event_name == 'pull_request' && needs.changes.result == 'success' && needs.static-preflight.result == 'success' && (needs.changes.outputs.macos != 'false' || needs.changes.outputs.cli == 'true') && (needs.changes.outputs.full_suite == 'true' || needs.changes.outputs.cli == 'true' || needs.changes.outputs.compile_admitted != 'true') }} runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || vars.LINUX_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }} timeout-minutes: 5 permissions: {} @@ -1060,9 +1060,8 @@ jobs: # Packages/ still needs the package-test lane, which is not part of compile # admission and would otherwise be skipped whenever the app-host product is # already admitted or the macOS area is neutral (a Packages/iOS package - # outside the desktop closure). Every other job in the called workflow stays - # gated on inputs.macos, so nothing else wakes up with it. - if: ${{ !cancelled() && needs.changes.result == 'success' && needs.static-preflight.result == 'success' && (needs.macos-admission-gate.result == 'success' || needs.macos-admission-gate.result == 'skipped') && ((needs.changes.outputs.macos != 'false' && (needs.changes.outputs.full_suite == 'true' || needs.changes.outputs.unit_suite == 'true' || needs.changes.outputs.compile_admitted != 'true')) || needs.changes.outputs.swift_packages == 'true') }} + # outside the desktop closure). Other jobs retain their macos/cli gates. + if: ${{ !cancelled() && needs.changes.result == 'success' && needs.static-preflight.result == 'success' && (needs.macos-admission-gate.result == 'success' || needs.macos-admission-gate.result == 'skipped') && (((needs.changes.outputs.macos != 'false' || needs.changes.outputs.cli == 'true') && (needs.changes.outputs.full_suite == 'true' || needs.changes.outputs.unit_suite == 'true' || needs.changes.outputs.cli == 'true' || needs.changes.outputs.compile_admitted != 'true')) || needs.changes.outputs.swift_packages == 'true') }} permissions: actions: read id-token: write @@ -1072,6 +1071,7 @@ jobs: uses: ./.github/workflows/ci-macos.yml with: macos: ${{ needs.changes.outputs.macos }} + cli: ${{ needs.changes.outputs.cli }} full_suite: ${{ needs.changes.outputs.full_suite }} unit_suite: ${{ needs.changes.outputs.unit_suite }} unit_selectors: ${{ needs.changes.outputs.unit_selectors }} @@ -1141,11 +1141,12 @@ jobs: # product and full_suite stayed false. unit_suite = outputs.get("unit_suite") == "true" macos_work_required = swift_packages or ( - macos_route == "true" - and ( + ( unit_suite + or outputs.get("cli") == "true" or not (full_suite == "false" and compile_admitted == "true") ) + and (macos_route == "true" or outputs.get("cli") == "true") ) if macos_work_required: if macos_result != "success": diff --git a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/AgentNotifyCategory.swift b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/AgentNotifyCategory.swift new file mode 100644 index 000000000000..e070feca1bc5 --- /dev/null +++ b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Values/AgentNotifyCategory.swift @@ -0,0 +1,18 @@ +/// Category an agent hook attaches to a notification so the app can gate +/// delivery by user config. Mirrors the CLI's `ClaudeNotifyCategory`; serialized +/// into the `notify_target_async` payload's optional `c=;p=<0|1>` meta. +public enum AgentNotifyCategory: String, Sendable { + case turnComplete = "turn-complete" + case needsPermission = "needs-permission" + case idleReminder = "idle-reminder" + case other + + public var soundAlertType: NotificationSoundAlertType? { + switch self { + case .turnComplete: return .turnDone + case .needsPermission, .idleReminder: return .needsInput + case .other: return nil + } + } +} + diff --git a/Sources/AgentNotificationGate.swift b/Sources/AgentNotificationGate.swift index 80a2f55c4fc2..1ddaccf91112 100644 --- a/Sources/AgentNotificationGate.swift +++ b/Sources/AgentNotificationGate.swift @@ -1,24 +1,6 @@ import CmuxSettings import Foundation -/// Category an agent hook attaches to a notification so the app can gate -/// delivery by user config. Mirrors the CLI's `ClaudeNotifyCategory`; serialized -/// into the `notify_target_async` payload's optional `c=;p=<0|1>` meta. -enum AgentNotifyCategory: String { - case turnComplete = "turn-complete" - case needsPermission = "needs-permission" - case idleReminder = "idle-reminder" - case other - - var soundAlertType: NotificationSoundAlertType? { - switch self { - case .turnComplete: return .turnDone - case .needsPermission, .idleReminder: return .needsInput - case .other: return nil - } - } -} - /// User policy for the "Claude finished a turn" notification. enum AgentTurnCompleteMode: String { case whenIdle diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index 4e666102c872..85436ff8e101 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -128,6 +128,7 @@ A76110010000000000000003 /* AgentHookNotificationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A76110010000000000000002 /* AgentHookNotificationPolicy.swift */; }; A76110020000000000000001 /* AgentHookNotificationPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A76110020000000000000002 /* AgentHookNotificationPolicyTests.swift */; }; 751AA9D65BBB1979C7957BAE /* AgentHookTestNotificationPipeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A0C49412A8AEDDDFA20067B /* AgentHookTestNotificationPipeline.swift */; }; + C11757000000000000000040 /* AgentHookTestNotificationPipeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A0C49412A8AEDDDFA20067B /* AgentHookTestNotificationPipeline.swift */; }; A77A0010A1B2C3D4E5F60001 /* AgentJournalLazyStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A77A000FA1B2C3D4E5F60001 /* AgentJournalLazyStore.swift */; }; 110438021104380211043802 /* AgentJournalLifecycleCenter+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 110438011104380111043801 /* AgentJournalLifecycleCenter+Database.swift */; }; C3C8079F2C1B6648739BA1A6 /* AgentJournalLifecycleCenter+Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = F49FD681C026A6B0C83211C6 /* AgentJournalLifecycleCenter+Notifications.swift */; }; @@ -135,6 +136,7 @@ A77A0005A1B2C3D4E5F60001 /* AgentJournalLifecycleCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A77A0006A1B2C3D4E5F60001 /* AgentJournalLifecycleCenter.swift */; }; A77A000EA1B2C3D4E5F60001 /* AgentJournalLifecycleCenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A77A000DA1B2C3D4E5F60001 /* AgentJournalLifecycleCenterTests.swift */; }; A77A000CA1B2C3D4E5F60001 /* AgentJournalTestSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A77A000BA1B2C3D4E5F60001 /* AgentJournalTestSupport.swift */; }; + C11757000000000000000041 /* AgentJournalTestSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A77A000BA1B2C3D4E5F60001 /* AgentJournalTestSupport.swift */; }; E506942F7E8A3C80F94EA4EB /* AgentNotificationAdmissionWaiters.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0F23A5D074809C236E5C607 /* AgentNotificationAdmissionWaiters.swift */; }; 0A1107110000000000000010 /* AgentNotificationDelivery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A1107110000000000000011 /* AgentNotificationDelivery.swift */; }; A5D41234A1B2C3D4E5F60718 /* AgentNotificationGate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D41235A1B2C3D4E5F60718 /* AgentNotificationGate.swift */; }; @@ -554,10 +556,11 @@ C0DE62600000000000000001 /* BrowserWindowPortalRegistryNotificationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE62600000000000000002 /* BrowserWindowPortalRegistryNotificationTests.swift */; }; 5C0C4A1E0000000000000001 /* BuildFlavor+ReleaseChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0C4A1E0000000000000002 /* BuildFlavor+ReleaseChannel.swift */; }; C0DE35530000000000000101 /* BundledCLILinkageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE35530000000000000102 /* BundledCLILinkageTests.swift */; }; + C11757000000000000000027 /* BundledCLITestSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1175700000000000000000E /* BundledCLITestSupport.swift */; }; F0ACC0E00000000000000001 /* CachedAgentProcessIdentityValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0ACC0E0000000000000002 /* CachedAgentProcessIdentityValidator.swift */; }; CAFE00000000000000000002 /* CaffeineController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFE00000000000000000001 /* CaffeineController.swift */; }; CAFE00000000000000000006 /* CaffeineControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFE00000000000000000005 /* CaffeineControllerTests.swift */; }; - 0A0F00560000000000000003 /* CampfireHookNotificationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A0F00560000000000000004 /* CampfireHookNotificationTests.swift */; }; + C11757000000000000000039 /* CampfireHookNotificationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C11757000000000000000020 /* CampfireHookNotificationTests.swift */; }; CAFE58130000000000000001 /* CampfireLaunchArgumentNormalizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFE58130000000000000002 /* CampfireLaunchArgumentNormalizer.swift */; }; 0A0F00570000000000000003 /* CampfireSupportProcessBindingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A0F00570000000000000004 /* CampfireSupportProcessBindingTests.swift */; }; 0A0F00550000000000000003 /* CampfireSupportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A0F00550000000000000004 /* CampfireSupportTests.swift */; }; @@ -582,9 +585,11 @@ A12123000000000000000009 /* ClaudeHookHibernationGenerationHistoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A12123000000000000000010 /* ClaudeHookHibernationGenerationHistoryTests.swift */; }; A12123000000000000000011 /* ClaudeHookHibernationSessionEndPreservationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A12123000000000000000012 /* ClaudeHookHibernationSessionEndPreservationTests.swift */; }; 7939000BAA11BB22CC33DD0B /* ClaudeHookLifecycleCleanupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7939000CAA11BB22CC33DD0C /* ClaudeHookLifecycleCleanupTests.swift */; }; + C11757000000000000000060 /* ClaudeHookLiveDeliverySocketState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B508AD047EB519F849B4F21 /* ClaudeHookLiveDeliverySocketState.swift */; }; F33BF21E04815ED46826DCB4 /* ClaudeHookLiveDeliverySocketState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B508AD047EB519F849B4F21 /* ClaudeHookLiveDeliverySocketState.swift */; }; 79390001AA11BB22CC33DD01 /* ClaudeHookLiveDeliveryTargetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79390002AA11BB22CC33DD02 /* ClaudeHookLiveDeliveryTargetTests.swift */; }; 79390009AA11BB22CC33DD09 /* ClaudeHookLiveDeliveryTargetTestSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7939000AAA11BB22CC33DD0A /* ClaudeHookLiveDeliveryTargetTestSupport.swift */; }; + C11757000000000000000049 /* ClaudeHookLiveDeliveryTargetTestSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7939000AAA11BB22CC33DD0A /* ClaudeHookLiveDeliveryTargetTestSupport.swift */; }; 79390011AA11BB22CC33DD11 /* ClaudeHookPIDAuthenticationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79390012AA11BB22CC33DD12 /* ClaudeHookPIDAuthenticationTests.swift */; }; D3AD00000000000000000002 /* ClaudeHookSessionStore+CodexTurnAdmission.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3AD00000000000000000001 /* ClaudeHookSessionStore+CodexTurnAdmission.swift */; }; 89930005AABBCCDDEEFF0001 /* ClaudeHookSessionStore+SupersededCleanup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89930005AABBCCDDEEFF0002 /* ClaudeHookSessionStore+SupersededCleanup.swift */; }; @@ -593,32 +598,36 @@ CC7FDF58CBB0EDDE5DADE167 /* ClaudeHookSurfaceSocketState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1136E25CD5300988F9F2162 /* ClaudeHookSurfaceSocketState.swift */; }; A5D41222A1B2C3D4E5F60718 /* ClaudeNotificationStatusLifecycleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D41223A1B2C3D4E5F60718 /* ClaudeNotificationStatusLifecycleTests.swift */; }; A9F200000000000000000015 /* ClaudeStreamJSONAccumulator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9F100000000000000000015 /* ClaudeStreamJSONAccumulator.swift */; }; - CE7000000000000000000001 /* ClaudeWrapperResumeEnvironmentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7000000000000000000002 /* ClaudeWrapperResumeEnvironmentTests.swift */; }; + C1175700000000000000003A /* ClaudeWrapperResumeEnvironmentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C11757000000000000000021 /* ClaudeWrapperResumeEnvironmentTests.swift */; }; 9758A0000000000000000004 /* CLIAmpLifecycleIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9758A0000000000000000003 /* CLIAmpLifecycleIntegrationTests.swift */; }; A5D4120DA1B2C3D4E5F60718 /* CLIAuthAliasTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D4120EA1B2C3D4E5F60718 /* CLIAuthAliasTests.swift */; }; 805500000000000000000001 /* CLIBrowserEvalOutputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 805500000000000000000002 /* CLIBrowserEvalOutputTests.swift */; }; CA11E4D0FA017DE500000B101 /* CLICallerWorkspaceDefaultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA11E4D0FA017DE500000F102 /* CLICallerWorkspaceDefaultTests.swift */; }; 964F2AF080DB48D8A2A2C186 /* CLIChildEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C00A2330C34E7DA7630E4D /* CLIChildEnvironment.swift */; }; + C11757000000000000000042 /* CLIChildEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C00A2330C34E7DA7630E4D /* CLIChildEnvironment.swift */; }; 2E931B5728B0478CB9B4937F /* CLIChildEnvironmentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC61257DCDCA431A908C55CC /* CLIChildEnvironmentTests.swift */; }; - C85350000000000000000101 /* CLIClaudeHookTimeoutRegressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C85350000000000000000102 /* CLIClaudeHookTimeoutRegressionTests.swift */; }; - C0DE60C0000000000000A036 /* CLICoderouterBootstrapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE60C0000000000000A035 /* CLICoderouterBootstrapTests.swift */; }; + C11757000000000000000028 /* CLIClaudeHookTimeoutRegressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1175700000000000000000F /* CLIClaudeHookTimeoutRegressionTests.swift */; }; + C11757000000000000000029 /* CLICoderouterBootstrapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C11757000000000000000010 /* CLICoderouterBootstrapTests.swift */; }; C0DE60C0000000000000A032 /* CLICoderouterCommandTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE60C0000000000000A031 /* CLICoderouterCommandTests.swift */; }; - C0D3F1F30000000000000103 /* CLICodexHookPathQuotingRegressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D3F1F30000000000000104 /* CLICodexHookPathQuotingRegressionTests.swift */; }; - C0D3F1F00000000000000103 /* CLICodexHookTimeoutRegressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D3F1F00000000000000104 /* CLICodexHookTimeoutRegressionTests.swift */; }; + C1175700000000000000002A /* CLICodexHookPathQuotingRegressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C11757000000000000000011 /* CLICodexHookPathQuotingRegressionTests.swift */; }; + C1175700000000000000002B /* CLICodexHookTimeoutRegressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C11757000000000000000012 /* CLICodexHookTimeoutRegressionTests.swift */; }; C0D3F1F10000000000000103 /* CLICodexHookTimeoutRegressionTestSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D3F1F10000000000000104 /* CLICodexHookTimeoutRegressionTestSupport.swift */; }; - C85350000000000000000105 /* CLICodexQueuedHookContractTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C85350000000000000000106 /* CLICodexQueuedHookContractTests.swift */; }; - 918100000000000000000002 /* CLICodexResumeNotificationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918100000000000000000001 /* CLICodexResumeNotificationTests.swift */; }; + C11757000000000000000043 /* CLICodexHookTimeoutRegressionTestSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D3F1F10000000000000104 /* CLICodexHookTimeoutRegressionTestSupport.swift */; }; + C1175700000000000000002C /* CLICodexQueuedHookContractTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C11757000000000000000013 /* CLICodexQueuedHookContractTests.swift */; }; + C1175700000000000000002D /* CLICodexResumeNotificationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C11757000000000000000014 /* CLICodexResumeNotificationTests.swift */; }; C6711A010000000000000001 /* CLICodexWeakEnvironmentRestoreBindingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6711B010000000000000001 /* CLICodexWeakEnvironmentRestoreBindingTests.swift */; }; C0D3F1F20000000000000103 /* CLICodexYoloResumePersistenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D3F1F20000000000000104 /* CLICodexYoloResumePersistenceTests.swift */; }; C11218000000000000000052 /* CLIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C11218000000000000000051 /* CLIError.swift */; }; C11218000000000000000053 /* CLIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C11218000000000000000051 /* CLIError.swift */; }; C51600000000000000000001 /* CLIExecInheritedSignalStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C51600000000000000000002 /* CLIExecInheritedSignalStateTests.swift */; }; - E295EA3753206FE3FFCA6C0A /* CLIExplicitSurfaceRoutingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1590EE4F01FEF02D40A48698 /* CLIExplicitSurfaceRoutingTests.swift */; }; + C1175700000000000000002E /* CLIExplicitSurfaceRoutingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C11757000000000000000015 /* CLIExplicitSurfaceRoutingTests.swift */; }; C46790000000000000000001 /* CLIForwardingLaunchArgumentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46790000000000000000002 /* CLIForwardingLaunchArgumentTests.swift */; }; C46790000000000000000003 /* CLIForwardingLaunchRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46790000000000000000004 /* CLIForwardingLaunchRouter.swift */; }; A5D41207A1B2C3D4E5F60718 /* CLIGenericHookPersistenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D41208A1B2C3D4E5F60718 /* CLIGenericHookPersistenceTests.swift */; }; A76110030000000000000001 /* CLIGrokNotificationNoiseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A76110030000000000000002 /* CLIGrokNotificationNoiseTests.swift */; }; - A5D41211A1B2C3D4E5F60718 /* CLIHookNoResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D41212A1B2C3D4E5F60718 /* CLIHookNoResponseTests.swift */; }; + C1175700000000000000002F /* CLIHookNoResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C11757000000000000000016 /* CLIHookNoResponseTests.swift */; }; + C11757000000000000000044 /* CLIHookProcessRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = C11757000000000000000051 /* CLIHookProcessRunner.swift */; }; + C11757000000000000000052 /* CLIHookProcessRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = C11757000000000000000051 /* CLIHookProcessRunner.swift */; }; 74346B19AC8D55243926D168 /* CLILegacyCodexRestoreLeaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ABECB16C9B3F79933C712B5 /* CLILegacyCodexRestoreLeaseTests.swift */; }; A5D4120BA1B2C3D4E5F60718 /* CLILegacyHookAliasTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D4120CA1B2C3D4E5F60718 /* CLILegacyHookAliasTests.swift */; }; C10657000000000000000003 /* CLILocalTmuxIdentityRegressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10657000000000000000004 /* CLILocalTmuxIdentityRegressionTests.swift */; }; @@ -633,8 +642,8 @@ 86D573598C8C1225D24AED5A /* CLINotifyProcessSocketState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E40480ECEC94949BB74D852 /* CLINotifyProcessSocketState.swift */; }; A5D41205A1B2C3D4E5F60718 /* CLINotifyProcessTestSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D41206A1B2C3D4E5F60718 /* CLINotifyProcessTestSupport.swift */; }; 89930001AABBCCDDEEFF0001 /* CLIOmpHookBindingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89930001AABBCCDDEEFF0002 /* CLIOmpHookBindingTests.swift */; }; - 89930006AABBCCDDEEFF0001 /* CLIOmpSupersededCleanupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89930006AABBCCDDEEFF0002 /* CLIOmpSupersededCleanupTests.swift */; }; - C85350000000000000000107 /* CLIRelayQueuedHookRegressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C85350000000000000000108 /* CLIRelayQueuedHookRegressionTests.swift */; }; + C11757000000000000000030 /* CLIOmpSupersededCleanupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C11757000000000000000017 /* CLIOmpSupersededCleanupTests.swift */; }; + C11757000000000000000031 /* CLIRelayQueuedHookRegressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C11757000000000000000018 /* CLIRelayQueuedHookRegressionTests.swift */; }; C0F16B000000000000000001 /* CLIRemoteShellStartupPerformanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F16B000000000000000002 /* CLIRemoteShellStartupPerformanceTests.swift */; }; A5D41209A1B2C3D4E5F60718 /* CLIRovoDevHookPersistenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D4120AA1B2C3D4E5F60718 /* CLIRovoDevHookPersistenceTests.swift */; }; FE89827F4FAB9F7524A38B41 /* CLISendQueuedOutputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85A183D9426E08846D029219 /* CLISendQueuedOutputTests.swift */; }; @@ -647,23 +656,28 @@ C73660020000000000000001 /* CLISSHPTYAttachSessionLostRespawnTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C73660020000000000000002 /* CLISSHPTYAttachSessionLostRespawnTests.swift */; }; 101CB3DBAD66CD6E3DBFB10E /* CLISSHPTYAttachTestHarness.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF53D5BA0442B7124B5071A6 /* CLISSHPTYAttachTestHarness.swift */; }; C77070000000000000000004 /* CLISSHPTYAttachTransientBridgeFailureExitCodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77070000000000000000005 /* CLISSHPTYAttachTransientBridgeFailureExitCodeTests.swift */; }; - 6379A0016379A0016379A001 /* CLISSHPTYResizeInputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6379A0026379A0026379A002 /* CLISSHPTYResizeInputTests.swift */; }; + C11757000000000000000032 /* CLISSHPTYResizeInputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C11757000000000000000019 /* CLISSHPTYResizeInputTests.swift */; }; F12713000000000000000101 /* CLISSHPTYStopPipe.swift in Sources */ = {isa = PBXBuildFile; fileRef = F12713000000000000000001 /* CLISSHPTYStopPipe.swift */; }; A7367001A1B2C3D4E5F60718 /* CLISSHSessionAttachAnchorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7367002A1B2C3D4E5F60718 /* CLISSHSessionAttachAnchorTests.swift */; }; - C12984000000000000000004 /* CLIStdioSIGPIPERegressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C12984000000000000000003 /* CLIStdioSIGPIPERegressionTests.swift */; }; + C11757000000000000000033 /* CLIStdioSIGPIPERegressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1175700000000000000001A /* CLIStdioSIGPIPERegressionTests.swift */; }; + C11757000000000000000045 /* CLITestBundleAnchor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C11757000000000000000053 /* CLITestBundleAnchor.swift */; }; + C11757000000000000000054 /* CLITestBundleAnchor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C11757000000000000000053 /* CLITestBundleAnchor.swift */; }; B05553B10000000000000001 /* CLITmuxCompatRemoteSplitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B05553B10000000000000002 /* CLITmuxCompatRemoteSplitTests.swift */; }; 7837E0057837E0057837E005 /* CLITmuxCompatResizePaneTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7837E0067837E0067837E006 /* CLITmuxCompatResizePaneTests.swift */; }; - C0DE11260000000000000001 /* CLITmuxCompatStoreConcurrencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE11260000000000000002 /* CLITmuxCompatStoreConcurrencyTests.swift */; }; + C11757000000000000000034 /* CLITmuxCompatStoreConcurrencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1175700000000000000001B /* CLITmuxCompatStoreConcurrencyTests.swift */; }; 84ED8B0271A2FD801E8F280C /* CLIVMDevTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20A03BD3D5FCAFBEA0D8CA4C /* CLIVMDevTests.swift */; }; C78951D0E70B3BF25DFABD26 /* CLIVMLayoutEnvTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C52C82B48E0D40919A92FF1 /* CLIVMLayoutEnvTests.swift */; }; A5D4120DA1B2C3D4E5F60F01 /* CLIVMTransferTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D4120EA1B2C3D4E5F60F02 /* CLIVMTransferTests.swift */; }; 773600000000000000000001 /* CLIWindowCommandMockServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 773600000000000000000002 /* CLIWindowCommandMockServer.swift */; }; - 773600000000000000000003 /* CLIWindowHandleRoutingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 773600000000000000000004 /* CLIWindowHandleRoutingTests.swift */; }; + C11757000000000000000046 /* CLIWindowCommandMockServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 773600000000000000000002 /* CLIWindowCommandMockServer.swift */; }; + C11757000000000000000035 /* CLIWindowHandleRoutingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1175700000000000000001C /* CLIWindowHandleRoutingTests.swift */; }; 846600000000000000000001 /* CLIWorkspaceGroupSafetyMockServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846600000000000000000002 /* CLIWorkspaceGroupSafetyMockServer.swift */; }; - 846600000000000000000003 /* CLIWorkspaceGroupSafetyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846600000000000000000004 /* CLIWorkspaceGroupSafetyTests.swift */; }; + C11757000000000000000047 /* CLIWorkspaceGroupSafetyMockServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846600000000000000000002 /* CLIWorkspaceGroupSafetyMockServer.swift */; }; + C11757000000000000000036 /* CLIWorkspaceGroupSafetyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1175700000000000000001D /* CLIWorkspaceGroupSafetyTests.swift */; }; FBA17843EE21A7F33B4BD0AD /* CLIWorkspaceRefResolutionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99CD6A76F75825E105715C8C /* CLIWorkspaceRefResolutionTests.swift */; }; 799100000000000000000001 /* CLIWorkspaceStableIDMockServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799100000000000000000002 /* CLIWorkspaceStableIDMockServer.swift */; }; - 799100000000000000000003 /* CLIWorkspaceStableIDTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799100000000000000000004 /* CLIWorkspaceStableIDTests.swift */; }; + C11757000000000000000048 /* CLIWorkspaceStableIDMockServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 799100000000000000000002 /* CLIWorkspaceStableIDMockServer.swift */; }; + C11757000000000000000037 /* CLIWorkspaceStableIDTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1175700000000000000001E /* CLIWorkspaceStableIDTests.swift */; }; 575000010000000000000001 /* CLIWriteDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 575000020000000000000001 /* CLIWriteDescriptor.swift */; }; C51A73000000000000000101 /* ClosedItemHistory+PanelTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C51A73000000000000000102 /* ClosedItemHistory+PanelTitle.swift */; }; C10D51700000000000000002 /* ClosedItemHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10D51700000000000000001 /* ClosedItemHistory.swift */; }; @@ -1109,9 +1123,11 @@ AC4A6E7C4A700001AC4A0005 /* CmuxAgentChat in Frameworks */ = {isa = PBXBuildFile; productRef = AC4A6E7C4A700001AC4A0004 /* CmuxAgentChat */; }; C0DE43000000000000000001 /* CmuxAgentChatConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE43000000000000000002 /* CmuxAgentChatConfig.swift */; }; C0DE43000000000000000009 /* CmuxAgentChatConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE4300000000000000000A /* CmuxAgentChatConfigTests.swift */; }; + C1175700000000000000004E /* CmuxAgentJournal in Frameworks */ = {isa = PBXBuildFile; productRef = A77A0002A1B2C3D4E5F60001 /* CmuxAgentJournal */; }; A5B00003A1B2C3D4E5F60718 /* CMUXAgentLaunch in Frameworks */ = {isa = PBXBuildFile; productRef = A5B00002A1B2C3D4E5F60718 /* CMUXAgentLaunch */; }; A5B00004A1B2C3D4E5F60718 /* CMUXAgentLaunch in Frameworks */ = {isa = PBXBuildFile; productRef = A5B00002A1B2C3D4E5F60718 /* CMUXAgentLaunch */; }; A5B00005A1B2C3D4E5F60718 /* CMUXAgentLaunch in Frameworks */ = {isa = PBXBuildFile; productRef = A5B00002A1B2C3D4E5F60718 /* CMUXAgentLaunch */; }; + C1175700000000000000004D /* CMUXAgentLaunch in Frameworks */ = {isa = PBXBuildFile; productRef = A5B00002A1B2C3D4E5F60718 /* CMUXAgentLaunch */; }; 9758E0039758E0039758E003 /* CmuxAgentSessionStore in Frameworks */ = {isa = PBXBuildFile; productRef = 9758E0029758E0029758E002 /* CmuxAgentSessionStore */; }; 9758E0049758E0049758E004 /* CmuxAgentSessionStore in Frameworks */ = {isa = PBXBuildFile; productRef = 9758E0029758E0029758E002 /* CmuxAgentSessionStore */; }; 8295A0018295A0018295A001 /* CmuxAlertContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8295A0028295A0028295A002 /* CmuxAlertContent.swift */; }; @@ -1354,6 +1370,7 @@ D0F1A0000000000000000003 /* CmuxFilePreviewCore in Frameworks */ = {isa = PBXBuildFile; productRef = D0F1A0000000000000000002 /* CmuxFilePreviewCore */; }; D0F1A0000000000000000004 /* CmuxFilePreviewCore in Frameworks */ = {isa = PBXBuildFile; productRef = D0F1A0000000000000000002 /* CmuxFilePreviewCore */; }; 5E2701040000000000000004 /* CmuxFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 5E2701040000000000000002 /* CmuxFoundation */; }; + C1175700000000000000004F /* CmuxFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 5E2701040000000000000002 /* CmuxFoundation */; }; CF00F00000000000000000A3 /* CmuxFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = CF00F00000000000000000A2 /* CmuxFoundation */; }; CF00F00000000000000000A4 /* CmuxFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = CF00F00000000000000000A2 /* CmuxFoundation */; }; C617000000000000000000A3 /* CmuxGit in Frameworks */ = {isa = PBXBuildFile; productRef = C617000000000000000000A2 /* CmuxGit */; }; @@ -1397,7 +1414,7 @@ E3B7A30000000000000000D3 /* CmuxNotifications in Frameworks */ = {isa = PBXBuildFile; productRef = E3B7A30000000000000000D2 /* CmuxNotifications */; }; C85390050000000000000001 /* CMUXOpenCommandLocationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C85390050000000000000002 /* CMUXOpenCommandLocationTests.swift */; }; C0DE31390000000000000101 /* CMUXOpenCommandTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE31390000000000000102 /* CMUXOpenCommandTests.swift */; }; - C0DE31390000000000000111 /* CMUXOpenHTMLFocusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE31390000000000000112 /* CMUXOpenHTMLFocusTests.swift */; }; + C11757000000000000000038 /* CMUXOpenHTMLFocusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1175700000000000000001F /* CMUXOpenHTMLFocusTests.swift */; }; E3B7A30000000000000000F3 /* CmuxPanes in Frameworks */ = {isa = PBXBuildFile; productRef = E3B7A30000000000000000F2 /* CmuxPanes */; }; F0C0A1000000000000000003 /* CmuxPhonePush in Frameworks */ = {isa = PBXBuildFile; productRef = F0C0A1000000000000000002 /* CmuxPhonePush */; }; F0C0A1000000000000000004 /* CmuxPhonePush in Frameworks */ = {isa = PBXBuildFile; productRef = F0C0A1000000000000000002 /* CmuxPhonePush */; }; @@ -1414,6 +1431,7 @@ CE57E10000000000000000B4 /* CmuxSentryReporting in Frameworks */ = {isa = PBXBuildFile; productRef = CE57E10000000000000000B2 /* CmuxSentryReporting */; }; CE57E10000000000000000A3 /* CmuxSentryScrubbing in Frameworks */ = {isa = PBXBuildFile; productRef = CE57E10000000000000000A2 /* CmuxSentryScrubbing */; }; CE57E10000000000000000A4 /* CmuxSentryScrubbing in Frameworks */ = {isa = PBXBuildFile; productRef = CE57E10000000000000000A2 /* CmuxSentryScrubbing */; }; + C11757000000000000000050 /* CmuxSettings in Frameworks */ = {isa = PBXBuildFile; productRef = 5E27010400000000000000A3 /* CmuxSettings */; }; CD0CFE5300000000CD0CFE53 /* CmuxSettings in Frameworks */ = {isa = PBXBuildFile; productRef = CD0CFE5200000000CD0CFE52 /* CmuxSettings */; }; CD0CFE5700000000CD0CFE57 /* CmuxSettings in Frameworks */ = {isa = PBXBuildFile; productRef = CD0CFE5200000000CD0CFE52 /* CmuxSettings */; }; 0025785C0AAC45F3A9C5A8C5 /* CmuxSettingsFileStore+AppSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95EC0379B5EE4AA18652FF31 /* CmuxSettingsFileStore+AppSection.swift */; }; @@ -1559,6 +1577,7 @@ B6F143F6E02543F3899F1E02 /* CodexAutoresumeChainTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F143F6E02543F3899F1E01 /* CodexAutoresumeChainTests.swift */; }; C8711B000000000000000002 /* CodexCodeModeRolloutIdentityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8711B000000000000000001 /* CodexCodeModeRolloutIdentityTests.swift */; }; 9167FFA2759E9AD60F49616C /* CodexHookCapturedSocketCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1893BDBF86104694F22BC3C /* CodexHookCapturedSocketCommands.swift */; }; + C1175700000000000000004A /* CodexHookCapturedSocketCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1893BDBF86104694F22BC3C /* CodexHookCapturedSocketCommands.swift */; }; 918100000000000000000051 /* CodexHookFailureCandidate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918100000000000000000050 /* CodexHookFailureCandidate.swift */; }; 918100000000000000000041 /* CodexHookFailureSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918100000000000000000040 /* CodexHookFailureSummary.swift */; }; 918100000000000000000061 /* CodexHookUserInputCandidate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918100000000000000000060 /* CodexHookUserInputCandidate.swift */; }; @@ -1573,14 +1592,18 @@ C0DECAFE0000000000000001 /* CodexTeamsApprovalBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DECAFE0000000000000002 /* CodexTeamsApprovalBridge.swift */; }; C0DECAFE0000000000000003 /* CodexTeamsApprovalBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DECAFE0000000000000002 /* CodexTeamsApprovalBridge.swift */; }; C0DEBACC0000000000000001 /* CodexTeamsAppServerFixture.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DEBACC0000000000000004 /* CodexTeamsAppServerFixture.swift */; }; + C1175700000000000000004B /* CodexTeamsAppServerFixture.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DEBACC0000000000000004 /* CodexTeamsAppServerFixture.swift */; }; A10240010000000000000001 /* CodexTeamsAppServerProcess.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10240020000000000000001 /* CodexTeamsAppServerProcess.swift */; }; A10240030000000000000001 /* CodexTeamsAppServerProcess.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10240020000000000000001 /* CodexTeamsAppServerProcess.swift */; }; - A10240040000000000000001 /* CodexTeamsAppServerProcessTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10240050000000000000001 /* CodexTeamsAppServerProcessTests.swift */; }; + C11757000000000000000061 /* CodexTeamsAppServerProcess.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10240020000000000000001 /* CodexTeamsAppServerProcess.swift */; }; + C1175700000000000000003B /* CodexTeamsAppServerProcessTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C11757000000000000000022 /* CodexTeamsAppServerProcessTests.swift */; }; A10240080000000000000001 /* CodexTeamsPOSIXSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10240090000000000000001 /* CodexTeamsPOSIXSupport.swift */; }; A10240100000000000000001 /* CodexTeamsPOSIXSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10240090000000000000001 /* CodexTeamsPOSIXSupport.swift */; }; - C0DEBACC0000000000000003 /* CodexTeamsResumedBackfillTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DEBACC0000000000000006 /* CodexTeamsResumedBackfillTests.swift */; }; + C11757000000000000000062 /* CodexTeamsPOSIXSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10240090000000000000001 /* CodexTeamsPOSIXSupport.swift */; }; + C1175700000000000000003C /* CodexTeamsResumedBackfillTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C11757000000000000000023 /* CodexTeamsResumedBackfillTests.swift */; }; C0DEBACC0000000000000002 /* CodexTeamsSocketFixture.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DEBACC0000000000000005 /* CodexTeamsSocketFixture.swift */; }; - C0DE7100C0DE7100C0DE7100 /* CodexTerminalErrorNotificationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE7101C0DE7101C0DE7101 /* CodexTerminalErrorNotificationTests.swift */; }; + C1175700000000000000004C /* CodexTeamsSocketFixture.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DEBACC0000000000000005 /* CodexTeamsSocketFixture.swift */; }; + C1175700000000000000003D /* CodexTerminalErrorNotificationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C11757000000000000000024 /* CodexTerminalErrorNotificationTests.swift */; }; 918100000000000000000081 /* CodexTranscriptFailureReadResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918100000000000000000080 /* CodexTranscriptFailureReadResult.swift */; }; 918100000000000000000021 /* CodexTranscriptMonitorStopReplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918100000000000000000020 /* CodexTranscriptMonitorStopReplay.swift */; }; 918100000000000000000071 /* CodexTranscriptSubagentSignals.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918100000000000000000070 /* CodexTranscriptSubagentSignals.swift */; }; @@ -2169,7 +2192,7 @@ 468160000000000000000001 /* KeyboardShortcutSettingsObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 468160000000000000000002 /* KeyboardShortcutSettingsObserverTests.swift */; }; A17110000000000000000001 /* KeyboardShortcutSpaceKeyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A17110000000000000000002 /* KeyboardShortcutSpaceKeyTests.swift */; }; C34670040000000000000001 /* KeyboardShortcutTestSettingsIsolation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C34670040000000000000002 /* KeyboardShortcutTestSettingsIsolation.swift */; }; - 827300000000000000000004 /* KimiHookConfigLocationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 827300000000000000000003 /* KimiHookConfigLocationTests.swift */; }; + C1175700000000000000003E /* KimiHookConfigLocationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C11757000000000000000025 /* KimiHookConfigLocationTests.swift */; }; 858200000000000000000002 /* KimiRestorableAgentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 858200000000000000000001 /* KimiRestorableAgentTests.swift */; }; 8582A1000000000000000002 /* KimiResumeReviewRegressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8582A1000000000000000001 /* KimiResumeReviewRegressionTests.swift */; }; 87C609D63F03597AC12C8B0A /* LastSurfaceClosePreferenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2EE0C080FF0FCCE8A8CA930 /* LastSurfaceClosePreferenceTests.swift */; }; @@ -2595,6 +2618,7 @@ C0DEA771000000000000000B /* ProBadgeStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DEA771000000000000000C /* ProBadgeStyle.swift */; }; F0ACC0DE000000000000000D /* ProcessDetectedResumeIndexes.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0ACC0DE000000000000000E /* ProcessDetectedResumeIndexes.swift */; }; AC0FCEFB6665CF134634D60B /* ProcessExitWait.swift in Sources */ = {isa = PBXBuildFile; fileRef = E06E7588C05AC6A8659CFA51 /* ProcessExitWait.swift */; }; + C11757000000000000000063 /* ProcessExitWait.swift in Sources */ = {isa = PBXBuildFile; fileRef = E06E7588C05AC6A8659CFA51 /* ProcessExitWait.swift */; }; C47110010000000000000001 /* ProcessPipeReadCrashRegressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47110010000000000000002 /* ProcessPipeReadCrashRegressionTests.swift */; }; 0A327C88E531C7E407CEF58C /* ProcessSnapshotMeasurement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4587EDAC6B63EF4846FB24A6 /* ProcessSnapshotMeasurement.swift */; }; D75280010000000000000001 /* ProfilingSignposts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D75280010000000000000002 /* ProfilingSignposts.swift */; }; @@ -4161,6 +4185,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + C1175700000000000000000D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = A5001070 /* Project object */; + proxyType = 1; + remoteGlobalIDString = B9000005A1B2C3D4E5F60719; + remoteInfo = "cmux-cli"; + }; 738BF3D3196765B250928A93 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = A5001070 /* Project object */; @@ -4792,10 +4823,11 @@ C0DE62600000000000000002 /* BrowserWindowPortalRegistryNotificationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserWindowPortalRegistryNotificationTests.swift; sourceTree = ""; }; 5C0C4A1E0000000000000002 /* BuildFlavor+ReleaseChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App/BuildFlavor+ReleaseChannel.swift"; sourceTree = ""; }; C0DE35530000000000000102 /* BundledCLILinkageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundledCLILinkageTests.swift; sourceTree = ""; }; + C1175700000000000000000E /* BundledCLITestSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundledCLITestSupport.swift; sourceTree = ""; }; F0ACC0E0000000000000002 /* CachedAgentProcessIdentityValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedAgentProcessIdentityValidator.swift; sourceTree = ""; }; CAFE00000000000000000001 /* CaffeineController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaffeineController.swift; sourceTree = ""; }; CAFE00000000000000000005 /* CaffeineControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaffeineControllerTests.swift; sourceTree = ""; }; - 0A0F00560000000000000004 /* CampfireHookNotificationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CampfireHookNotificationTests.swift; sourceTree = ""; }; + C11757000000000000000020 /* CampfireHookNotificationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CampfireHookNotificationTests.swift; sourceTree = ""; }; CAFE58130000000000000002 /* CampfireLaunchArgumentNormalizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CampfireLaunchArgumentNormalizer.swift; sourceTree = ""; }; 0A0F00570000000000000004 /* CampfireSupportProcessBindingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CampfireSupportProcessBindingTests.swift; sourceTree = ""; }; 0A0F00550000000000000004 /* CampfireSupportTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CampfireSupportTests.swift; sourceTree = ""; }; @@ -4831,31 +4863,32 @@ D1136E25CD5300988F9F2162 /* ClaudeHookSurfaceSocketState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ClaudeHookSurfaceSocketState.swift"; sourceTree = ""; }; A5D41223A1B2C3D4E5F60718 /* ClaudeNotificationStatusLifecycleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClaudeNotificationStatusLifecycleTests.swift; sourceTree = ""; }; A9F100000000000000000015 /* ClaudeStreamJSONAccumulator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/ClaudeStreamJSONAccumulator.swift; sourceTree = ""; }; - CE7000000000000000000002 /* ClaudeWrapperResumeEnvironmentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClaudeWrapperResumeEnvironmentTests.swift; sourceTree = ""; }; + C11757000000000000000021 /* ClaudeWrapperResumeEnvironmentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClaudeWrapperResumeEnvironmentTests.swift; sourceTree = ""; }; 9758A0000000000000000003 /* CLIAmpLifecycleIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIAmpLifecycleIntegrationTests.swift; sourceTree = ""; }; A5D4120EA1B2C3D4E5F60718 /* CLIAuthAliasTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIAuthAliasTests.swift; sourceTree = ""; }; 805500000000000000000002 /* CLIBrowserEvalOutputTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIBrowserEvalOutputTests.swift; sourceTree = ""; }; CA11E4D0FA017DE500000F102 /* CLICallerWorkspaceDefaultTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLICallerWorkspaceDefaultTests.swift; sourceTree = ""; }; 95C00A2330C34E7DA7630E4D /* CLIChildEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIChildEnvironment.swift; sourceTree = ""; }; AC61257DCDCA431A908C55CC /* CLIChildEnvironmentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIChildEnvironmentTests.swift; sourceTree = ""; }; - C85350000000000000000102 /* CLIClaudeHookTimeoutRegressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIClaudeHookTimeoutRegressionTests.swift; sourceTree = ""; }; - C0DE60C0000000000000A035 /* CLICoderouterBootstrapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLICoderouterBootstrapTests.swift; sourceTree = ""; }; + C1175700000000000000000F /* CLIClaudeHookTimeoutRegressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIClaudeHookTimeoutRegressionTests.swift; sourceTree = ""; }; + C11757000000000000000010 /* CLICoderouterBootstrapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLICoderouterBootstrapTests.swift; sourceTree = ""; }; C0DE60C0000000000000A031 /* CLICoderouterCommandTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLICoderouterCommandTests.swift; sourceTree = ""; }; - C0D3F1F30000000000000104 /* CLICodexHookPathQuotingRegressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLICodexHookPathQuotingRegressionTests.swift; sourceTree = ""; }; - C0D3F1F00000000000000104 /* CLICodexHookTimeoutRegressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLICodexHookTimeoutRegressionTests.swift; sourceTree = ""; }; + C11757000000000000000011 /* CLICodexHookPathQuotingRegressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLICodexHookPathQuotingRegressionTests.swift; sourceTree = ""; }; + C11757000000000000000012 /* CLICodexHookTimeoutRegressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLICodexHookTimeoutRegressionTests.swift; sourceTree = ""; }; C0D3F1F10000000000000104 /* CLICodexHookTimeoutRegressionTestSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLICodexHookTimeoutRegressionTestSupport.swift; sourceTree = ""; }; - C85350000000000000000106 /* CLICodexQueuedHookContractTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLICodexQueuedHookContractTests.swift; sourceTree = ""; }; - 918100000000000000000001 /* CLICodexResumeNotificationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLICodexResumeNotificationTests.swift; sourceTree = ""; }; + C11757000000000000000013 /* CLICodexQueuedHookContractTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLICodexQueuedHookContractTests.swift; sourceTree = ""; }; + C11757000000000000000014 /* CLICodexResumeNotificationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLICodexResumeNotificationTests.swift; sourceTree = ""; }; C6711B010000000000000001 /* CLICodexWeakEnvironmentRestoreBindingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLICodexWeakEnvironmentRestoreBindingTests.swift; sourceTree = ""; }; C0D3F1F20000000000000104 /* CLICodexYoloResumePersistenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLICodexYoloResumePersistenceTests.swift; sourceTree = ""; }; C11218000000000000000051 /* CLIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIError.swift; sourceTree = ""; }; C51600000000000000000002 /* CLIExecInheritedSignalStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIExecInheritedSignalStateTests.swift; sourceTree = ""; }; - 1590EE4F01FEF02D40A48698 /* CLIExplicitSurfaceRoutingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIExplicitSurfaceRoutingTests.swift; sourceTree = ""; }; + C11757000000000000000015 /* CLIExplicitSurfaceRoutingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIExplicitSurfaceRoutingTests.swift; sourceTree = ""; }; C46790000000000000000002 /* CLIForwardingLaunchArgumentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIForwardingLaunchArgumentTests.swift; sourceTree = ""; }; C46790000000000000000004 /* CLIForwardingLaunchRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/CLIForwardingLaunchRouter.swift; sourceTree = ""; }; A5D41208A1B2C3D4E5F60718 /* CLIGenericHookPersistenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIGenericHookPersistenceTests.swift; sourceTree = ""; }; A76110030000000000000002 /* CLIGrokNotificationNoiseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIGrokNotificationNoiseTests.swift; sourceTree = ""; }; - A5D41212A1B2C3D4E5F60718 /* CLIHookNoResponseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIHookNoResponseTests.swift; sourceTree = ""; }; + C11757000000000000000016 /* CLIHookNoResponseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIHookNoResponseTests.swift; sourceTree = ""; }; + C11757000000000000000051 /* CLIHookProcessRunner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIHookProcessRunner.swift; sourceTree = ""; }; 1ABECB16C9B3F79933C712B5 /* CLILegacyCodexRestoreLeaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CLILegacyCodexRestoreLeaseTests.swift"; sourceTree = ""; }; A5D4120CA1B2C3D4E5F60718 /* CLILegacyHookAliasTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLILegacyHookAliasTests.swift; sourceTree = ""; }; C10657000000000000000004 /* CLILocalTmuxIdentityRegressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLILocalTmuxIdentityRegressionTests.swift; sourceTree = ""; }; @@ -4870,8 +4903,8 @@ 1E40480ECEC94949BB74D852 /* CLINotifyProcessSocketState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CLINotifyProcessSocketState.swift"; sourceTree = ""; }; A5D41206A1B2C3D4E5F60718 /* CLINotifyProcessTestSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLINotifyProcessTestSupport.swift; sourceTree = ""; }; 89930001AABBCCDDEEFF0002 /* CLIOmpHookBindingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIOmpHookBindingTests.swift; sourceTree = ""; }; - 89930006AABBCCDDEEFF0002 /* CLIOmpSupersededCleanupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIOmpSupersededCleanupTests.swift; sourceTree = ""; }; - C85350000000000000000108 /* CLIRelayQueuedHookRegressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIRelayQueuedHookRegressionTests.swift; sourceTree = ""; }; + C11757000000000000000017 /* CLIOmpSupersededCleanupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIOmpSupersededCleanupTests.swift; sourceTree = ""; }; + C11757000000000000000018 /* CLIRelayQueuedHookRegressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIRelayQueuedHookRegressionTests.swift; sourceTree = ""; }; C0F16B000000000000000002 /* CLIRemoteShellStartupPerformanceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIRemoteShellStartupPerformanceTests.swift; sourceTree = ""; }; A5D4120AA1B2C3D4E5F60718 /* CLIRovoDevHookPersistenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIRovoDevHookPersistenceTests.swift; sourceTree = ""; }; 85A183D9426E08846D029219 /* CLISendQueuedOutputTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLISendQueuedOutputTests.swift; sourceTree = ""; }; @@ -4883,23 +4916,24 @@ C73660020000000000000002 /* CLISSHPTYAttachSessionLostRespawnTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLISSHPTYAttachSessionLostRespawnTests.swift; sourceTree = ""; }; DF53D5BA0442B7124B5071A6 /* CLISSHPTYAttachTestHarness.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLISSHPTYAttachTestHarness.swift; sourceTree = ""; }; C77070000000000000000005 /* CLISSHPTYAttachTransientBridgeFailureExitCodeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLISSHPTYAttachTransientBridgeFailureExitCodeTests.swift; sourceTree = ""; }; - 6379A0026379A0026379A002 /* CLISSHPTYResizeInputTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLISSHPTYResizeInputTests.swift; sourceTree = ""; }; + C11757000000000000000019 /* CLISSHPTYResizeInputTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLISSHPTYResizeInputTests.swift; sourceTree = ""; }; F12713000000000000000001 /* CLISSHPTYStopPipe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLISSHPTYStopPipe.swift; sourceTree = ""; }; A7367002A1B2C3D4E5F60718 /* CLISSHSessionAttachAnchorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLISSHSessionAttachAnchorTests.swift; sourceTree = ""; }; - C12984000000000000000003 /* CLIStdioSIGPIPERegressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIStdioSIGPIPERegressionTests.swift; sourceTree = ""; }; + C1175700000000000000001A /* CLIStdioSIGPIPERegressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIStdioSIGPIPERegressionTests.swift; sourceTree = ""; }; + C11757000000000000000053 /* CLITestBundleAnchor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLITestBundleAnchor.swift; sourceTree = ""; }; B05553B10000000000000002 /* CLITmuxCompatRemoteSplitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLITmuxCompatRemoteSplitTests.swift; sourceTree = ""; }; 7837E0067837E0067837E006 /* CLITmuxCompatResizePaneTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLITmuxCompatResizePaneTests.swift; sourceTree = ""; }; - C0DE11260000000000000002 /* CLITmuxCompatStoreConcurrencyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLITmuxCompatStoreConcurrencyTests.swift; sourceTree = ""; }; + C1175700000000000000001B /* CLITmuxCompatStoreConcurrencyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLITmuxCompatStoreConcurrencyTests.swift; sourceTree = ""; }; 20A03BD3D5FCAFBEA0D8CA4C /* CLIVMDevTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIVMDevTests.swift; sourceTree = ""; }; 7C52C82B48E0D40919A92FF1 /* CLIVMLayoutEnvTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIVMLayoutEnvTests.swift; sourceTree = ""; }; A5D4120EA1B2C3D4E5F60F02 /* CLIVMTransferTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIVMTransferTests.swift; sourceTree = ""; }; 773600000000000000000002 /* CLIWindowCommandMockServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIWindowCommandMockServer.swift; sourceTree = ""; }; - 773600000000000000000004 /* CLIWindowHandleRoutingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIWindowHandleRoutingTests.swift; sourceTree = ""; }; + C1175700000000000000001C /* CLIWindowHandleRoutingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIWindowHandleRoutingTests.swift; sourceTree = ""; }; 846600000000000000000002 /* CLIWorkspaceGroupSafetyMockServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIWorkspaceGroupSafetyMockServer.swift; sourceTree = ""; }; - 846600000000000000000004 /* CLIWorkspaceGroupSafetyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIWorkspaceGroupSafetyTests.swift; sourceTree = ""; }; + C1175700000000000000001D /* CLIWorkspaceGroupSafetyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIWorkspaceGroupSafetyTests.swift; sourceTree = ""; }; 99CD6A76F75825E105715C8C /* CLIWorkspaceRefResolutionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CLIWorkspaceRefResolutionTests.swift"; sourceTree = ""; }; 799100000000000000000002 /* CLIWorkspaceStableIDMockServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIWorkspaceStableIDMockServer.swift; sourceTree = ""; }; - 799100000000000000000004 /* CLIWorkspaceStableIDTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIWorkspaceStableIDTests.swift; sourceTree = ""; }; + C1175700000000000000001E /* CLIWorkspaceStableIDTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIWorkspaceStableIDTests.swift; sourceTree = ""; }; 575000020000000000000001 /* CLIWriteDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLIWriteDescriptor.swift; sourceTree = ""; }; C51A73000000000000000102 /* ClosedItemHistory+PanelTitle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ClosedItemHistory+PanelTitle.swift"; sourceTree = ""; }; C10D51700000000000000001 /* ClosedItemHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClosedItemHistory.swift; sourceTree = ""; }; @@ -5509,6 +5543,7 @@ C0DE64950000000000000006 /* CMUXCLISessionsListTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CMUXCLISessionsListTests.swift; sourceTree = ""; }; C0DE6495000000000000000C /* CMUXCLISessionsListTranscriptPidTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CMUXCLISessionsListTranscriptPidTests.swift; sourceTree = ""; }; C0DE64950000000000000004 /* CMUXCLITestAssertions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CMUXCLITestAssertions.swift; sourceTree = ""; }; + C11757000000000000000005 /* cmuxCLITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = cmuxCLITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; A5001651 /* CmuxConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxConfig.swift; sourceTree = ""; }; A5FB120A /* CmuxConfigActionDefinition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxConfigActionDefinition.swift; sourceTree = ""; }; A5FB1202 /* CmuxConfigActionSaver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxConfigActionSaver.swift; sourceTree = ""; }; @@ -5565,7 +5600,7 @@ 468140000000000000000008 /* CmuxNotificationHookParsedConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxNotificationHookParsedConfig.swift; sourceTree = ""; }; C85390050000000000000002 /* CMUXOpenCommandLocationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CMUXOpenCommandLocationTests.swift; sourceTree = ""; }; C0DE31390000000000000102 /* CMUXOpenCommandTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CMUXOpenCommandTests.swift; sourceTree = ""; }; - C0DE31390000000000000112 /* CMUXOpenHTMLFocusTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CMUXOpenHTMLFocusTests.swift; sourceTree = ""; }; + C1175700000000000000001F /* CMUXOpenHTMLFocusTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CMUXOpenHTMLFocusTests.swift; sourceTree = ""; }; 96063440572D43F6AB2B499D /* CmuxPopoverGroupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CmuxPopoverGroupTests.swift"; sourceTree = ""; }; A9F100000000000000000005 /* CmuxRuntimeDebugCapture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxRuntimeDebugCapture.swift; sourceTree = ""; }; A9F100000000000000000006 /* CmuxRuntimeDebugCaptureConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxRuntimeDebugCaptureConfiguration.swift; sourceTree = ""; }; @@ -5696,11 +5731,11 @@ C0DECAFE0000000000000002 /* CodexTeamsApprovalBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodexTeamsApprovalBridge.swift; sourceTree = ""; }; C0DEBACC0000000000000004 /* CodexTeamsAppServerFixture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodexTeamsAppServerFixture.swift; sourceTree = ""; }; A10240020000000000000001 /* CodexTeamsAppServerProcess.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodexTeamsAppServerProcess.swift; sourceTree = ""; }; - A10240050000000000000001 /* CodexTeamsAppServerProcessTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodexTeamsAppServerProcessTests.swift; sourceTree = ""; }; + C11757000000000000000022 /* CodexTeamsAppServerProcessTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodexTeamsAppServerProcessTests.swift; sourceTree = ""; }; A10240090000000000000001 /* CodexTeamsPOSIXSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodexTeamsPOSIXSupport.swift; sourceTree = ""; }; - C0DEBACC0000000000000006 /* CodexTeamsResumedBackfillTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodexTeamsResumedBackfillTests.swift; sourceTree = ""; }; + C11757000000000000000023 /* CodexTeamsResumedBackfillTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodexTeamsResumedBackfillTests.swift; sourceTree = ""; }; C0DEBACC0000000000000005 /* CodexTeamsSocketFixture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodexTeamsSocketFixture.swift; sourceTree = ""; }; - C0DE7101C0DE7101C0DE7101 /* CodexTerminalErrorNotificationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodexTerminalErrorNotificationTests.swift; sourceTree = ""; }; + C11757000000000000000024 /* CodexTerminalErrorNotificationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodexTerminalErrorNotificationTests.swift; sourceTree = ""; }; 918100000000000000000080 /* CodexTranscriptFailureReadResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodexTranscriptFailureReadResult.swift; sourceTree = ""; }; 918100000000000000000020 /* CodexTranscriptMonitorStopReplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodexTranscriptMonitorStopReplay.swift; sourceTree = ""; }; 918100000000000000000070 /* CodexTranscriptSubagentSignals.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodexTranscriptSubagentSignals.swift; sourceTree = ""; }; @@ -6286,7 +6321,7 @@ 468160000000000000000002 /* KeyboardShortcutSettingsObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardShortcutSettingsObserverTests.swift; sourceTree = ""; }; A17110000000000000000002 /* KeyboardShortcutSpaceKeyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardShortcutSpaceKeyTests.swift; sourceTree = ""; }; C34670040000000000000002 /* KeyboardShortcutTestSettingsIsolation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardShortcutTestSettingsIsolation.swift; sourceTree = ""; }; - 827300000000000000000003 /* KimiHookConfigLocationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KimiHookConfigLocationTests.swift; sourceTree = ""; }; + C11757000000000000000025 /* KimiHookConfigLocationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KimiHookConfigLocationTests.swift; sourceTree = ""; }; 858200000000000000000001 /* KimiRestorableAgentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KimiRestorableAgentTests.swift; sourceTree = ""; }; 8582A1000000000000000001 /* KimiResumeReviewRegressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KimiResumeReviewRegressionTests.swift; sourceTree = ""; }; F2EE0C080FF0FCCE8A8CA930 /* LastSurfaceClosePreferenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastSurfaceClosePreferenceTests.swift; sourceTree = ""; }; @@ -8255,6 +8290,17 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + C11757000000000000000007 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C1175700000000000000004E /* CmuxAgentJournal in Frameworks */, + C1175700000000000000004D /* CMUXAgentLaunch in Frameworks */, + C1175700000000000000004F /* CmuxFoundation in Frameworks */, + C11757000000000000000050 /* CmuxSettings in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; A5001030 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -8426,6 +8472,59 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + C11757000000000000000003 /* cmuxCLITests */ = { + isa = PBXGroup; + children = ( + C1175700000000000000000E /* BundledCLITestSupport.swift */, + C1175700000000000000000F /* CLIClaudeHookTimeoutRegressionTests.swift */, + C11757000000000000000010 /* CLICoderouterBootstrapTests.swift */, + C11757000000000000000011 /* CLICodexHookPathQuotingRegressionTests.swift */, + C11757000000000000000012 /* CLICodexHookTimeoutRegressionTests.swift */, + C11757000000000000000013 /* CLICodexQueuedHookContractTests.swift */, + C11757000000000000000014 /* CLICodexResumeNotificationTests.swift */, + C11757000000000000000015 /* CLIExplicitSurfaceRoutingTests.swift */, + C11757000000000000000016 /* CLIHookNoResponseTests.swift */, + C11757000000000000000017 /* CLIOmpSupersededCleanupTests.swift */, + C11757000000000000000018 /* CLIRelayQueuedHookRegressionTests.swift */, + C11757000000000000000019 /* CLISSHPTYResizeInputTests.swift */, + C1175700000000000000001A /* CLIStdioSIGPIPERegressionTests.swift */, + C1175700000000000000001B /* CLITmuxCompatStoreConcurrencyTests.swift */, + C1175700000000000000001C /* CLIWindowHandleRoutingTests.swift */, + C1175700000000000000001D /* CLIWorkspaceGroupSafetyTests.swift */, + C1175700000000000000001E /* CLIWorkspaceStableIDTests.swift */, + C1175700000000000000001F /* CMUXOpenHTMLFocusTests.swift */, + C11757000000000000000020 /* CampfireHookNotificationTests.swift */, + C11757000000000000000021 /* ClaudeWrapperResumeEnvironmentTests.swift */, + C11757000000000000000022 /* CodexTeamsAppServerProcessTests.swift */, + C11757000000000000000023 /* CodexTeamsResumedBackfillTests.swift */, + C11757000000000000000024 /* CodexTerminalErrorNotificationTests.swift */, + C11757000000000000000025 /* KimiHookConfigLocationTests.swift */, + ); + path = cmuxCLITests; + sourceTree = ""; + }; + C11757000000000000000004 /* cmuxCLITestSupport */ = { + isa = PBXGroup; + children = ( + 2A0C49412A8AEDDDFA20067B /* AgentHookTestNotificationPipeline.swift */, + A77A000BA1B2C3D4E5F60001 /* AgentJournalTestSupport.swift */, + 95C00A2330C34E7DA7630E4D /* CLIChildEnvironment.swift */, + C0D3F1F10000000000000104 /* CLICodexHookTimeoutRegressionTestSupport.swift */, + C11757000000000000000051 /* CLIHookProcessRunner.swift */, + C11757000000000000000053 /* CLITestBundleAnchor.swift */, + 773600000000000000000002 /* CLIWindowCommandMockServer.swift */, + 846600000000000000000002 /* CLIWorkspaceGroupSafetyMockServer.swift */, + 799100000000000000000002 /* CLIWorkspaceStableIDMockServer.swift */, + 7939000AAA11BB22CC33DD0A /* ClaudeHookLiveDeliveryTargetTestSupport.swift */, + D1893BDBF86104694F22BC3C /* CodexHookCapturedSocketCommands.swift */, + C0DEBACC0000000000000004 /* CodexTeamsAppServerFixture.swift */, + C0DEBACC0000000000000005 /* CodexTeamsSocketFixture.swift */, + 3B508AD047EB519F849B4F21 /* ClaudeHookLiveDeliverySocketState.swift */, + E06E7588C05AC6A8659CFA51 /* ProcessExitWait.swift */, + ); + path = cmuxCLITestSupport; + sourceTree = ""; + }; 087C454FFF74443AB06942C3 /* Resources */ = { isa = PBXGroup; children = ( @@ -9074,6 +9173,8 @@ A5001018 /* cmux-Bridging-Header.h */, 3196C9C2D01F054C1D3385DD /* cmuxUITests */, F1000003A1B2C3D4E5F60718 /* cmuxTests */, + C11757000000000000000003 /* cmuxCLITests */, + C11757000000000000000004 /* cmuxCLITestSupport */, 7A0CE1000000000000000053 /* TunnelExtension */, A5001042 /* Products */, ); @@ -11160,6 +11261,7 @@ 7A0CE1000000000000000026 /* cmuxTunnel.systemextension */, 7E7E6EF344A568AC7FEE3715 /* cmuxUITests.xctest */, F1000002A1B2C3D4E5F60718 /* cmuxTests.xctest */, + C11757000000000000000005 /* cmuxCLITests.xctest */, ); name = Products; sourceTree = ""; @@ -11628,7 +11730,6 @@ C73660030000000000000002 /* WorkspaceRemoteBadgeTruthTests.swift */, F6100001A1B2C3D4E5F60718 /* WorkspaceRemoteConnectionTests.swift */, F6100001A1B2C3D4E5F60719 /* WorkspaceRemoteDaemonRecoveryTests.swift */, - C0DE7101C0DE7101C0DE7101 /* CodexTerminalErrorNotificationTests.swift */, 7277B0017277B0017277B001 /* WorkspaceRemoteDirectoryProvenanceTests.swift */, F6357301A1B2C3D4E5F60718 /* WorkspaceRemoteReconnectPolicyTests.swift */, 0C4AD348F196FBBEE52D53A7 /* SSHPTYAttachReconnectInputFilterTests.swift */, @@ -11642,7 +11743,6 @@ F6724603A1B2C3D4E5F67246 /* RemoteTmuxHostRemoteCommandOverrideTests.swift */, F6724601A1B2C3D4E5F67246 /* SSHConfiguredRemoteCommandHostTests.swift */, 4575F4BB7F52CF88AB6D0022 /* SSHStartupCommandTestSupport.swift */, - E06E7588C05AC6A8659CFA51 /* ProcessExitWait.swift */, 37B04BC7D56945E59781D11F /* SSHStartupManualReconnectTests+Fixtures.swift */, EDF9EEAA233347858BCF3320 /* SSHConfiguredRemoteCommandHostTests+Harness.swift */, A82715B8BDE347A18031067C /* CLINotifyProcessIntegrationRegressionTests+JSON.swift */, @@ -11823,7 +11923,6 @@ AA9457FR0000000000000001 /* AutoNamingClaudeArgumentsTests.swift */, 07EC52007B2144C59C047774 /* AutoNamingGrokAdapterTests.swift */, 46792DAA478444ED87B17B31 /* AutoNamingHookPayloadAdapterTests.swift */, - CE7000000000000000000002 /* ClaudeWrapperResumeEnvironmentTests.swift */, F016B5C09357B3226FA2E014 /* SidebarWorkspaceSnapshotRefreshPolicyTests.swift */, A6AC73040000000000000002 /* SidebarWorkspaceSnapshotAgentActivityTests.swift */, C9A57514C9A57514C9A57514 /* SidebarScrollViewConfiguratorTests.swift */, @@ -11861,25 +11960,12 @@ C85390050000000000000002 /* CMUXOpenCommandLocationTests.swift */, 2720F0012720F0012720F001 /* BrowserProfileSocketTests.swift */, A6AC72080000000000000002 /* CMUXWorkspaceLoadingCLIRegressionTests.swift */, - C0DE31390000000000000112 /* CMUXOpenHTMLFocusTests.swift */, C0DE75890000000000000004 /* DeflatedAssetTestSupport.swift */, - 1590EE4F01FEF02D40A48698 /* CLIExplicitSurfaceRoutingTests.swift */, A1002000000000000000000A /* SurfaceSelectionTests.swift */, A10020000000000000000018 /* SurfaceSelectionNavigationLoader.swift */, 93300002A1B2C3D4E5F60718 /* SocketTerminalBindingRegressionTests.swift */, A7367002A1B2C3D4E5F60718 /* CLISSHSessionAttachAnchorTests.swift */, - C0DEBACC0000000000000004 /* CodexTeamsAppServerFixture.swift */, - A10240050000000000000001 /* CodexTeamsAppServerProcessTests.swift */, - C0DEBACC0000000000000006 /* CodexTeamsResumedBackfillTests.swift */, - C0DEBACC0000000000000005 /* CodexTeamsSocketFixture.swift */, - 773600000000000000000002 /* CLIWindowCommandMockServer.swift */, - 773600000000000000000004 /* CLIWindowHandleRoutingTests.swift */, - 846600000000000000000002 /* CLIWorkspaceGroupSafetyMockServer.swift */, - 846600000000000000000004 /* CLIWorkspaceGroupSafetyTests.swift */, - 799100000000000000000002 /* CLIWorkspaceStableIDMockServer.swift */, - 799100000000000000000004 /* CLIWorkspaceStableIDTests.swift */, AC61257DCDCA431A908C55CC /* CLIChildEnvironmentTests.swift */, - 95C00A2330C34E7DA7630E4D /* CLIChildEnvironment.swift */, 805500000000000000000002 /* CLIBrowserEvalOutputTests.swift */, CA11E4D0FA017DE500000F102 /* CLICallerWorkspaceDefaultTests.swift */, C0DE31390000000000000106 /* CMUXCLIErrorOutputRegressionTests.swift */, @@ -11893,7 +11979,6 @@ C0DE6495000000000000000C /* CMUXCLISessionsListTranscriptPidTests.swift */, C0DE64950000000000000004 /* CMUXCLITestAssertions.swift */, C12985000000000000000003 /* CMUXCLISentryTelemetryRegressionTests.swift */, - 827300000000000000000003 /* KimiHookConfigLocationTests.swift */, 858200000000000000000001 /* KimiRestorableAgentTests.swift */, 8582A1000000000000000001 /* KimiResumeReviewRegressionTests.swift */, C47110010000000000000002 /* ProcessPipeReadCrashRegressionTests.swift */, @@ -12255,11 +12340,9 @@ A5D41221A1B2C3D4E5F60718 /* ClaudeHookSurfaceResolutionSwiftTests.swift */, 79390002AA11BB22CC33DD02 /* ClaudeHookLiveDeliveryTargetTests.swift */, 89930001AABBCCDDEEFF0002 /* CLIOmpHookBindingTests.swift */, - 89930006AABBCCDDEEFF0002 /* CLIOmpSupersededCleanupTests.swift */, 7939000CAA11BB22CC33DD0C /* ClaudeHookLifecycleCleanupTests.swift */, A12123000000000000000010 /* ClaudeHookHibernationGenerationHistoryTests.swift */, A12123000000000000000012 /* ClaudeHookHibernationSessionEndPreservationTests.swift */, - 7939000AAA11BB22CC33DD0A /* ClaudeHookLiveDeliveryTargetTestSupport.swift */, 79390004AA11BB22CC33DD04 /* AgentNotificationLiveRetargetTests.swift */, 2F2C0D107664F88CAE6E4E45 /* AgentSemanticNotificationDeliveryTests.swift */, 79390010AA11BB22CC33DD10 /* AgentNotificationMoveRaceTests.swift */, @@ -12271,14 +12354,10 @@ A5D41223A1B2C3D4E5F60718 /* ClaudeNotificationStatusLifecycleTests.swift */, A5D41231A1B2C3D4E5F60718 /* AgentNotificationGateTests.swift */, A76110020000000000000002 /* AgentHookNotificationPolicyTests.swift */, - A77A000BA1B2C3D4E5F60001 /* AgentJournalTestSupport.swift */, A77A000DA1B2C3D4E5F60001 /* AgentJournalLifecycleCenterTests.swift */, 7B74E6D94323FE9C5804E0B4 /* FeedWaiterRegistryTests.swift */, - D1893BDBF86104694F22BC3C /* CodexHookCapturedSocketCommands.swift */, - 3B508AD047EB519F849B4F21 /* ClaudeHookLiveDeliverySocketState.swift */, D1136E25CD5300988F9F2162 /* ClaudeHookSurfaceSocketState.swift */, 1E40480ECEC94949BB74D852 /* CLINotifyProcessSocketState.swift */, - 2A0C49412A8AEDDDFA20067B /* AgentHookTestNotificationPipeline.swift */, A5D41233A1B2C3D4E5F60718 /* ClaudeBackgroundWorkNotifyTests.swift */, F0F0CF0E0000000000000002 /* CLINotifyClaudeForkOfForkRegressionTests.swift */, C72280000000000000000003 /* CLINotifyClaudeHookWorkspaceRoutingTests.swift */, @@ -12289,7 +12368,6 @@ A5D41204A1B2C3D4E5F60718 /* CLINotifyProcessIntegrationRegressionTests.swift */, 11189001A1B2C3D4E5F60718 /* CLINotificationPaneAttributionTests.swift */, A76110030000000000000002 /* CLIGrokNotificationNoiseTests.swift */, - 6379A0026379A0026379A002 /* CLISSHPTYResizeInputTests.swift */, C0F16B000000000000000002 /* CLIRemoteShellStartupPerformanceTests.swift */, A5D41331A1B2C3D4E5F60718 /* CLIMockSocketServerSupport.swift */, A5D41206A1B2C3D4E5F60718 /* CLINotifyProcessTestSupport.swift */, @@ -12301,29 +12379,18 @@ D0F102000000000000000003 /* CLISSHPTYAttachBridgeServer.swift */, F12713000000000000000001 /* CLISSHPTYStopPipe.swift */, CD3B5F249DECAB446CC71D32 /* MockBridgeInputCapture.swift */, - C12984000000000000000003 /* CLIStdioSIGPIPERegressionTests.swift */, B05553B10000000000000002 /* CLITmuxCompatRemoteSplitTests.swift */, - C0DE11260000000000000002 /* CLITmuxCompatStoreConcurrencyTests.swift */, 7837E0067837E0067837E006 /* CLITmuxCompatResizePaneTests.swift */, - C0D3F1F00000000000000104 /* CLICodexHookTimeoutRegressionTests.swift */, - C0D3F1F30000000000000104 /* CLICodexHookPathQuotingRegressionTests.swift */, C0D3F1F20000000000000104 /* CLICodexYoloResumePersistenceTests.swift */, - C0D3F1F10000000000000104 /* CLICodexHookTimeoutRegressionTestSupport.swift */, - 918100000000000000000001 /* CLICodexResumeNotificationTests.swift */, - C85350000000000000000102 /* CLIClaudeHookTimeoutRegressionTests.swift */, C85350000000000000000104 /* AgentHookDeliveryQueueTests.swift */, - C85350000000000000000106 /* CLICodexQueuedHookContractTests.swift */, - C85350000000000000000108 /* CLIRelayQueuedHookRegressionTests.swift */, C6711B010000000000000001 /* CLICodexWeakEnvironmentRestoreBindingTests.swift */, D96290020000000000000001 /* CodexResumeBindingVerificationRegressionTests.swift */, A5D41208A1B2C3D4E5F60718 /* CLIGenericHookPersistenceTests.swift */, - A5D41212A1B2C3D4E5F60718 /* CLIHookNoResponseTests.swift */, A5D4120AA1B2C3D4E5F60718 /* CLIRovoDevHookPersistenceTests.swift */, A5D4120CA1B2C3D4E5F60718 /* CLILegacyHookAliasTests.swift */, A5D4120EA1B2C3D4E5F60718 /* CLIAuthAliasTests.swift */, C0DE60C0000000000000A031 /* CLICoderouterCommandTests.swift */, F6572063A1B2C3D4E5F60718 /* RestoreAdmissionRetryTests.swift */, - C0DE60C0000000000000A035 /* CLICoderouterBootstrapTests.swift */, A5D4120EA1B2C3D4E5F60F02 /* CLIVMTransferTests.swift */, 7C52C82B48E0D40919A92FF1 /* CLIVMLayoutEnvTests.swift */, 12AA4D8BC110BC7543621C26 /* CloudTunnelLaunchGateTests.swift */, @@ -12337,7 +12404,6 @@ C37800000000000000000002 /* VMSSHCommandTests.swift */, 0A0F00550000000000000002 /* OmpSupportTests.swift */, 0A1107110000000000000002 /* OllamaAgentDetectionTests.swift */, - 0A0F00560000000000000004 /* CampfireHookNotificationTests.swift */, 0A0F00570000000000000004 /* CampfireSupportProcessBindingTests.swift */, 0A0F00550000000000000004 /* CampfireSupportTests.swift */, 0A0F00580000000000000004 /* CampfireSupportTestSupport.swift */, @@ -12576,6 +12642,30 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + C11757000000000000000002 /* cmuxCLITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = C11757000000000000000009 /* Build configuration list for PBXNativeTarget "cmuxCLITests" */; + buildPhases = ( + C11757000000000000000006 /* Sources */, + C11757000000000000000007 /* Frameworks */, + C11757000000000000000008 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + C1175700000000000000000C /* PBXTargetDependency */, + ); + name = cmuxCLITests; + packageProductDependencies = ( + A5B00002A1B2C3D4E5F60718 /* CMUXAgentLaunch */, + A77A0002A1B2C3D4E5F60001 /* CmuxAgentJournal */, + 5E2701040000000000000002 /* CmuxFoundation */, + 5E27010400000000000000A3 /* CmuxSettings */, + ); + productName = cmuxCLITests; + productReference = C11757000000000000000005 /* cmuxCLITests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; A5001050 /* cmux */ = { isa = PBXNativeTarget; buildConfigurationList = A5001060 /* Build configuration list for PBXNativeTarget "cmux" */; @@ -12949,11 +13039,19 @@ B9000005A1B2C3D4E5F60719 /* cmux-cli */, CB450DF0F0B3839599082C4D /* cmuxUITests */, F1000004A1B2C3D4E5F60718 /* cmuxTests */, + C11757000000000000000002 /* cmuxCLITests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + C11757000000000000000008 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; A5001102 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -13184,6 +13282,54 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + C11757000000000000000006 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C11757000000000000000040 /* AgentHookTestNotificationPipeline.swift in Sources */, + C11757000000000000000041 /* AgentJournalTestSupport.swift in Sources */, + C11757000000000000000027 /* BundledCLITestSupport.swift in Sources */, + C11757000000000000000039 /* CampfireHookNotificationTests.swift in Sources */, + C11757000000000000000060 /* ClaudeHookLiveDeliverySocketState.swift in Sources */, + C11757000000000000000049 /* ClaudeHookLiveDeliveryTargetTestSupport.swift in Sources */, + C1175700000000000000003A /* ClaudeWrapperResumeEnvironmentTests.swift in Sources */, + C11757000000000000000042 /* CLIChildEnvironment.swift in Sources */, + C11757000000000000000028 /* CLIClaudeHookTimeoutRegressionTests.swift in Sources */, + C11757000000000000000029 /* CLICoderouterBootstrapTests.swift in Sources */, + C1175700000000000000002A /* CLICodexHookPathQuotingRegressionTests.swift in Sources */, + C1175700000000000000002B /* CLICodexHookTimeoutRegressionTests.swift in Sources */, + C11757000000000000000043 /* CLICodexHookTimeoutRegressionTestSupport.swift in Sources */, + C1175700000000000000002C /* CLICodexQueuedHookContractTests.swift in Sources */, + C1175700000000000000002D /* CLICodexResumeNotificationTests.swift in Sources */, + C1175700000000000000002E /* CLIExplicitSurfaceRoutingTests.swift in Sources */, + C1175700000000000000002F /* CLIHookNoResponseTests.swift in Sources */, + C11757000000000000000044 /* CLIHookProcessRunner.swift in Sources */, + C11757000000000000000030 /* CLIOmpSupersededCleanupTests.swift in Sources */, + C11757000000000000000031 /* CLIRelayQueuedHookRegressionTests.swift in Sources */, + C11757000000000000000032 /* CLISSHPTYResizeInputTests.swift in Sources */, + C11757000000000000000033 /* CLIStdioSIGPIPERegressionTests.swift in Sources */, + C11757000000000000000045 /* CLITestBundleAnchor.swift in Sources */, + C11757000000000000000034 /* CLITmuxCompatStoreConcurrencyTests.swift in Sources */, + C11757000000000000000046 /* CLIWindowCommandMockServer.swift in Sources */, + C11757000000000000000035 /* CLIWindowHandleRoutingTests.swift in Sources */, + C11757000000000000000047 /* CLIWorkspaceGroupSafetyMockServer.swift in Sources */, + C11757000000000000000036 /* CLIWorkspaceGroupSafetyTests.swift in Sources */, + C11757000000000000000048 /* CLIWorkspaceStableIDMockServer.swift in Sources */, + C11757000000000000000037 /* CLIWorkspaceStableIDTests.swift in Sources */, + C11757000000000000000038 /* CMUXOpenHTMLFocusTests.swift in Sources */, + C1175700000000000000004A /* CodexHookCapturedSocketCommands.swift in Sources */, + C1175700000000000000004B /* CodexTeamsAppServerFixture.swift in Sources */, + C11757000000000000000061 /* CodexTeamsAppServerProcess.swift in Sources */, + C1175700000000000000003B /* CodexTeamsAppServerProcessTests.swift in Sources */, + C11757000000000000000062 /* CodexTeamsPOSIXSupport.swift in Sources */, + C1175700000000000000003C /* CodexTeamsResumedBackfillTests.swift in Sources */, + C1175700000000000000004C /* CodexTeamsSocketFixture.swift in Sources */, + C1175700000000000000003D /* CodexTerminalErrorNotificationTests.swift in Sources */, + C1175700000000000000003E /* KimiHookConfigLocationTests.swift in Sources */, + C11757000000000000000063 /* ProcessExitWait.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; A5001051 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -16256,7 +16402,6 @@ C0DE62600000000000000001 /* BrowserWindowPortalRegistryNotificationTests.swift in Sources */, C0DE35530000000000000101 /* BundledCLILinkageTests.swift in Sources */, CAFE00000000000000000006 /* CaffeineControllerTests.swift in Sources */, - 0A0F00560000000000000003 /* CampfireHookNotificationTests.swift in Sources */, 0A0F00570000000000000003 /* CampfireSupportProcessBindingTests.swift in Sources */, 0A0F00550000000000000003 /* CampfireSupportTests.swift in Sources */, 0A0F00580000000000000003 /* CampfireSupportTestSupport.swift in Sources */, @@ -16277,30 +16422,22 @@ A5D41220A1B2C3D4E5F60718 /* ClaudeHookSurfaceResolutionSwiftTests.swift in Sources */, CC7FDF58CBB0EDDE5DADE167 /* ClaudeHookSurfaceSocketState.swift in Sources */, A5D41222A1B2C3D4E5F60718 /* ClaudeNotificationStatusLifecycleTests.swift in Sources */, - CE7000000000000000000001 /* ClaudeWrapperResumeEnvironmentTests.swift in Sources */, 9758A0000000000000000004 /* CLIAmpLifecycleIntegrationTests.swift in Sources */, A5D4120DA1B2C3D4E5F60718 /* CLIAuthAliasTests.swift in Sources */, 805500000000000000000001 /* CLIBrowserEvalOutputTests.swift in Sources */, CA11E4D0FA017DE500000B101 /* CLICallerWorkspaceDefaultTests.swift in Sources */, 964F2AF080DB48D8A2A2C186 /* CLIChildEnvironment.swift in Sources */, 2E931B5728B0478CB9B4937F /* CLIChildEnvironmentTests.swift in Sources */, - C85350000000000000000101 /* CLIClaudeHookTimeoutRegressionTests.swift in Sources */, - C0DE60C0000000000000A036 /* CLICoderouterBootstrapTests.swift in Sources */, C0DE60C0000000000000A032 /* CLICoderouterCommandTests.swift in Sources */, - C0D3F1F30000000000000103 /* CLICodexHookPathQuotingRegressionTests.swift in Sources */, - C0D3F1F00000000000000103 /* CLICodexHookTimeoutRegressionTests.swift in Sources */, C0D3F1F10000000000000103 /* CLICodexHookTimeoutRegressionTestSupport.swift in Sources */, - C85350000000000000000105 /* CLICodexQueuedHookContractTests.swift in Sources */, - 918100000000000000000002 /* CLICodexResumeNotificationTests.swift in Sources */, C6711A010000000000000001 /* CLICodexWeakEnvironmentRestoreBindingTests.swift in Sources */, C0D3F1F20000000000000103 /* CLICodexYoloResumePersistenceTests.swift in Sources */, C11218000000000000000053 /* CLIError.swift in Sources */, C51600000000000000000001 /* CLIExecInheritedSignalStateTests.swift in Sources */, - E295EA3753206FE3FFCA6C0A /* CLIExplicitSurfaceRoutingTests.swift in Sources */, C46790000000000000000001 /* CLIForwardingLaunchArgumentTests.swift in Sources */, A5D41207A1B2C3D4E5F60718 /* CLIGenericHookPersistenceTests.swift in Sources */, A76110030000000000000001 /* CLIGrokNotificationNoiseTests.swift in Sources */, - A5D41211A1B2C3D4E5F60718 /* CLIHookNoResponseTests.swift in Sources */, + C11757000000000000000052 /* CLIHookProcessRunner.swift in Sources */, 74346B19AC8D55243926D168 /* CLILegacyCodexRestoreLeaseTests.swift in Sources */, A5D4120BA1B2C3D4E5F60718 /* CLILegacyHookAliasTests.swift in Sources */, C10657000000000000000003 /* CLILocalTmuxIdentityRegressionTests.swift in Sources */, @@ -16315,8 +16452,6 @@ 86D573598C8C1225D24AED5A /* CLINotifyProcessSocketState.swift in Sources */, A5D41205A1B2C3D4E5F60718 /* CLINotifyProcessTestSupport.swift in Sources */, 89930001AABBCCDDEEFF0001 /* CLIOmpHookBindingTests.swift in Sources */, - 89930006AABBCCDDEEFF0001 /* CLIOmpSupersededCleanupTests.swift in Sources */, - C85350000000000000000107 /* CLIRelayQueuedHookRegressionTests.swift in Sources */, C0F16B000000000000000001 /* CLIRemoteShellStartupPerformanceTests.swift in Sources */, A5D41209A1B2C3D4E5F60718 /* CLIRovoDevHookPersistenceTests.swift in Sources */, FE89827F4FAB9F7524A38B41 /* CLISendQueuedOutputTests.swift in Sources */, @@ -16327,23 +16462,18 @@ C73660020000000000000001 /* CLISSHPTYAttachSessionLostRespawnTests.swift in Sources */, 101CB3DBAD66CD6E3DBFB10E /* CLISSHPTYAttachTestHarness.swift in Sources */, C77070000000000000000004 /* CLISSHPTYAttachTransientBridgeFailureExitCodeTests.swift in Sources */, - 6379A0016379A0016379A001 /* CLISSHPTYResizeInputTests.swift in Sources */, F12713000000000000000101 /* CLISSHPTYStopPipe.swift in Sources */, A7367001A1B2C3D4E5F60718 /* CLISSHSessionAttachAnchorTests.swift in Sources */, - C12984000000000000000004 /* CLIStdioSIGPIPERegressionTests.swift in Sources */, + C11757000000000000000054 /* CLITestBundleAnchor.swift in Sources */, B05553B10000000000000001 /* CLITmuxCompatRemoteSplitTests.swift in Sources */, 7837E0057837E0057837E005 /* CLITmuxCompatResizePaneTests.swift in Sources */, - C0DE11260000000000000001 /* CLITmuxCompatStoreConcurrencyTests.swift in Sources */, 84ED8B0271A2FD801E8F280C /* CLIVMDevTests.swift in Sources */, C78951D0E70B3BF25DFABD26 /* CLIVMLayoutEnvTests.swift in Sources */, A5D4120DA1B2C3D4E5F60F01 /* CLIVMTransferTests.swift in Sources */, 773600000000000000000001 /* CLIWindowCommandMockServer.swift in Sources */, - 773600000000000000000003 /* CLIWindowHandleRoutingTests.swift in Sources */, 846600000000000000000001 /* CLIWorkspaceGroupSafetyMockServer.swift in Sources */, - 846600000000000000000003 /* CLIWorkspaceGroupSafetyTests.swift in Sources */, FBA17843EE21A7F33B4BD0AD /* CLIWorkspaceRefResolutionTests.swift in Sources */, 799100000000000000000001 /* CLIWorkspaceStableIDMockServer.swift in Sources */, - 799100000000000000000003 /* CLIWorkspaceStableIDTests.swift in Sources */, 7375A0037375A0037375A003 /* ClosedMainWindowRoutingTests.swift in Sources */, D15E75C8E60487168BA7E9DF /* CloudActivationPolicyTests.swift in Sources */, C6D4C5A38097BB05AB98E460 /* CloudAgentSkillLauncherTests.swift in Sources */, @@ -16526,7 +16656,6 @@ 468100000000000000000001 /* CmuxNotificationHookCacheTests.swift in Sources */, C85390050000000000000001 /* CMUXOpenCommandLocationTests.swift in Sources */, C0DE31390000000000000101 /* CMUXOpenCommandTests.swift in Sources */, - C0DE31390000000000000111 /* CMUXOpenHTMLFocusTests.swift in Sources */, CF6237863B42421E82388C27 /* CmuxPopoverGroupTests.swift in Sources */, 7837E0017837E0017837E001 /* CmuxSocketEventMapperTests.swift in Sources */, C3677001000000000000001 /* CmuxSSHURLRequestTests.swift in Sources */, @@ -16558,10 +16687,7 @@ 35241766A401B4F40BFFF49D /* CodexStaleTurnRestoreIntentTests.swift in Sources */, C0DECAFE0000000000000003 /* CodexTeamsApprovalBridge.swift in Sources */, C0DEBACC0000000000000001 /* CodexTeamsAppServerFixture.swift in Sources */, - A10240040000000000000001 /* CodexTeamsAppServerProcessTests.swift in Sources */, - C0DEBACC0000000000000003 /* CodexTeamsResumedBackfillTests.swift in Sources */, C0DEBACC0000000000000002 /* CodexTeamsSocketFixture.swift in Sources */, - C0DE7100C0DE7100C0DE7100 /* CodexTerminalErrorNotificationTests.swift in Sources */, 7520C0DF0000000000000001 /* CodexTurnCompletionOwnershipTests.swift in Sources */, C0DEC0DE000000000000F202 /* CommandPaletteNucleoFFILibrarySupport.swift in Sources */, C0DEC0DE000000000000F102 /* CommandPaletteNucleoFFITests.swift in Sources */, @@ -16716,7 +16842,6 @@ 468160000000000000000001 /* KeyboardShortcutSettingsObserverTests.swift in Sources */, A17110000000000000000001 /* KeyboardShortcutSpaceKeyTests.swift in Sources */, C34670040000000000000001 /* KeyboardShortcutTestSettingsIsolation.swift in Sources */, - 827300000000000000000004 /* KimiHookConfigLocationTests.swift in Sources */, 858200000000000000000002 /* KimiRestorableAgentTests.swift in Sources */, 8582A1000000000000000002 /* KimiResumeReviewRegressionTests.swift in Sources */, 87C609D63F03597AC12C8B0A /* LastSurfaceClosePreferenceTests.swift in Sources */, @@ -17229,6 +17354,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + C1175700000000000000000C /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = B9000005A1B2C3D4E5F60719 /* cmux-cli */; + targetProxy = C1175700000000000000000D /* PBXContainerItemProxy */; + }; 32568B0DCFC8656BA952468E /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = A5001050 /* cmux */; @@ -17261,6 +17391,37 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + C1175700000000000000000A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 106; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 14.0; + MARKETING_VERSION = 0.64.25; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.cmuxterm.clitests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + C1175700000000000000000B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 106; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 14.0; + MARKETING_VERSION = 0.64.25; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = com.cmuxterm.clitests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; 442A6A86CAC6E8AD27FDD8F2 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { @@ -17648,6 +17809,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + C11757000000000000000009 /* Build configuration list for PBXNativeTarget "cmuxCLITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C1175700000000000000000A /* Debug */, + C1175700000000000000000B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; A5001060 /* Build configuration list for PBXNativeTarget "cmux" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/cmux.xcodeproj/xcshareddata/xcschemes/cmux-cli-tests.xcscheme b/cmux.xcodeproj/xcshareddata/xcschemes/cmux-cli-tests.xcscheme new file mode 100644 index 000000000000..a34413a207ca --- /dev/null +++ b/cmux.xcodeproj/xcshareddata/xcschemes/cmux-cli-tests.xcscheme @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cmuxTests/AgentHookTestNotificationPipeline.swift b/cmuxCLITestSupport/AgentHookTestNotificationPipeline.swift similarity index 97% rename from cmuxTests/AgentHookTestNotificationPipeline.swift rename to cmuxCLITestSupport/AgentHookTestNotificationPipeline.swift index 572a83293ed1..69ec7490c646 100644 --- a/cmuxTests/AgentHookTestNotificationPipeline.swift +++ b/cmuxCLITestSupport/AgentHookTestNotificationPipeline.swift @@ -2,11 +2,6 @@ import CmuxAgentJournal import CmuxSettings import Foundation import Testing -#if canImport(cmux_DEV) -@testable import cmux_DEV -#elseif canImport(cmux) -@testable import cmux -#endif /// The hook socket fixtures run production reconciliation and durable admission. /// Their command trace includes the accepted notification effect in the historical diff --git a/cmuxTests/AgentJournalTestSupport.swift b/cmuxCLITestSupport/AgentJournalTestSupport.swift similarity index 100% rename from cmuxTests/AgentJournalTestSupport.swift rename to cmuxCLITestSupport/AgentJournalTestSupport.swift diff --git a/cmuxTests/CLIChildEnvironment.swift b/cmuxCLITestSupport/CLIChildEnvironment.swift similarity index 62% rename from cmuxTests/CLIChildEnvironment.swift rename to cmuxCLITestSupport/CLIChildEnvironment.swift index 06288fbe9ccd..fd59525af8b9 100644 --- a/cmuxTests/CLIChildEnvironment.swift +++ b/cmuxCLITestSupport/CLIChildEnvironment.swift @@ -6,7 +6,13 @@ struct CLIChildEnvironment { func normalizing(_ environment: [String: String]) -> [String: String] { // Callers scrub CMUX_* from the child; isolation belongs to the host. - guard appHostEnvironment["CMUX_APP_HOST_ISOLATION_REQUIRED"] == "1", + // A host is isolated exactly when its own CFFIXED_USER_HOME is pinned + // to its HOME -- what every CI lane sets, and what a developer's + // machine never has. Reading that condition rather than a lane marker + // lets the host-free CLI lane qualify without claiming an app host. + guard let hostHome = appHostEnvironment["HOME"], + !hostHome.isEmpty, + appHostEnvironment["CFFIXED_USER_HOME"] == hostHome, let rawHome = environment["HOME"]?.trimmingCharacters(in: .whitespacesAndNewlines), !rawHome.isEmpty else { return environment diff --git a/cmuxTests/CLICodexHookTimeoutRegressionTestSupport.swift b/cmuxCLITestSupport/CLICodexHookTimeoutRegressionTestSupport.swift similarity index 97% rename from cmuxTests/CLICodexHookTimeoutRegressionTestSupport.swift rename to cmuxCLITestSupport/CLICodexHookTimeoutRegressionTestSupport.swift index 41b18bdfdf55..1fdb0b722f5c 100644 --- a/cmuxTests/CLICodexHookTimeoutRegressionTestSupport.swift +++ b/cmuxCLITestSupport/CLICodexHookTimeoutRegressionTestSupport.swift @@ -156,6 +156,9 @@ func handleCodexHookMockSocketClient( processBinding: CodexHookMockProcessBinding? = nil ) { defer { Darwin.close(clientFD) } + // Hook clients can time out or exit before a delayed response. Keep a + // disconnected client from terminating the host-free test runner. + guard ignoreSIGPIPE(onAcceptedFixtureSocket: clientFD) else { return } var pending = Data() var buffer = [UInt8](repeating: 0, count: 4096) while true { @@ -181,9 +184,7 @@ func handleCodexHookMockSocketClient( surfaceId: surfaceId, processBinding: processBinding ) + "\n" - _ = response.withCString { ptr in - Darwin.write(clientFD, ptr, strlen(ptr)) - } + guard writeAllToFixtureSocket(response, fd: clientFD) else { return } } } } @@ -270,7 +271,7 @@ func runCodexHookProcess( ) -> CodexHookProcessRunResult { let result: CodexHookProcessRunResult if !fileBackedStandardInput { - let shared = CLINotifyProcessIntegrationRegressionTests.runProcess( + let shared = CLIHookProcessRunner.run( executablePath: executablePath, arguments: arguments, environment: environment, diff --git a/cmuxCLITestSupport/CLIHookProcessRunner.swift b/cmuxCLITestSupport/CLIHookProcessRunner.swift new file mode 100644 index 000000000000..8e693a2c874b --- /dev/null +++ b/cmuxCLITestSupport/CLIHookProcessRunner.swift @@ -0,0 +1,152 @@ +import Darwin +import Dispatch +import Foundation + +// Black-box subprocess runner shared by the app-host test bundle (cmuxTests) +// and the product-level bundle (cmuxCLITests). +// +// It used to live as `CLINotifyProcessIntegrationRegressionTests.runProcess`, +// which tied every hook helper to that one app-host suite. The helpers that +// spawn the bundled CLI do not need an app host, so the runner they share +// cannot be attached to a suite that stays behind: this file is a member of +// both test targets and owns the implementation, while the old static method +// forwards to it. +enum CLIHookProcessRunner { + struct Result { + let status: Int32 + let stdout: String + let stderr: String + let timedOut: Bool + } + + static func run( + executablePath: String, + arguments: [String], + environment: [String: String], + standardInput: String? = nil, + timeout: TimeInterval + ) -> Result { + let process = Process() + let stdoutPipe = Pipe() + let stderrPipe = Pipe() + let stdinPipe = standardInput == nil ? nil : Pipe() + process.executableURL = URL(fileURLWithPath: executablePath) + process.arguments = arguments + process.environment = CLIChildEnvironment( + appHostEnvironment: ProcessInfo.processInfo.environment + ).normalizing(environment) + process.standardInput = stdinPipe ?? FileHandle.nullDevice + process.standardOutput = stdoutPipe + process.standardError = stderrPipe + let exitSignal = DispatchSemaphore(value: 0) + // Observe actual termination instead of scheduling a blocking waiter on + // the same global pool used to drain the child's output. + process.terminationHandler = { _ in exitSignal.signal() } + + // A child may exit or be killed while its stdin writer is blocked. + // Suppress SIGPIPE on this descriptor only, leaving the test process + // and spawned CLI signal dispositions unchanged. + if let stdinPipe, + fcntl(stdinPipe.fileHandleForWriting.fileDescriptor, F_SETNOSIGPIPE, 1) == -1 { + return Result(status: -1, stdout: "", stderr: "Cannot suppress stdin SIGPIPE: \(errno)", timedOut: false) + } + + do { + try process.run() + } catch { + return Result(status: -1, stdout: "", stderr: String(describing: error), timedOut: false) + } + let outputLock = NSLock() + var stdoutData = Data() + var stderrData = Data() + let ioGroup = DispatchGroup() + + ioGroup.enter() + DispatchQueue.global(qos: .utility).async { + let data = stdoutPipe.fileHandleForReading.readDataToEndOfFile() + outputLock.lock() + stdoutData = data + outputLock.unlock() + ioGroup.leave() + } + + ioGroup.enter() + DispatchQueue.global(qos: .utility).async { + let data = stderrPipe.fileHandleForReading.readDataToEndOfFile() + outputLock.lock() + stderrData = data + outputLock.unlock() + ioGroup.leave() + } + + // Start input only after both drains exist: a child can fill either + // output pipe before it reads a large input. The writer shares the + // lifecycle group so timeout termination also releases blocked writes. + if let standardInput, let stdinPipe { + ioGroup.enter() + DispatchQueue.global(qos: .utility).async { + defer { + try? stdinPipe.fileHandleForWriting.close() + ioGroup.leave() + } + try? stdinPipe.fileHandleForWriting.write(contentsOf: Data(standardInput.utf8)) + } + } + + // Termination callbacks may lag behind the actual process exit. + let timedOut = exitSignal.wait(timeout: .now() + timeout) == .timedOut && process.isRunning + if timedOut { + process.terminate() + if exitSignal.wait(timeout: .now() + 1) == .timedOut && process.isRunning { + kill(process.processIdentifier, SIGKILL) + _ = exitSignal.wait(timeout: .now() + 1) + } + } + + _ = ioGroup.wait(timeout: .now() + 2) + + outputLock.lock() + let finalStdoutData = stdoutData + let finalStderrData = stderrData + outputLock.unlock() + return Result( + status: process.isRunning ? SIGKILL : process.terminationStatus, + stdout: String(data: finalStdoutData, encoding: .utf8) ?? "", + stderr: String(data: finalStderrData, encoding: .utf8) ?? "", + timedOut: timedOut + ) + } +} + +/// Keeps a fixture server's write to a client that already hung up from +/// raising SIGPIPE. cmuxCLITests runs without an app host, so nothing else +/// ignores the signal and it would terminate the whole test runner. The option +/// is set per socket so the CLI's own SIGPIPE behavior stays under test. +/// +/// Returns false when the option could not be set. The caller must then close +/// the client without writing to it. +func ignoreSIGPIPE(onAcceptedFixtureSocket fd: Int32) -> Bool { + var noSignal: Int32 = 1 + return setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &noSignal, socklen_t(MemoryLayout.size)) == 0 +} + +/// Writes all of `text` to a fixture socket, retrying short and interrupted +/// writes. Returns false once the write fails, for example after the client +/// hung up. +@discardableResult +func writeAllToFixtureSocket(_ text: String, fd: Int32) -> Bool { + let bytes = Array(text.utf8) + var offset = 0 + while offset < bytes.count { + let written = bytes.withUnsafeBytes { buffer in + Darwin.write(fd, buffer.baseAddress!.advanced(by: offset), buffer.count - offset) + } + if written < 0 { + if errno == EINTR { continue } + return false + } + if written == 0 { return false } + offset += written + } + return true +} diff --git a/cmuxCLITestSupport/CLITestBundleAnchor.swift b/cmuxCLITestSupport/CLITestBundleAnchor.swift new file mode 100644 index 000000000000..028f36fe5ade --- /dev/null +++ b/cmuxCLITestSupport/CLITestBundleAnchor.swift @@ -0,0 +1,9 @@ +import Foundation + +/// A class whose only job is to name the test bundle it was compiled into. +/// +/// `Bundle(for:)` needs a class from the bundle being located, and the helpers +/// in this directory compile into both `cmuxTests` and `cmuxCLITests`. Naming a +/// suite from either bundle would tie them to one of the two, so they name this +/// instead and resolve whichever bundle is running them. +final class CLITestBundleAnchor: NSObject {} diff --git a/cmuxTests/CLIWindowCommandMockServer.swift b/cmuxCLITestSupport/CLIWindowCommandMockServer.swift similarity index 96% rename from cmuxTests/CLIWindowCommandMockServer.swift rename to cmuxCLITestSupport/CLIWindowCommandMockServer.swift index 023a4642dc58..99a2d7e905bc 100644 --- a/cmuxTests/CLIWindowCommandMockServer.swift +++ b/cmuxCLITestSupport/CLIWindowCommandMockServer.swift @@ -154,6 +154,11 @@ final class CLIWindowCommandMockServer: @unchecked Sendable { } } guard clientFD >= 0 else { return } + // Configure before publishing the descriptor to the fixture's stop path. + guard ignoreSIGPIPE(onAcceptedFixtureSocket: clientFD) else { + Darwin.close(clientFD) + return + } lock.lock() guard !stopping else { lock.unlock() @@ -186,10 +191,7 @@ final class CLIWindowCommandMockServer: @unchecked Sendable { record(line) response = self.response(for: line) } - let responseLine = response + "\n" - _ = responseLine.withCString { pointer in - Darwin.write(clientFD, pointer, strlen(pointer)) - } + guard writeAllToFixtureSocket(response + "\n", fd: clientFD) else { return } } } } diff --git a/cmuxTests/CLIWorkspaceGroupSafetyMockServer.swift b/cmuxCLITestSupport/CLIWorkspaceGroupSafetyMockServer.swift similarity index 97% rename from cmuxTests/CLIWorkspaceGroupSafetyMockServer.swift rename to cmuxCLITestSupport/CLIWorkspaceGroupSafetyMockServer.swift index e7e98cb588dc..aaadbc9a5b0c 100644 --- a/cmuxTests/CLIWorkspaceGroupSafetyMockServer.swift +++ b/cmuxCLITestSupport/CLIWorkspaceGroupSafetyMockServer.swift @@ -60,6 +60,8 @@ struct CLIWorkspaceGroupSafetyMockServer: Sendable { } guard clientDescriptor >= 0 else { return nil } defer { Darwin.close(clientDescriptor) } + // A disconnected CLI must not terminate the host-free test runner. + guard ignoreSIGPIPE(onAcceptedFixtureSocket: clientDescriptor) else { return nil } var pending = Data() var buffer = [UInt8](repeating: 0, count: 4_096) diff --git a/cmuxTests/CLIWorkspaceStableIDMockServer.swift b/cmuxCLITestSupport/CLIWorkspaceStableIDMockServer.swift similarity index 98% rename from cmuxTests/CLIWorkspaceStableIDMockServer.swift rename to cmuxCLITestSupport/CLIWorkspaceStableIDMockServer.swift index 447fdf4a0f70..13b4347ea541 100644 --- a/cmuxTests/CLIWorkspaceStableIDMockServer.swift +++ b/cmuxCLITestSupport/CLIWorkspaceStableIDMockServer.swift @@ -72,6 +72,8 @@ struct CLIWorkspaceStableIDMockServer: Sendable { } guard clientDescriptor >= 0 else { return [] } defer { Darwin.close(clientDescriptor) } + // A disconnected CLI must not terminate the host-free test runner. + guard ignoreSIGPIPE(onAcceptedFixtureSocket: clientDescriptor) else { return [] } var requests: [String] = [] var pending = Data() diff --git a/cmuxTests/ClaudeHookLiveDeliverySocketState.swift b/cmuxCLITestSupport/ClaudeHookLiveDeliverySocketState.swift similarity index 100% rename from cmuxTests/ClaudeHookLiveDeliverySocketState.swift rename to cmuxCLITestSupport/ClaudeHookLiveDeliverySocketState.swift diff --git a/cmuxTests/ClaudeHookLiveDeliveryTargetTestSupport.swift b/cmuxCLITestSupport/ClaudeHookLiveDeliveryTargetTestSupport.swift similarity index 97% rename from cmuxTests/ClaudeHookLiveDeliveryTargetTestSupport.swift rename to cmuxCLITestSupport/ClaudeHookLiveDeliveryTargetTestSupport.swift index fcc374d55a1c..f703ea767c37 100644 --- a/cmuxTests/ClaudeHookLiveDeliveryTargetTestSupport.swift +++ b/cmuxCLITestSupport/ClaudeHookLiveDeliveryTargetTestSupport.swift @@ -49,7 +49,7 @@ enum ClaudeHookLiveDeliveryHarness { .appendingPathComponent("cli-\(name.prefix(6))-\(shortID).sock") .path return Context( - cliPath: try BundledCLITestSupport.bundledCLIPath(for: BundledCLILinkageTests.self), + cliPath: try BundledCLITestSupport.bundledCLIPath(for: CLITestBundleAnchor.self), socketPath: socketPath, listenerFD: try bindUnixSocket(at: socketPath), state: ServerState(), @@ -326,13 +326,10 @@ enum ClaudeHookLiveDeliveryHarness { Darwin.close(clientFD) handled.signal() } - - func writeResponse(_ response: String) { - let line = response + "\n" - _ = line.withCString { ptr in - Darwin.write(clientFD, ptr, strlen(ptr)) - } - } + // Hook clients may disconnect before their response arrives. + // Protect the host-free test runner without changing signals + // for any other socket or subprocess. + guard ignoreSIGPIPE(onAcceptedFixtureSocket: clientFD) else { return } var pending = Data() var buffer = [UInt8](repeating: 0, count: 4096) @@ -350,7 +347,7 @@ enum ClaudeHookLiveDeliveryHarness { pending.removeSubrange(0...newlineRange.lowerBound) guard let line = String(data: lineData, encoding: .utf8) else { continue } state.append(line) - writeResponse(handler(line)) + guard writeAllToFixtureSocket(handler(line) + "\n", fd: clientFD) else { return } } } } diff --git a/cmuxTests/CodexHookCapturedSocketCommands.swift b/cmuxCLITestSupport/CodexHookCapturedSocketCommands.swift similarity index 100% rename from cmuxTests/CodexHookCapturedSocketCommands.swift rename to cmuxCLITestSupport/CodexHookCapturedSocketCommands.swift diff --git a/cmuxTests/CodexTeamsAppServerFixture.swift b/cmuxCLITestSupport/CodexTeamsAppServerFixture.swift similarity index 98% rename from cmuxTests/CodexTeamsAppServerFixture.swift rename to cmuxCLITestSupport/CodexTeamsAppServerFixture.swift index a7d9edec56dc..3dfad0af64c8 100644 --- a/cmuxTests/CodexTeamsAppServerFixture.swift +++ b/cmuxCLITestSupport/CodexTeamsAppServerFixture.swift @@ -104,6 +104,11 @@ final class CodexTeamsAppServerFixture: @unchecked Sendable { private func acceptAndServe() { let accepted = Darwin.accept(listenerFD, nil, nil) guard accepted >= 0 else { return } + // Configure before publishing the descriptor to the fixture's stop path. + guard ignoreSIGPIPE(onAcceptedFixtureSocket: accepted) else { + Darwin.close(accepted) + return + } stateLock.lock() guard !stopped else { stateLock.unlock() diff --git a/cmuxTests/CodexTeamsSocketFixture.swift b/cmuxCLITestSupport/CodexTeamsSocketFixture.swift similarity index 96% rename from cmuxTests/CodexTeamsSocketFixture.swift rename to cmuxCLITestSupport/CodexTeamsSocketFixture.swift index f4a331551c95..33aa6946d2c5 100644 --- a/cmuxTests/CodexTeamsSocketFixture.swift +++ b/cmuxCLITestSupport/CodexTeamsSocketFixture.swift @@ -95,6 +95,11 @@ final class CodexTeamsSocketFixture: @unchecked Sendable { private func acceptAndServe() { let accepted = Darwin.accept(listenerFD, nil, nil) guard accepted >= 0 else { return } + // Configure before publishing the descriptor to the fixture's stop path. + guard ignoreSIGPIPE(onAcceptedFixtureSocket: accepted) else { + Darwin.close(accepted) + return + } stateLock.lock() guard !stopped else { stateLock.unlock() diff --git a/cmuxTests/ProcessExitWait.swift b/cmuxCLITestSupport/ProcessExitWait.swift similarity index 100% rename from cmuxTests/ProcessExitWait.swift rename to cmuxCLITestSupport/ProcessExitWait.swift diff --git a/cmuxCLITests/BundledCLITestSupport.swift b/cmuxCLITests/BundledCLITestSupport.swift new file mode 100644 index 000000000000..2aebcc02f600 --- /dev/null +++ b/cmuxCLITests/BundledCLITestSupport.swift @@ -0,0 +1,93 @@ +import Foundation +import XCTest + +/// Resolves the `cmux` binary these tests spawn. +/// +/// `cmuxTests` resolves it relative to the app host that loads it. `cmuxCLITests` +/// has no app host, so it resolves it from `CMUX_CLI_PATH` or relative to the +/// built products directory the bundle itself was copied into. +enum BundledCLITestSupport { + /// The daemon handshake must match the CLI under test, not the xctest host. + static func appVersion(cliPath: String) throws -> String { + let result = CLIHookProcessRunner.run( + executablePath: cliPath, + arguments: ["--version"], + environment: [:], + timeout: 10 + ) + let fields = result.stdout.split(whereSeparator: { $0.isWhitespace }) + guard !result.timedOut, result.status == 0, + fields.count >= 2, fields[0] == "cmux", + fields[1].first?.isNumber == true else { + throw NSError(domain: "cmux.tests", code: 3, userInfo: [ + NSLocalizedDescriptionKey: "Cannot read CLI version: \(result.stdout) \(result.stderr)", + ]) + } + return String(fields[1]) + } + + /// Path to the bundled `cmux` CLI under test. + /// + /// Resolution order: + /// 1. `CMUX_CLI_PATH` — CI and local runs point this at the binary they + /// built, which is what makes this bundle independent of an app host. + /// 2. `/cmux` — the `cmux-cli` target's own product, which sits + /// beside this .xctest bundle in `Build/Products/`. + /// 3. Any `*.app/Contents/Resources/bin/cmux` below the products + /// directory — the copy the app bundle ships. + static func bundledCLIPath( + for bundleClass: AnyClass = CLITestBundleAnchor.self, + file: StaticString = #filePath, + line: UInt = #line + ) throws -> String { + try bundledCLIURL(for: bundleClass, file: file, line: line).path + } + + static func bundledCLIURL( + for bundleClass: AnyClass = CLITestBundleAnchor.self, + file: StaticString = #filePath, + line: UInt = #line + ) throws -> URL { + let fileManager = FileManager.default + + if let override = ProcessInfo.processInfo.environment["CMUX_CLI_PATH"], !override.isEmpty { + let overrideURL = URL(fileURLWithPath: override) + if fileManager.isExecutableFile(atPath: overrideURL.path) { + return overrideURL + } + let message = "CMUX_CLI_PATH is set but not executable: \(override)" + XCTFail(message, file: file, line: line) + throw NSError(domain: "cmux.tests", code: 2, userInfo: [ + NSLocalizedDescriptionKey: message, + ]) + } + + let productsURL = Bundle(for: bundleClass).bundleURL.deletingLastPathComponent() + + let siblingCLI = productsURL.appendingPathComponent("cmux", isDirectory: false) + if fileManager.isExecutableFile(atPath: siblingCLI.path) { + return siblingCLI + } + + let enumerator = fileManager.enumerator( + at: productsURL, + includingPropertiesForKeys: nil, + options: [.skipsHiddenFiles] + ) + while let item = enumerator?.nextObject() as? URL { + guard item.lastPathComponent == "cmux", + item.path.contains(".app/Contents/Resources/bin/cmux"), + fileManager.isExecutableFile(atPath: item.path) else { continue } + return item + } + + let message = """ + cmux CLI not found. Set CMUX_CLI_PATH, or build the cmux-cli product \ + into \(productsURL.path). + """ + XCTFail(message, file: file, line: line) + throw NSError(domain: "cmux.tests", code: 1, userInfo: [ + NSLocalizedDescriptionKey: message, + ]) + } +} diff --git a/cmuxTests/CLIClaudeHookTimeoutRegressionTests.swift b/cmuxCLITests/CLIClaudeHookTimeoutRegressionTests.swift similarity index 98% rename from cmuxTests/CLIClaudeHookTimeoutRegressionTests.swift rename to cmuxCLITests/CLIClaudeHookTimeoutRegressionTests.swift index 0eedbaf9e492..fad4ed83b614 100644 --- a/cmuxTests/CLIClaudeHookTimeoutRegressionTests.swift +++ b/cmuxCLITests/CLIClaudeHookTimeoutRegressionTests.swift @@ -4,12 +4,6 @@ import CryptoKit import Testing import CMUXAgentLaunch import CmuxFoundation -#if canImport(cmux_DEV) -@testable import cmux_DEV -#elseif canImport(cmux) -@testable import cmux -#endif - @Suite(.serialized) struct CLIClaudeHookTimeoutRegressionTests { @Test("Queued lifecycle routes trust an admitted surface snapshot") @@ -109,7 +103,7 @@ struct CLIClaudeHookTimeoutRegressionTests { @Test("Claude non-decision hooks use bounded ordered admission") func generatedSettingsUseQueuedAdmissionAndPreserveDecisionHooks() throws { let fileManager = FileManager.default - let cliPath = try BundledCLITestSupport.bundledCLIPath(for: BundledCLILinkageTests.self) + let cliPath = try BundledCLITestSupport.bundledCLIPath(for: CLITestBundleAnchor.self) let wrapper = repositoryRoot.appendingPathComponent("Resources/bin/cmux-claude-wrapper") let root = fileManager.temporaryDirectory.appendingPathComponent( "cmux-claude-queued-hooks-\(UUID().uuidString)", @@ -244,7 +238,7 @@ struct CLIClaudeHookTimeoutRegressionTests { @Test("Local queue admission snapshots the authoritative process route") func localQueueAdmissionSnapshotsAuthoritativeProcessRoute() throws { let cliPath = try BundledCLITestSupport.bundledCLIPath( - for: BundledCLILinkageTests.self + for: CLITestBundleAnchor.self ) let socketPath = makeCodexHookSocketPath("local-route") let listenerFD = try bindCodexHookUnixSocket(at: socketPath) @@ -310,7 +304,7 @@ struct CLIClaudeHookTimeoutRegressionTests { @Test("Local queue admission retains its lane when route probing fails") func localQueueAdmissionFallsBackToLivePIDWhenRouteProbingFails() throws { let cliPath = try BundledCLITestSupport.bundledCLIPath( - for: BundledCLILinkageTests.self + for: CLITestBundleAnchor.self ) let socketPath = makeCodexHookSocketPath("local-route-fallback") let listenerFD = try bindCodexHookUnixSocket(at: socketPath) @@ -363,7 +357,7 @@ struct CLIClaudeHookTimeoutRegressionTests { @Test("Local queue admission preserves an existing route snapshot when reprobe fails") func localQueueAdmissionPreservesRouteSnapshotWhenReprobeFails() throws { let cliPath = try BundledCLITestSupport.bundledCLIPath( - for: BundledCLILinkageTests.self + for: CLITestBundleAnchor.self ) let socketPath = makeCodexHookSocketPath("local-route-snapshot-fallback") let listenerFD = try bindCodexHookUnixSocket(at: socketPath) @@ -429,7 +423,7 @@ struct CLIClaudeHookTimeoutRegressionTests { subcommand: String ) throws { let cliPath = try BundledCLITestSupport.bundledCLIPath( - for: BundledCLILinkageTests.self + for: CLITestBundleAnchor.self ) let root = FileManager.default.temporaryDirectory.appendingPathComponent( "cmux-queued-route-replay-\(UUID().uuidString)", @@ -531,7 +525,7 @@ struct CLIClaudeHookTimeoutRegressionTests { @Test("Claude prompt hook fails open before its declared timeout") func promptAdmissionHasAShortInternalDeadline() throws { - let cliPath = try BundledCLITestSupport.bundledCLIPath(for: BundledCLILinkageTests.self) + let cliPath = try BundledCLITestSupport.bundledCLIPath(for: CLITestBundleAnchor.self) let settingsResult = runCodexHookProcess( executablePath: cliPath, arguments: ["hooks", "claude", "inject-settings"], @@ -582,7 +576,7 @@ struct CLIClaudeHookTimeoutRegressionTests { @Test("Queue admission compacts oversized telemetry without losing identity") func queueAdmissionCompactsOversizedTelemetry() throws { - let cliPath = try BundledCLITestSupport.bundledCLIPath(for: BundledCLILinkageTests.self) + let cliPath = try BundledCLITestSupport.bundledCLIPath(for: CLITestBundleAnchor.self) let socketPath = makeCodexHookSocketPath("large-queue") let listenerFD = try bindCodexHookUnixSocket(at: socketPath) defer { @@ -637,7 +631,7 @@ struct CLIClaudeHookTimeoutRegressionTests { @Test("Queue admission fails open when stdin exceeds the finite ingress budget") func queueAdmissionRejectsPayloadBeyondIngressBudget() throws { - let cliPath = try BundledCLITestSupport.bundledCLIPath(for: BundledCLILinkageTests.self) + let cliPath = try BundledCLITestSupport.bundledCLIPath(for: CLITestBundleAnchor.self) let socketPath = makeCodexHookSocketPath("ingress-budget") let listenerFD = try bindCodexHookUnixSocket(at: socketPath) defer { @@ -689,7 +683,7 @@ struct CLIClaudeHookTimeoutRegressionTests { @Test("Queue compaction preserves behavior-critical needs-input fields") func queueCompactionPreservesNeedsInputClassification() throws { - let cliPath = try BundledCLITestSupport.bundledCLIPath(for: BundledCLILinkageTests.self) + let cliPath = try BundledCLITestSupport.bundledCLIPath(for: CLITestBundleAnchor.self) let socketPath = makeCodexHookSocketPath("large-needs-input") let listenerFD = try bindCodexHookUnixSocket(at: socketPath) defer { @@ -792,7 +786,7 @@ struct CLIClaudeHookTimeoutRegressionTests { flag: String, value: Bool ) throws { - let cliPath = try BundledCLITestSupport.bundledCLIPath(for: BundledCLILinkageTests.self) + let cliPath = try BundledCLITestSupport.bundledCLIPath(for: CLITestBundleAnchor.self) let socketPath = makeCodexHookSocketPath("\(agent)-lifecycle-flags") let listenerFD = try bindCodexHookUnixSocket(at: socketPath) defer { @@ -861,7 +855,7 @@ struct CLIClaudeHookTimeoutRegressionTests { @Test("Queue compaction preserves Claude background-work evidence") func queueCompactionPreservesClaudeBackgroundWorkEvidence() throws { - let cliPath = try BundledCLITestSupport.bundledCLIPath(for: BundledCLILinkageTests.self) + let cliPath = try BundledCLITestSupport.bundledCLIPath(for: CLITestBundleAnchor.self) let socketPath = makeCodexHookSocketPath("claude-background-work") let listenerFD = try bindCodexHookUnixSocket(at: socketPath) defer { @@ -929,7 +923,7 @@ struct CLIClaudeHookTimeoutRegressionTests { arguments: ["grok", "antigravity"] ) func pinnedAgentQueueAdmissionUsesExplicitSocket(agent: String) throws { - let cliPath = try BundledCLITestSupport.bundledCLIPath(for: BundledCLILinkageTests.self) + let cliPath = try BundledCLITestSupport.bundledCLIPath(for: CLITestBundleAnchor.self) let socketPath = makeCodexHookSocketPath("\(agent)-pinned") let listenerFD = try bindCodexHookUnixSocket(at: socketPath) defer { @@ -983,7 +977,7 @@ struct CLIClaudeHookTimeoutRegressionTests { arguments: [("claude", "CMUX_CLAUDE_PID"), ("codex", "CMUX_CODEX_PID")] ) func relayOriginSkipsLocalProcessRouting(agent: String, pidKey: String) throws { - let cliPath = try BundledCLITestSupport.bundledCLIPath(for: BundledCLILinkageTests.self) + let cliPath = try BundledCLITestSupport.bundledCLIPath(for: CLITestBundleAnchor.self) let root = FileManager.default.temporaryDirectory.appendingPathComponent( "cmux-relay-hook-routing-\(agent)-\(UUID().uuidString)", isDirectory: true @@ -1059,7 +1053,7 @@ struct CLIClaudeHookTimeoutRegressionTests { @Test("Relay feed fallbacks omit synthetic process identity") func relayFeedFallbackOmitsSyntheticProcessIdentity() throws { - let cliPath = try BundledCLITestSupport.bundledCLIPath(for: BundledCLILinkageTests.self) + let cliPath = try BundledCLITestSupport.bundledCLIPath(for: CLITestBundleAnchor.self) let root = FileManager.default.temporaryDirectory.appendingPathComponent( "cmux-relay-feed-fallback-\(UUID().uuidString)", isDirectory: true @@ -1125,7 +1119,7 @@ struct CLIClaudeHookTimeoutRegressionTests { @Test("Relay-origin Codex stop ignores local transcript path collisions") func relayOriginCodexStopIgnoresLocalTranscriptPathCollisions() throws { - let cliPath = try BundledCLITestSupport.bundledCLIPath(for: BundledCLILinkageTests.self) + let cliPath = try BundledCLITestSupport.bundledCLIPath(for: CLITestBundleAnchor.self) let root = FileManager.default.temporaryDirectory.appendingPathComponent( "cmux-relay-codex-stop-\(UUID().uuidString)", isDirectory: true @@ -1181,7 +1175,7 @@ struct CLIClaudeHookTimeoutRegressionTests { @Test("Actionable Feed decisions establish their delivery lane barrier before push") func actionableFeedDecisionWaitsForEarlierQueuedLifecycleWork() throws { - let cliPath = try BundledCLITestSupport.bundledCLIPath(for: BundledCLILinkageTests.self) + let cliPath = try BundledCLITestSupport.bundledCLIPath(for: CLITestBundleAnchor.self) let root = FileManager.default.temporaryDirectory.appendingPathComponent( "cmux-feed-decision-barrier-\(UUID().uuidString)", isDirectory: true @@ -1267,7 +1261,7 @@ struct CLIClaudeHookTimeoutRegressionTests { @Test("Direct Codex permission status waits for its queued lifecycle lane") func codexPermissionStatusWaitsForEarlierQueuedLifecycleWork() throws { - let cliPath = try BundledCLITestSupport.bundledCLIPath(for: BundledCLILinkageTests.self) + let cliPath = try BundledCLITestSupport.bundledCLIPath(for: CLITestBundleAnchor.self) let root = FileManager.default.temporaryDirectory.appendingPathComponent( "cmux-codex-decision-barrier-\(UUID().uuidString)", isDirectory: true @@ -1332,7 +1326,7 @@ struct CLIClaudeHookTimeoutRegressionTests { let artifact: URL } - let cliPath = try BundledCLITestSupport.bundledCLIPath(for: BundledCLILinkageTests.self) + let cliPath = try BundledCLITestSupport.bundledCLIPath(for: CLITestBundleAnchor.self) let root = FileManager.default.temporaryDirectory.appendingPathComponent( "cmux-custom-agent-queue-\(UUID().uuidString)", isDirectory: true diff --git a/cmuxTests/CLICoderouterBootstrapTests.swift b/cmuxCLITests/CLICoderouterBootstrapTests.swift similarity index 99% rename from cmuxTests/CLICoderouterBootstrapTests.swift rename to cmuxCLITests/CLICoderouterBootstrapTests.swift index 018c36ccb9d3..198d63dfcf37 100644 --- a/cmuxTests/CLICoderouterBootstrapTests.swift +++ b/cmuxCLITests/CLICoderouterBootstrapTests.swift @@ -212,7 +212,7 @@ struct CLICoderouterBootstrapTests { let socketPath: String init() throws { - cliPath = try BundledCLITestSupport.bundledCLIPath(for: BundledCLILinkageTests.self) + cliPath = try BundledCLITestSupport.bundledCLIPath(for: CLITestBundleAnchor.self) root = fileManager.temporaryDirectory .appendingPathComponent("cmux-coderouter-bootstrap-\(UUID().uuidString)", isDirectory: true) home = root.appendingPathComponent("home", isDirectory: true) diff --git a/cmuxTests/CLICodexHookPathQuotingRegressionTests.swift b/cmuxCLITests/CLICodexHookPathQuotingRegressionTests.swift similarity index 98% rename from cmuxTests/CLICodexHookPathQuotingRegressionTests.swift rename to cmuxCLITests/CLICodexHookPathQuotingRegressionTests.swift index 75576cbd721b..54d3ab7c24f4 100644 --- a/cmuxTests/CLICodexHookPathQuotingRegressionTests.swift +++ b/cmuxCLITests/CLICodexHookPathQuotingRegressionTests.swift @@ -125,7 +125,7 @@ struct CLICodexHookPathQuotingRegressionTests { } private func bundledCLIPath() throws -> String { - try BundledCLITestSupport.bundledCLIPath(for: BundledCLILinkageTests.self) + try BundledCLITestSupport.bundledCLIPath(for: CLITestBundleAnchor.self) } private func injectedCodexHookCommand(eventName: String, output: String) throws -> String { diff --git a/cmuxTests/CLICodexHookTimeoutRegressionTests.swift b/cmuxCLITests/CLICodexHookTimeoutRegressionTests.swift similarity index 99% rename from cmuxTests/CLICodexHookTimeoutRegressionTests.swift rename to cmuxCLITests/CLICodexHookTimeoutRegressionTests.swift index 0b572b3f9f2d..b43889385dc2 100644 --- a/cmuxTests/CLICodexHookTimeoutRegressionTests.swift +++ b/cmuxCLITests/CLICodexHookTimeoutRegressionTests.swift @@ -1141,7 +1141,7 @@ struct CLICodexHookTimeoutRegressionTests { } private func bundledCLIPath() throws -> String { - try BundledCLITestSupport.bundledCLIPath(for: BundledCLILinkageTests.self) + try BundledCLITestSupport.bundledCLIPath(for: CLITestBundleAnchor.self) } private func injectedCodexHookEventNames(_ output: String) -> [String] { diff --git a/cmuxTests/CLICodexQueuedHookContractTests.swift b/cmuxCLITests/CLICodexQueuedHookContractTests.swift similarity index 99% rename from cmuxTests/CLICodexQueuedHookContractTests.swift rename to cmuxCLITests/CLICodexQueuedHookContractTests.swift index 985efbbc1002..e8694629b8d4 100644 --- a/cmuxTests/CLICodexQueuedHookContractTests.swift +++ b/cmuxCLITests/CLICodexQueuedHookContractTests.swift @@ -5,7 +5,7 @@ import Testing struct CLICodexQueuedHookContractTests { @Test("Codex queues non-decision hooks but keeps decision hooks direct") func wrapperInjectionPreservesDecisionSemantics() throws { - let cliPath = try BundledCLITestSupport.bundledCLIPath(for: BundledCLILinkageTests.self) + let cliPath = try BundledCLITestSupport.bundledCLIPath(for: CLITestBundleAnchor.self) let root = FileManager.default.temporaryDirectory.appendingPathComponent( "cmux-codex-queued-contract-\(UUID().uuidString)", isDirectory: true @@ -55,7 +55,7 @@ struct CLICodexQueuedHookContractTests { @Test("Codex generated decision hooks propagate CLI failure while lifecycle hooks fail open") func generatedScriptsPreserveFailureSemantics() throws { - let cliPath = try BundledCLITestSupport.bundledCLIPath(for: BundledCLILinkageTests.self) + let cliPath = try BundledCLITestSupport.bundledCLIPath(for: CLITestBundleAnchor.self) let root = FileManager.default.temporaryDirectory.appendingPathComponent( "cmux-codex-generated-hook-failure-\(UUID().uuidString)", isDirectory: true diff --git a/cmuxTests/CLICodexResumeNotificationTests.swift b/cmuxCLITests/CLICodexResumeNotificationTests.swift similarity index 100% rename from cmuxTests/CLICodexResumeNotificationTests.swift rename to cmuxCLITests/CLICodexResumeNotificationTests.swift diff --git a/cmuxTests/CLIExplicitSurfaceRoutingTests.swift b/cmuxCLITests/CLIExplicitSurfaceRoutingTests.swift similarity index 99% rename from cmuxTests/CLIExplicitSurfaceRoutingTests.swift rename to cmuxCLITests/CLIExplicitSurfaceRoutingTests.swift index 8c4fa3d494c1..dda1fbd188f1 100644 --- a/cmuxTests/CLIExplicitSurfaceRoutingTests.swift +++ b/cmuxCLITests/CLIExplicitSurfaceRoutingTests.swift @@ -820,6 +820,10 @@ struct CLIExplicitSurfaceRoutingTests { return } defer { Darwin.close(clientFD) } + guard ignoreSIGPIPE(onAcceptedFixtureSocket: clientFD) else { + state.recordError("mock socket server could not set SO_NOSIGPIPE, errno \(errno)") + return + } var pending = Data() var buffer = [UInt8](repeating: 0, count: 4096) @@ -838,10 +842,7 @@ struct CLIExplicitSurfaceRoutingTests { pending.removeSubrange(0...newlineRange.lowerBound) guard let line = String(data: lineData, encoding: .utf8) else { continue } state.record(line) - let response = handler(line) + "\n" - _ = response.withCString { pointer in - Darwin.write(clientFD, pointer, strlen(pointer)) - } + guard writeAllToFixtureSocket(handler(line) + "\n", fd: clientFD) else { return } } } } diff --git a/cmuxTests/CLIHookNoResponseTests.swift b/cmuxCLITests/CLIHookNoResponseTests.swift similarity index 95% rename from cmuxTests/CLIHookNoResponseTests.swift rename to cmuxCLITests/CLIHookNoResponseTests.swift index de2261dd8f0d..bb28c0ab326a 100644 --- a/cmuxTests/CLIHookNoResponseTests.swift +++ b/cmuxCLITests/CLIHookNoResponseTests.swift @@ -4,7 +4,6 @@ import Testing @Suite("CLI hook no-response telemetry", .serialized) struct CLIHookNoResponseTests { - final class BundleProbe {} struct ProcessRunResult { let status: Int32 @@ -253,29 +252,7 @@ struct CLIHookNoResponseTests { } private static func bundledCLIPath() throws -> String { - let fileManager = FileManager.default - let appBundleURL = Bundle(for: BundleProbe.self) - .bundleURL - .deletingLastPathComponent() - .deletingLastPathComponent() - .deletingLastPathComponent() - let enumerator = fileManager.enumerator( - at: appBundleURL, - includingPropertiesForKeys: nil, - options: [.skipsHiddenFiles] - ) - - while let item = enumerator?.nextObject() as? URL { - guard item.lastPathComponent == "cmux", - item.path.contains(".app/Contents/Resources/bin/cmux") else { - continue - } - return item.path - } - - throw NSError(domain: "cmux.tests", code: 1, userInfo: [ - NSLocalizedDescriptionKey: "Bundled cmux CLI not found in \(appBundleURL.path)", - ]) + try BundledCLITestSupport.bundledCLIPath() } private static func makeSocketPath(_ name: String) -> String { @@ -355,6 +332,10 @@ struct CLIHookNoResponseTests { return } defer { Darwin.close(clientFD) } + guard ignoreSIGPIPE(onAcceptedFixtureSocket: clientFD) else { + fulfillOnce() + return + } readLines(from: clientFD) { line in state.append(line) @@ -406,6 +387,10 @@ struct CLIHookNoResponseTests { return } accepted += 1 + guard ignoreSIGPIPE(onAcceptedFixtureSocket: clientFD) else { + Darwin.close(clientFD) + continue + } DispatchQueue.global(qos: .userInitiated).async { defer { Darwin.close(clientFD) } @@ -480,10 +465,7 @@ struct CLIHookNoResponseTests { } private static func writeLine(_ line: String, to fd: Int32) { - let response = line + "\n" - _ = response.withCString { ptr in - Darwin.write(fd, ptr, strlen(ptr)) - } + writeAllToFixtureSocket(line + "\n", fd: fd) } private static func v2Response( diff --git a/cmuxTests/CLIOmpSupersededCleanupTests.swift b/cmuxCLITests/CLIOmpSupersededCleanupTests.swift similarity index 99% rename from cmuxTests/CLIOmpSupersededCleanupTests.swift rename to cmuxCLITests/CLIOmpSupersededCleanupTests.swift index 7248f0950313..8445f153dd22 100644 --- a/cmuxTests/CLIOmpSupersededCleanupTests.swift +++ b/cmuxCLITests/CLIOmpSupersededCleanupTests.swift @@ -2,12 +2,6 @@ import Darwin import Foundation import Testing -#if canImport(cmux_DEV) -@testable import cmux_DEV -#elseif canImport(cmux) -@testable import cmux -#endif - @Suite(.serialized) struct CLIOmpSupersededCleanupTests { private typealias Harness = ClaudeHookLiveDeliveryHarness diff --git a/cmuxTests/CLIRelayQueuedHookRegressionTests.swift b/cmuxCLITests/CLIRelayQueuedHookRegressionTests.swift similarity index 98% rename from cmuxTests/CLIRelayQueuedHookRegressionTests.swift rename to cmuxCLITests/CLIRelayQueuedHookRegressionTests.swift index 01a1edcfac1e..49bd0828b13f 100644 --- a/cmuxTests/CLIRelayQueuedHookRegressionTests.swift +++ b/cmuxCLITests/CLIRelayQueuedHookRegressionTests.swift @@ -11,7 +11,7 @@ struct CLIRelayQueuedHookRegressionTests { @Test("Relay admission carries portable TTY evidence in one RPC") func relayAdmissionCarriesPortableTTYInOneRPC() throws { - let cliPath = try BundledCLITestSupport.bundledCLIPath(for: BundledCLILinkageTests.self) + let cliPath = try BundledCLITestSupport.bundledCLIPath(for: CLITestBundleAnchor.self) let relay = try RelayQueuedHookMockServer( ttyName: "8535", workspaceID: remoteWorkspaceID, @@ -48,7 +48,7 @@ struct CLIRelayQueuedHookRegressionTests { @Test("Relay compaction preserves Claude background-work evidence") func relayCompactionPreservesClaudeBackgroundWorkEvidence() throws { - let cliPath = try BundledCLITestSupport.bundledCLIPath(for: BundledCLILinkageTests.self) + let cliPath = try BundledCLITestSupport.bundledCLIPath(for: CLITestBundleAnchor.self) let relay = try RelayQueuedHookMockServer( ttyName: "8540", workspaceID: remoteWorkspaceID, @@ -111,7 +111,7 @@ struct CLIRelayQueuedHookRegressionTests { @Test("Relay admission carries transcript-only Codex failures into local replay") func relayAdmissionPreservesCodexFailureEvidence() throws { - let cliPath = try BundledCLITestSupport.bundledCLIPath(for: BundledCLILinkageTests.self) + let cliPath = try BundledCLITestSupport.bundledCLIPath(for: CLITestBundleAnchor.self) let root = FileManager.default.temporaryDirectory.appendingPathComponent( "cmux-relay-codex-evidence-\(UUID().uuidString)", isDirectory: true @@ -179,7 +179,7 @@ struct CLIRelayQueuedHookRegressionTests { @Test("Relay admission resolves the Rovo session on the remote filesystem") func relayAdmissionPreservesRovoSessionIdentity() throws { - let cliPath = try BundledCLITestSupport.bundledCLIPath(for: BundledCLILinkageTests.self) + let cliPath = try BundledCLITestSupport.bundledCLIPath(for: CLITestBundleAnchor.self) let root = FileManager.default.temporaryDirectory.appendingPathComponent( "cmux-relay-rovo-session-\(UUID().uuidString)", isDirectory: true @@ -253,7 +253,7 @@ struct CLIRelayQueuedHookRegressionTests { @Test("Relay replay strips remote filesystem identity and omits a synthetic local agent PID") func relayReplayUsesOnlyPortableFeedIdentity() throws { - let cliPath = try BundledCLITestSupport.bundledCLIPath(for: BundledCLILinkageTests.self) + let cliPath = try BundledCLITestSupport.bundledCLIPath(for: CLITestBundleAnchor.self) let root = FileManager.default.temporaryDirectory.appendingPathComponent( "cmux-relay-portable-feed-\(UUID().uuidString)", isDirectory: true @@ -329,7 +329,7 @@ struct CLIRelayQueuedHookRegressionTests { @Test("Relay replay preserves a Claude fork's parent through start and early end") func relayReplayPreservesClaudeForkParentRecord() throws { - let cliPath = try BundledCLITestSupport.bundledCLIPath(for: BundledCLILinkageTests.self) + let cliPath = try BundledCLITestSupport.bundledCLIPath(for: CLITestBundleAnchor.self) let root = FileManager.default.temporaryDirectory.appendingPathComponent( "cmux-relay-claude-fork-\(UUID().uuidString)", isDirectory: true @@ -458,7 +458,7 @@ struct CLIRelayQueuedHookRegressionTests { @Test("Relay replay creates its isolated hook state directory before locking") func relayReplayCreatesFreshHookStateDirectory() throws { - let cliPath = try BundledCLITestSupport.bundledCLIPath(for: BundledCLILinkageTests.self) + let cliPath = try BundledCLITestSupport.bundledCLIPath(for: CLITestBundleAnchor.self) let root = FileManager.default.temporaryDirectory.appendingPathComponent( "cmux-relay-fresh-hook-state-\(UUID().uuidString)", isDirectory: true @@ -751,6 +751,10 @@ private final class RelayQueuedHookMockServer: @unchecked Sendable { while true { let clientFD = Darwin.accept(listenerFD, nil, nil) guard clientFD >= 0 else { return } + guard ignoreSIGPIPE(onAcceptedFixtureSocket: clientFD) else { + Darwin.close(clientFD) + continue + } handle(clientFD: clientFD) } } diff --git a/cmuxTests/CLISSHPTYResizeInputTests.swift b/cmuxCLITests/CLISSHPTYResizeInputTests.swift similarity index 97% rename from cmuxTests/CLISSHPTYResizeInputTests.swift rename to cmuxCLITests/CLISSHPTYResizeInputTests.swift index 17e4ca1b39c6..3e97ea8f4ea9 100644 --- a/cmuxTests/CLISSHPTYResizeInputTests.swift +++ b/cmuxCLITests/CLISSHPTYResizeInputTests.swift @@ -7,6 +7,7 @@ struct CLISSHPTYResizeInputTests { @Test func attachReportsPTYSizeChangeBeforeForwardingInput() throws { let cliPath = try bundledCLIPath() + let cliVersion = try BundledCLITestSupport.appVersion(cliPath: cliPath) let socketPath = makeSocketPath("sshptyinputresize") var listenerFD = try bindUnixSocket(at: socketPath) let bridge = try bindLoopbackTCP() @@ -51,7 +52,7 @@ struct CLISSHPTYResizeInputTests { id: id, ok: true, result: [ - "host": "127.0.0.1", "daemon_version": BundledCLITestSupport.appVersion, + "host": "127.0.0.1", "daemon_version": cliVersion, "port": bridge.port, "token": token, "session_id": sessionId, @@ -386,6 +387,10 @@ struct CLISSHPTYResizeInputTests { } } if clientFD >= 0 { + guard ignoreSIGPIPE(onAcceptedFixtureSocket: clientFD) else { + Darwin.close(clientFD) + continue + } // Darwin inherits O_NONBLOCK; the line reader needs blocking reads. let clientFlags = fcntl(clientFD, F_GETFL, 0) _ = fcntl(clientFD, F_SETFL, clientFlags & ~O_NONBLOCK) @@ -458,6 +463,7 @@ struct CLISSHPTYResizeInputTests { } guard clientFD >= 0 else { return } defer { Darwin.close(clientFD) } + guard ignoreSIGPIPE(onAcceptedFixtureSocket: clientFD) else { return } var pending = Data() var buffer = [UInt8](repeating: 0, count: 1024) @@ -472,9 +478,7 @@ struct CLISSHPTYResizeInputTests { } let ready = #"{"type":"ready","attachment_token":"attach-token"}"# + "\n" - ready.withCString { ptr in - _ = Darwin.write(clientFD, ptr, strlen(ptr)) - } + guard writeAllToFixtureSocket(ready, fd: clientFD) else { return } bridgeReady.signal() while true { let count = Darwin.read(clientFD, &buffer, buffer.count) diff --git a/cmuxTests/CLIStdioSIGPIPERegressionTests.swift b/cmuxCLITests/CLIStdioSIGPIPERegressionTests.swift similarity index 83% rename from cmuxTests/CLIStdioSIGPIPERegressionTests.swift rename to cmuxCLITests/CLIStdioSIGPIPERegressionTests.swift index fd86c5e261c9..e4d7b3327737 100644 --- a/cmuxTests/CLIStdioSIGPIPERegressionTests.swift +++ b/cmuxCLITests/CLIStdioSIGPIPERegressionTests.swift @@ -2,6 +2,45 @@ import Darwin import XCTest final class CLIStdioSIGPIPERegressionTests: XCTestCase { + func testSharedRunnerDrainsOutputWhileWritingLargeInput() { + let result = CLIHookProcessRunner.run( + executablePath: "/bin/sh", + arguments: ["-c", "dd if=/dev/zero bs=65536 count=16 2>/dev/null; dd if=/dev/zero bs=65536 count=16 >&2 2>/dev/null; cat >/dev/null"], + environment: ["PATH": "/usr/bin:/bin"], + standardInput: String(repeating: "x", count: 1024 * 1024), + timeout: 5 + ) + XCTAssertFalse(result.timedOut) + XCTAssertEqual(result.status, 0, result.stderr) + XCTAssertEqual(result.stdout.utf8.count, 1024 * 1024) + XCTAssertEqual(result.stderr.utf8.count, 1024 * 1024) + } + + func testSharedRunnerTimeoutInterruptsBlockedInput() { + let started = Date() + let result = CLIHookProcessRunner.run( + executablePath: "/bin/sleep", + arguments: ["30"], + environment: [:], + standardInput: String(repeating: "x", count: 1024 * 1024), + timeout: 0.1 + ) + XCTAssertTrue(result.timedOut) + XCTAssertLessThan(Date().timeIntervalSince(started), 5) + } + + func testSharedRunnerToleratesChildClosingInput() { + let result = CLIHookProcessRunner.run( + executablePath: "/usr/bin/true", + arguments: [], + environment: [:], + standardInput: String(repeating: "x", count: 1024 * 1024), + timeout: 5 + ) + XCTAssertFalse(result.timedOut) + XCTAssertEqual(result.status, 0) + } + private struct ProcessRunResult { let status: Int32 let stdout: String diff --git a/cmuxTests/CLITmuxCompatStoreConcurrencyTests.swift b/cmuxCLITests/CLITmuxCompatStoreConcurrencyTests.swift similarity index 100% rename from cmuxTests/CLITmuxCompatStoreConcurrencyTests.swift rename to cmuxCLITests/CLITmuxCompatStoreConcurrencyTests.swift diff --git a/cmuxTests/CLIWindowHandleRoutingTests.swift b/cmuxCLITests/CLIWindowHandleRoutingTests.swift similarity index 100% rename from cmuxTests/CLIWindowHandleRoutingTests.swift rename to cmuxCLITests/CLIWindowHandleRoutingTests.swift diff --git a/cmuxTests/CLIWorkspaceGroupSafetyTests.swift b/cmuxCLITests/CLIWorkspaceGroupSafetyTests.swift similarity index 100% rename from cmuxTests/CLIWorkspaceGroupSafetyTests.swift rename to cmuxCLITests/CLIWorkspaceGroupSafetyTests.swift diff --git a/cmuxTests/CLIWorkspaceStableIDTests.swift b/cmuxCLITests/CLIWorkspaceStableIDTests.swift similarity index 99% rename from cmuxTests/CLIWorkspaceStableIDTests.swift rename to cmuxCLITests/CLIWorkspaceStableIDTests.swift index d63b8eab9b38..76c620420cb2 100644 --- a/cmuxTests/CLIWorkspaceStableIDTests.swift +++ b/cmuxCLITests/CLIWorkspaceStableIDTests.swift @@ -6,7 +6,7 @@ import Testing struct CLIWorkspaceStableIDTests { @Test("Default pane inspection JSON keeps IDs for agent targeting") func paneInspectionJSONKeepsIDsByDefault() async throws { - let cliPath = try BundledCLITestSupport.bundledCLIPath(for: BundledCLILinkageTests.self) + let cliPath = try BundledCLITestSupport.bundledCLIPath(for: CLITestBundleAnchor.self) let commands: [(command: [String], key: String, id: String, ref: String)] = [ ( ["list-panes", "--workspace", "workspace:1", "--json"], @@ -41,7 +41,7 @@ struct CLIWorkspaceStableIDTests { @Test("Explicit pane inspection ID formats remain respected") func explicitIDFormatsStillShapePaneInspectionJSON() async throws { - let cliPath = try BundledCLITestSupport.bundledCLIPath(for: BundledCLILinkageTests.self) + let cliPath = try BundledCLITestSupport.bundledCLIPath(for: CLITestBundleAnchor.self) let expectations: [(mode: String, hasID: Bool, hasRef: Bool)] = [ ("refs", false, true), ("uuids", true, false), @@ -86,7 +86,7 @@ struct CLIWorkspaceStableIDTests { @Test("Workspace inspection JSON keeps mirror UUIDs by default") func workspaceInspectionJSONKeepsMirrorUUIDsByDefault() async throws { - let cliPath = try BundledCLITestSupport.bundledCLIPath(for: BundledCLILinkageTests.self) + let cliPath = try BundledCLITestSupport.bundledCLIPath(for: CLITestBundleAnchor.self) let commands = [ ["list-workspaces", "--window", Self.windowID, "--json"], ["workspace", "list", "--window", Self.windowID, "--json"], @@ -114,7 +114,7 @@ struct CLIWorkspaceStableIDTests { @Test("Explicit ID formats still shape workspace inspection JSON") func explicitIDFormatsStillShapeWorkspaceInspectionJSON() async throws { - let cliPath = try BundledCLITestSupport.bundledCLIPath(for: BundledCLILinkageTests.self) + let cliPath = try BundledCLITestSupport.bundledCLIPath(for: CLITestBundleAnchor.self) let expectations: [(mode: String, hasID: Bool, hasRef: Bool)] = [ ("refs", false, true), ("uuids", true, false), diff --git a/cmuxTests/CMUXOpenHTMLFocusTests.swift b/cmuxCLITests/CMUXOpenHTMLFocusTests.swift similarity index 98% rename from cmuxTests/CMUXOpenHTMLFocusTests.swift rename to cmuxCLITests/CMUXOpenHTMLFocusTests.swift index 2480250fb4c8..efd098c0837f 100644 --- a/cmuxTests/CMUXOpenHTMLFocusTests.swift +++ b/cmuxCLITests/CMUXOpenHTMLFocusTests.swift @@ -234,6 +234,7 @@ final class CMUXOpenHTMLFocusTests { Darwin.close(clientFD) handled.signal() } + guard ignoreSIGPIPE(onAcceptedFixtureSocket: clientFD) else { return } var pending = Data() var buffer = [UInt8](repeating: 0, count: 4096) @@ -251,10 +252,7 @@ final class CMUXOpenHTMLFocusTests { pending.removeSubrange(0...newlineRange.lowerBound) guard let line = String(data: lineData, encoding: .utf8) else { continue } state.append(line) - let response = handler(line) + "\n" - _ = response.withCString { ptr in - Darwin.write(clientFD, ptr, strlen(ptr)) - } + guard writeAllToFixtureSocket(handler(line) + "\n", fd: clientFD) else { return } } } } diff --git a/cmuxTests/CampfireHookNotificationTests.swift b/cmuxCLITests/CampfireHookNotificationTests.swift similarity index 89% rename from cmuxTests/CampfireHookNotificationTests.swift rename to cmuxCLITests/CampfireHookNotificationTests.swift index 03e37f170fdb..4ab5f4d566a9 100644 --- a/cmuxTests/CampfireHookNotificationTests.swift +++ b/cmuxCLITests/CampfireHookNotificationTests.swift @@ -238,6 +238,9 @@ struct CampfireHookNotificationTests { clientFD: Int32 ) { defer { Darwin.close(clientFD) } + // A hook may exit before its response arrives. A closed client must + // not terminate the host-free test runner. + guard ignoreSIGPIPE(onAcceptedFixtureSocket: clientFD) else { return } var pending = Data() var buffer = [UInt8](repeating: 0, count: 4096) while true { @@ -254,9 +257,7 @@ struct CampfireHookNotificationTests { guard let line = String(data: lineData, encoding: .utf8) else { continue } context.state.append(line) let response = agentHookMockResponse(line: line, context: context) + "\n" - _ = response.withCString { ptr in - Darwin.write(clientFD, ptr, strlen(ptr)) - } + guard writeAllToFixtureSocket(response, fd: clientFD) else { return } } } } @@ -293,42 +294,18 @@ struct CampfireHookNotificationTests { standardInput: String, timeout: TimeInterval ) -> ProcessRunResult { - let process = Process() - let stdoutPipe = Pipe() - let stderrPipe = Pipe() - let stdinPipe = Pipe() - process.executableURL = URL(fileURLWithPath: executablePath) - process.arguments = arguments - process.environment = environment - process.standardInput = stdinPipe - process.standardOutput = stdoutPipe - process.standardError = stderrPipe - - do { - try process.run() - } catch { - return ProcessRunResult(status: -1, stdout: "", stderr: String(describing: error), timedOut: false) - } - stdinPipe.fileHandleForWriting.write(Data(standardInput.utf8)) - try? stdinPipe.fileHandleForWriting.close() - - - let timedOut = waitForProcessExit(process, timeout: timeout) == .timedOut - if timedOut { - process.terminate() - if waitForProcessExit(process, timeout: 1) == .timedOut { - kill(process.processIdentifier, SIGKILL) - _ = waitForProcessExit(process, timeout: 1) - } - } - - let stdout = String(data: stdoutPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "" - let stderr = String(data: stderrPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "" + let result = CLIHookProcessRunner.run( + executablePath: executablePath, + arguments: arguments, + environment: environment, + standardInput: standardInput, + timeout: timeout + ) return ProcessRunResult( - status: process.isRunning ? SIGKILL : process.terminationStatus, - stdout: stdout, - stderr: stderr, - timedOut: timedOut + status: result.status, + stdout: result.stdout, + stderr: result.stderr, + timedOut: result.timedOut ) } diff --git a/cmuxTests/ClaudeWrapperResumeEnvironmentTests.swift b/cmuxCLITests/ClaudeWrapperResumeEnvironmentTests.swift similarity index 100% rename from cmuxTests/ClaudeWrapperResumeEnvironmentTests.swift rename to cmuxCLITests/ClaudeWrapperResumeEnvironmentTests.swift diff --git a/cmuxTests/CodexTeamsAppServerProcessTests.swift b/cmuxCLITests/CodexTeamsAppServerProcessTests.swift similarity index 76% rename from cmuxTests/CodexTeamsAppServerProcessTests.swift rename to cmuxCLITests/CodexTeamsAppServerProcessTests.swift index 0ab6b04d8c19..9d322a2be3eb 100644 --- a/cmuxTests/CodexTeamsAppServerProcessTests.swift +++ b/cmuxCLITests/CodexTeamsAppServerProcessTests.swift @@ -2,12 +2,6 @@ import Darwin import Foundation import Testing -#if canImport(cmux_DEV) -@testable import cmux_DEV -#elseif canImport(cmux) -@testable import cmux -#endif - @Suite(.serialized) struct CodexTeamsAppServerProcessTests { @Test("closing the launcher lifetime terminates the app-server process tree") @@ -94,28 +88,6 @@ struct CodexTeamsAppServerProcessTests { } private func bundledCLIPath() throws -> String { - let appBundleURL = Bundle(for: BundleToken.self) - .bundleURL - .deletingLastPathComponent() - .deletingLastPathComponent() - .deletingLastPathComponent() - let cliURL = appBundleURL - .appendingPathComponent("Contents", isDirectory: true) - .appendingPathComponent("Resources", isDirectory: true) - .appendingPathComponent("bin", isDirectory: true) - .appendingPathComponent("cmux", isDirectory: false) - guard FileManager.default.isExecutableFile(atPath: cliURL.path) else { - throw NSError( - domain: "CodexTeamsAppServerProcessTests", - code: 1, - userInfo: [ - NSLocalizedDescriptionKey: - "Bundled cmux CLI not found at \(cliURL.path)" - ] - ) - } - return cliURL.path + try BundledCLITestSupport.bundledCLIPath() } - - private final class BundleToken {} } diff --git a/cmuxTests/CodexTeamsResumedBackfillTests.swift b/cmuxCLITests/CodexTeamsResumedBackfillTests.swift similarity index 82% rename from cmuxTests/CodexTeamsResumedBackfillTests.swift rename to cmuxCLITests/CodexTeamsResumedBackfillTests.swift index bd73c0e05cac..e376c88c1bb9 100644 --- a/cmuxTests/CodexTeamsResumedBackfillTests.swift +++ b/cmuxCLITests/CodexTeamsResumedBackfillTests.swift @@ -94,24 +94,7 @@ struct CodexTeamsResumedBackfillTests { } private func bundledCLIPath() throws -> String { - let appBundleURL = Bundle(for: BundleToken.self) - .bundleURL - .deletingLastPathComponent() - .deletingLastPathComponent() - .deletingLastPathComponent() - let cliURL = appBundleURL - .appendingPathComponent("Contents", isDirectory: true) - .appendingPathComponent("Resources", isDirectory: true) - .appendingPathComponent("bin", isDirectory: true) - .appendingPathComponent("cmux", isDirectory: false) - guard FileManager.default.isExecutableFile(atPath: cliURL.path) else { - throw NSError( - domain: "CodexTeamsResumedBackfillTests", - code: 1, - userInfo: [NSLocalizedDescriptionKey: "Bundled cmux CLI not found at \(cliURL.path)"] - ) - } - return cliURL.path + try BundledCLITestSupport.bundledCLIPath() } private func stop(_ process: Process) { @@ -127,5 +110,4 @@ struct CodexTeamsResumedBackfillTests { process.waitUntilExit() } - private final class BundleToken {} } diff --git a/cmuxTests/CodexTerminalErrorNotificationTests.swift b/cmuxCLITests/CodexTerminalErrorNotificationTests.swift similarity index 95% rename from cmuxTests/CodexTerminalErrorNotificationTests.swift rename to cmuxCLITests/CodexTerminalErrorNotificationTests.swift index ffef2620d534..88e9501f2f91 100644 --- a/cmuxTests/CodexTerminalErrorNotificationTests.swift +++ b/cmuxCLITests/CodexTerminalErrorNotificationTests.swift @@ -94,6 +94,7 @@ private final class CodexTerminalErrorSocketServer: @unchecked Sendable { private let surfaceID: String private let lock = NSLock() private var recordedCommands: [String] = [] + private let notifications = AgentHookTestNotificationPipeline() private let finished = DispatchSemaphore(value: 0) var commands: [String] { @@ -150,6 +151,7 @@ private final class CodexTerminalErrorSocketServer: @unchecked Sendable { } guard clientFD >= 0 else { return } defer { Darwin.close(clientFD) } + guard ignoreSIGPIPE(onAcceptedFixtureSocket: clientFD) else { return } var pending = Data() var buffer = [UInt8](repeating: 0, count: 4096) @@ -166,7 +168,12 @@ private final class CodexTerminalErrorSocketServer: @unchecked Sendable { let lineData = pending.subdata(in: 0.. ProcessRunResult { - let process = Process() - let stdoutPipe = Pipe() - let stderrPipe = Pipe() - let stdinPipe = standardInput == nil ? nil : Pipe() - process.executableURL = URL(fileURLWithPath: executablePath) - process.arguments = arguments - process.environment = CLIChildEnvironment(appHostEnvironment: ProcessInfo.processInfo.environment).normalizing(environment) - process.standardInput = stdinPipe ?? FileHandle.nullDevice - process.standardOutput = stdoutPipe - process.standardError = stderrPipe - let exitSignal = DispatchSemaphore(value: 0) - // Observe actual termination instead of scheduling a blocking waiter on - // the same global pool used to drain the child's output. - process.terminationHandler = { _ in exitSignal.signal() } - - do { - try process.run() - } catch { - return ProcessRunResult(status: -1, stdout: "", stderr: String(describing: error), timedOut: false) - } - if let standardInput, let stdinPipe { - stdinPipe.fileHandleForWriting.write(Data(standardInput.utf8)) - try? stdinPipe.fileHandleForWriting.close() - } - - let outputLock = NSLock() - var stdoutData = Data() - var stderrData = Data() - let outputGroup = DispatchGroup() - - outputGroup.enter() - DispatchQueue.global(qos: .utility).async { - let data = stdoutPipe.fileHandleForReading.readDataToEndOfFile() - outputLock.lock() - stdoutData = data - outputLock.unlock() - outputGroup.leave() - } - - outputGroup.enter() - DispatchQueue.global(qos: .utility).async { - let data = stderrPipe.fileHandleForReading.readDataToEndOfFile() - outputLock.lock() - stderrData = data - outputLock.unlock() - outputGroup.leave() - } - - let timedOut = exitSignal.wait(timeout: .now() + timeout) == .timedOut - if timedOut { - process.terminate() - if exitSignal.wait(timeout: .now() + 1) == .timedOut { - kill(process.processIdentifier, SIGKILL) - _ = exitSignal.wait(timeout: .now() + 1) - } - } - - _ = outputGroup.wait(timeout: .now() + 2) - - outputLock.lock() - let finalStdoutData = stdoutData - let finalStderrData = stderrData - outputLock.unlock() - let stdout = String(data: finalStdoutData, encoding: .utf8) ?? "" - let stderr = String(data: finalStderrData, encoding: .utf8) ?? "" + // Implementation lives in CLIHookProcessRunner so the hook helpers that + // only need the built CLI can compile into the product-level test + // bundle, which does not have this app-host suite. + let result = CLIHookProcessRunner.run( + executablePath: executablePath, + arguments: arguments, + environment: environment, + standardInput: standardInput, + timeout: timeout + ) return ProcessRunResult( - status: process.isRunning ? SIGKILL : process.terminationStatus, - stdout: stdout, - stderr: stderr, - timedOut: timedOut + status: result.status, + stdout: result.stdout, + stderr: result.stderr, + timedOut: result.timedOut ) } diff --git a/scripts/ci/app_host_test_products.py b/scripts/ci/app_host_test_products.py index 285930b3548d..f2d4eb910642 100644 --- a/scripts/ci/app_host_test_products.py +++ b/scripts/ci/app_host_test_products.py @@ -12,7 +12,14 @@ import sys from pathlib import Path -SCHEMES = {"cmux": "CMUX_UI_XCTESTRUN", "cmux-unit": "CMUX_APP_HOST_XCTESTRUN", "cmux-numeric-locale": "CMUX_NUMERIC_LOCALE_XCTESTRUN"} +SCHEMES = { + "cmux": "CMUX_UI_XCTESTRUN", + "cmux-unit": "CMUX_APP_HOST_XCTESTRUN", + "cmux-numeric-locale": "CMUX_NUMERIC_LOCALE_XCTESTRUN", + # cmuxCLITests has no app host: its bundle is loaded by the platform's own + # xctest agent, so the manifest names no product as its test host. + "cmux-cli-tests": "CMUX_CLI_TESTS_XCTESTRUN", +} RECEIPT = "cmux-test-products.json" @@ -104,6 +111,18 @@ def targets(value): yield from targets(item) +def hosted_by_product(target) -> bool: + """False when the platform's xctest agent loads the bundle directly. + + A unit-test target without TEST_HOST is hosted by + __PLATFORMS__/.../Agents/xctest, which lives inside Xcode and not inside + Build/Products. Such a target has no product test host to validate, only a + bundle. + """ + host = target.get("TestHostPath", "") + return bool(host) and "__PLATFORMS__" not in host + + def validate_manifest(value, products: Path) -> None: """Prove that the relocated manifest references an existing app and test bundle.""" found = list(targets(value)) @@ -112,7 +131,7 @@ def validate_manifest(value, products: Path) -> None: for target in found: host = target.get("TestHostPath", "").replace("__TESTROOT__", str(products)) bundle = target["TestBundlePath"].replace("__TESTROOT__", str(products)).replace("__TESTHOST__", host) - paths = [("host", host), ("bundle", bundle)] + paths = [("host", host), ("bundle", bundle)] if hosted_by_product(target) else [("bundle", bundle)] if "UITargetAppPath" in target: app = target["UITargetAppPath"].replace("__TESTROOT__", str(products)) paths.append(("UI target app", app)) diff --git a/scripts/ci/cmux-unit-test-timings.json b/scripts/ci/cmux-unit-test-timings.json index c0727dd37f74..a816b9674237 100644 --- a/scripts/ci/cmux-unit-test-timings.json +++ b/scripts/ci/cmux-unit-test-timings.json @@ -182,34 +182,21 @@ "CLIBrowserEvalOutputTests": 711, "CLICallerWorkspaceDefaultTests": 1151, "CLIChildEnvironmentTests": 2710, - "CLIClaudeHookTimeoutRegressionTests": 6544, - "CLICodexHookPathQuotingRegressionTests": 28027, - "CLICodexHookTimeoutRegressionTests": 1120, - "CLICodexQueuedHookContractTests": 1952, "CLIExecInheritedSignalStateTests": 4051, - "CLIExplicitSurfaceRoutingTests": 5250, "CLIForwardingLaunchArgumentTests": 5, "CLINotifyProcessIntegrationRegressionTests": 234131, "CLINotifyProcessIntegrationTests": 10300, "CLIOmpHookBindingTests": 3292, - "CLIOmpSupersededCleanupTests": 1571, - "CLIRelayQueuedHookRegressionTests": 6257, "CLIRemoteShellStartupPerformanceTests": 1, "CLISSHPTYAttachReplayBoundaryTests": 4263, - "CLISSHPTYResizeInputTests": 1221, "CLISSHSessionAttachAnchorTests": 602, "CLISendQueuedOutputTests": 716, - "CLIStdioSIGPIPERegressionTests": 499, "CLITmuxCompatRemoteSplitTests": 4287, - "CLITmuxCompatStoreConcurrencyTests": 76, - "CLIWindowHandleRoutingTests": 1, - "CLIWorkspaceGroupSafetyTests": 3180, "CMUXCLIErrorOutputRegressionTests": 14183, "CMUXCLIForkLegacyFallbackTests": 22351, "CMUXCLIForkVerbRegressionTests": 1020, "CMUXCLISentryTelemetryRegressionTests": 650, "CMUXOpenCommandTests": 18407, - "CMUXOpenHTMLFocusTests": 27794, "CMUXWorkspaceLoadingCLIRegressionTests": 613, "CaffeineControllerTests": 6346, "CanvasShortcutRoutingFeedbackTests": 9222, @@ -223,7 +210,6 @@ "ClaudeHookPIDAuthenticationTests": 3139, "ClaudeHookSurfaceResolutionSwiftTests": 1067, "ClaudeNotificationStatusLifecycleTests": 3661, - "ClaudeWrapperResumeEnvironmentTests": 1620, "CloudActivationPolicyTests": 430, "CloudAgentSkillLauncherTests": 432, "CloudDesktopAccessTests": 10832, @@ -301,8 +287,6 @@ "CoalesceLatestPublisherDemandTests": 5385, "CodexAppServerSessionTests": 13430, "CodexCodeModeRolloutIdentityTests": 27738, - "CodexTeamsAppServerProcessTests": 1477, - "CodexTeamsResumedBackfillTests": 2857, "CodexTurnCompletionOwnershipTests": 1243, "CommandEquivalentTransientFocusRepairTests": 4, "CommandPaletteAuthCommandTests": 3, diff --git a/scripts/ci/compile-app-host-test-product.sh b/scripts/ci/compile-app-host-test-product.sh index 36074ed35572..2b9011f9c19a 100755 --- a/scripts/ci/compile-app-host-test-product.sh +++ b/scripts/ci/compile-app-host-test-product.sh @@ -125,7 +125,7 @@ build() { # Build the app/UI scheme first so its warning log retains the old runtime # job warning-budget scope; subsequent schemes reuse the same app objects. # shellcheck disable=SC2016 # Xcode expands $(inherited), not the shell - for scheme in cmux cmux-unit cmux-numeric-locale; do + for scheme in cmux cmux-unit cmux-numeric-locale cmux-cli-tests; do xcodebuild -project cmux.xcodeproj -scheme "$scheme" -configuration Debug \ -derivedDataPath "$derived_data" \ -clonedSourcePackagesDirPath "$source_packages" \ diff --git a/scripts/ci/detect_ci_change_areas.py b/scripts/ci/detect_ci_change_areas.py index 5b3d712414be..7bb12670d76a 100644 --- a/scripts/ci/detect_ci_change_areas.py +++ b/scripts/ci/detect_ci_change_areas.py @@ -456,14 +456,20 @@ def _release_jobs(jobs: dict[str, str]) -> frozenset[str]: consumers |= feeders +# The ci-macos.yml jobs the CLI product lane runs through: its own job, and +# the admission job that builds and publishes the product it restores. +MACOS_CLI_LANE_JOBS = frozenset({"cli-product-tests", "macos-compile-admission"}) + + def macos_workflow_change_areas(base: str, head: str) -> Optional[ChangeAreas]: """The areas a ci-macos.yml edit selects, compared job by job, or None for all. Every job the workflow owns runs behind the macOS area. Only a job that mentions `release_build` (the Release jobs, the steps that produce their helper, the status gate that reports them), or whose outputs such a job - reads, needs the Release build as well. The preamble, which holds the - workflow_call inputs, reaches every job. + reads, needs the Release build as well. The CLI lane runs only when + cli-product-tests or the admission job that builds its product changed. + The preamble, which holds the workflow_call inputs, reaches every job. """ diff = _changed_workflow_jobs(base, head) if diff is None: @@ -471,7 +477,8 @@ def macos_workflow_change_areas(base: str, head: str) -> Optional[ChangeAreas]: base_jobs, head_jobs, changed = diff release = _release_jobs(base_jobs) | _release_jobs(head_jobs) return ChangeAreas( - macos=True, web=False, agent_session_web=False, cli=False, + macos=True, web=False, agent_session_web=False, + cli=bool(changed & MACOS_CLI_LANE_JOBS), swift_packages=False, release_build=bool(changed & release), ) @@ -617,14 +624,31 @@ def is_guard_only_test(path: str, references: Optional[tuple[frozenset[str], fro "scripts/generate-cmux-config-schema.py", "web/data/cmux.schema.json", "skills/cmux-settings/scripts/cmux-settings", + # ci-macos.yml's cli-product-tests restores the compiled product and runs + # the host-free bundle through these. Without them here, a change to one + # would compile admission without ever running the lane that reads it. + "scripts/ci/node_product_cache.py", + "scripts/ci/peer_product_source.py", + "scripts/ci/restore-r2-artifact.py", + "scripts/ci/parallel_artifact_download.py", + "scripts/ci/restore-app-host-test-product.sh", + "scripts/ci/run-and-capture.sh", + "scripts/ci/require_selected_test_execution.sh", + # What restore-app-host-test-product.sh itself runs. + "scripts/ci/app_host_test_products.py", + "scripts/ci/canonical-build-root.sh", }) CLI_LANE_INPUT_PREFIXES = ( "CLI/", + "cmuxCLITests/", + "cmuxCLITestSupport/", # The lane builds the cmux-cli scheme of this project and keys its package # cache on the project's Package.resolved. "cmux.xcodeproj/", ".github/actions/cache-restore/", + # cli-product-tests' canonical fallback download of the compiled product. + ".github/actions/download-test-product/", # The lane runs `swift test` in this package directly. "Packages/macOS/CmuxFoundation/", ) @@ -1494,7 +1518,7 @@ def is_macos_change( def is_test_only_source(path: str) -> bool: # The Release app builds only the cmux target, so test sources cannot reach # it. A new test file also edits project.pbxproj, which is not matched here. - return path.startswith(("cmuxTests/", "cmuxUITests/")) or bool(_PACKAGE_TESTS_RE.match(path)) + return path.startswith(("cmuxTests/", "cmuxCLITests/", "cmuxCLITestSupport/", "cmuxUITests/")) or bool(_PACKAGE_TESTS_RE.match(path)) RELEASE_BUILD_NEUTRAL_INPUTS = frozenset({ @@ -1659,6 +1683,8 @@ def classify_files(paths: Iterable[str], *, macos = True if macos_workflow_areas is None or macos_workflow_areas.release_build: release_build = True + if macos_workflow_areas is None or macos_workflow_areas.cli: + cli = True continue if path == WEB_WORKFLOW_PATH: # A reusable web workflow edit must exercise every job body it owns. diff --git a/scripts/ci/detect_linux_guard_changes.py b/scripts/ci/detect_linux_guard_changes.py index 1f0dba5e1750..5b2f6272faf7 100755 --- a/scripts/ci/detect_linux_guard_changes.py +++ b/scripts/ci/detect_linux_guard_changes.py @@ -95,7 +95,8 @@ def classify(paths: list[str], *, event: str, macos: str) -> dict[str, bool]: routes["linux_guard_history"] = True routes["linux_guard_source"] = True elif path.startswith(("Sources/", "CLI/", "Resources/", "Packages/", - "cmuxTests/", "cmuxUITests/", "cmux.xcodeproj/", + "cmuxTests/", "cmuxCLITests/", "cmuxCLITestSupport/", + "cmuxUITests/", "cmux.xcodeproj/", "cmux.xcworkspace/", "vendor/bonsplit/", "ios/")): routes["linux_guard_source"] = True elif path.startswith(("web/", "webviews/", "cmux-tui/")): diff --git a/scripts/ci/select_package_tests.py b/scripts/ci/select_package_tests.py index c0c54d7311b0..cedc219772fa 100755 --- a/scripts/ci/select_package_tests.py +++ b/scripts/ci/select_package_tests.py @@ -57,6 +57,8 @@ "TunnelExtension/", "Resources/", "cmuxTests/", + "cmuxCLITests/", + "cmuxCLITestSupport/", "cmuxUITests/", "cmux.xcodeproj/", "cmux.xcworkspace/", diff --git a/scripts/ci/workflow_guard_groups.py b/scripts/ci/workflow_guard_groups.py index 63a624bdc20c..c4e0ffd42f31 100644 --- a/scripts/ci/workflow_guard_groups.py +++ b/scripts/ci/workflow_guard_groups.py @@ -154,7 +154,8 @@ def _python_syntax_scan(path: str) -> bool: def _determinism_scan(path: str) -> bool: if not path.endswith(DETERMINISM_SUFFIXES): return False - if path.startswith(("cmuxTests/", "cmuxUITests/", "ios/cmuxUITests/", + if path.startswith(("cmuxTests/", "cmuxCLITests/", "cmuxCLITestSupport/", + "cmuxUITests/", "ios/cmuxUITests/", "tests/", "tests_v2/", "web/tests/", "webviews/test/")): return True return path.startswith("Packages/") and "/Tests/" in path diff --git a/scripts/ci/workloads/ci-guard.sh b/scripts/ci/workloads/ci-guard.sh index 317c81dfcd69..2a56c3557c23 100644 --- a/scripts/ci/workloads/ci-guard.sh +++ b/scripts/ci/workloads/ci-guard.sh @@ -18,4 +18,7 @@ python3 tests/test_ci_linux_guard_routing.py python3 tests/test_ci_merge_queue_required_checks.py python3 tests/test_ci_reusable_workflow_permissions.py ./scripts/lint-pbxproj-test-wiring.sh +./scripts/lint-pbxproj-test-wiring.sh --target cmuxCLITests --tests-dir cmuxCLITests +./scripts/lint-pbxproj-test-wiring.sh --target cmuxCLITests --tests-dir cmuxCLITestSupport +./scripts/lint-pbxproj-test-wiring.sh --target cmuxTests --tests-dir cmuxCLITestSupport stage end test diff --git a/scripts/lint-errno-in-test-assertions.py b/scripts/lint-errno-in-test-assertions.py index d89914524655..8a8bcc78d01b 100755 --- a/scripts/lint-errno-in-test-assertions.py +++ b/scripts/lint-errno-in-test-assertions.py @@ -48,7 +48,7 @@ # Swift test sources. Matches the paths the workflow guard router sends to the # quality-determinism group, which runs this lint. TEST_PATH = re.compile( - r"^(?:cmuxTests|cmuxUITests|ios/cmuxUITests)/.*\.swift$" + r"^(?:cmuxTests|cmuxCLITests|cmuxCLITestSupport|cmuxUITests|ios/cmuxUITests)/.*\.swift$" r"|^Packages/.*/Tests/.*\.swift$" ) diff --git a/scripts/lint-pbxproj-test-wiring.sh b/scripts/lint-pbxproj-test-wiring.sh index 446502941cbd..949404327837 100755 --- a/scripts/lint-pbxproj-test-wiring.sh +++ b/scripts/lint-pbxproj-test-wiring.sh @@ -25,12 +25,22 @@ set -euo pipefail REPO_ROOT="" +TARGET_NAME="cmuxTests" +TESTS_DIR_ARG="" while [ "$#" -gt 0 ]; do case "$1" in --repo-root) REPO_ROOT="$2" shift 2 ;; + --target) + TARGET_NAME="$2" + shift 2 + ;; + --tests-dir) + TESTS_DIR_ARG="$2" + shift 2 + ;; -h|--help) sed -n '1,25p' "$0" | sed 's/^# *//' exit 0 @@ -47,7 +57,8 @@ if [ -z "$REPO_ROOT" ]; then fi PBXPROJ="$REPO_ROOT/cmux.xcodeproj/project.pbxproj" -TESTS_DIR="$REPO_ROOT/cmuxTests" +TESTS_REL="${TESTS_DIR_ARG:-$TARGET_NAME}" +TESTS_DIR="$REPO_ROOT/$TESTS_REL" if [ ! -f "$PBXPROJ" ]; then echo "lint-pbxproj-test-wiring: not found: $PBXPROJ" >&2 @@ -77,8 +88,8 @@ fi # only care about the native-target block. Use awk to capture every # `/* cmuxTests */ = { ... };` block and keep only the one whose `isa = # PBXNativeTarget;` line is present. -tests_target_block="$(awk ' - /\/\* cmuxTests \*\/ = \{/ { capture = 1; buf = "" } +tests_target_block="$(awk -v target="$TARGET_NAME" ' + $0 ~ "/\\* " target " \\*/ = \\{" { capture = 1; buf = "" } capture { buf = buf $0 "\n" } capture && /^[[:space:]]*\};[[:space:]]*$/ { if (buf ~ /isa = PBXNativeTarget;/) { @@ -91,7 +102,7 @@ tests_target_block="$(awk ' ' "$PBXPROJ")" if [ -z "$tests_target_block" ]; then - echo "lint-pbxproj-test-wiring: could not locate cmuxTests PBXNativeTarget in $PBXPROJ" >&2 + echo "lint-pbxproj-test-wiring: could not locate $TARGET_NAME PBXNativeTarget in $PBXPROJ" >&2 exit 2 fi @@ -104,7 +115,7 @@ tests_sources_uuid="$(printf '%s\n' "$tests_target_block" \ | awk '{print $1}')" if [ -z "$tests_sources_uuid" ]; then - echo "lint-pbxproj-test-wiring: cmuxTests target has no Sources build phase reference" >&2 + echo "lint-pbxproj-test-wiring: $TARGET_NAME target has no Sources build phase reference" >&2 exit 2 fi @@ -118,7 +129,7 @@ tests_sources_block="$(awk -v uuid="$tests_sources_uuid" ' ' "$PBXPROJ")" if [ -z "$tests_sources_block" ]; then - echo "lint-pbxproj-test-wiring: could not slice cmuxTests Sources build phase (uuid=$tests_sources_uuid)" >&2 + echo "lint-pbxproj-test-wiring: could not slice $TARGET_NAME Sources build phase (uuid=$tests_sources_uuid)" >&2 exit 2 fi @@ -146,25 +157,31 @@ if [ "${#missing[@]}" -eq 0 ]; then exit 0 fi -echo "lint-pbxproj-test-wiring: ${#missing[@]} test file(s) not a member of the cmuxTests target's Sources build phase (uuid=$tests_sources_uuid) in cmux.xcodeproj/project.pbxproj" +echo "lint-pbxproj-test-wiring: ${#missing[@]} test file(s) not a member of the $TARGET_NAME target's Sources build phase (uuid=$tests_sources_uuid) in cmux.xcodeproj/project.pbxproj" for entry in "${missing[@]}"; do echo " - $entry" done echo "" -echo "Each cmuxTests/.swift must be wired into cmux.xcodeproj/project.pbxproj" -echo "as a full target member of cmuxTests:" +echo "Each $TESTS_REL/.swift must be wired into cmux.xcodeproj/project.pbxproj" +echo "as a full target member of $TARGET_NAME:" echo " 1. a PBXBuildFile entry (line ends with '.swift in Sources */ = { ... };')" echo " 2. a PBXFileReference entry" -echo " 3. an entry in the cmuxTests group children list" -echo " 4. an entry in the cmuxTests target's PBXSourcesBuildPhase files" +echo " 3. an entry in the $TARGET_NAME group children list" +echo " 4. an entry in the $TARGET_NAME target's PBXSourcesBuildPhase files" echo " (line ends with '.swift in Sources */,')" echo "" -echo "This lint slices the cmuxTests Sources phase and looks for entry 4 there." +echo "This lint slices the $TARGET_NAME Sources phase and looks for entry 4 there." echo "Files wired only into cmuxUITests, cmux, or the project tree (without" -echo "cmuxTests target membership) are silently skipped by Xcode and will be" +echo "$TARGET_NAME target membership) are silently skipped by Xcode and will be" echo "flagged here." echo "" -echo "Run ./scripts/sync-test-wiring to reconcile direct cmuxTests/*.swift files." -echo "Use ./scripts/sync-test-wiring --check for a read-only authoring/CI check." -echo "This lint remains the defensive cmuxTests Sources-phase guard." +if [ "$TARGET_NAME" = "cmuxTests" ] && [ "$TESTS_REL" = "cmuxTests" ]; then + echo "Run ./scripts/sync-test-wiring to reconcile direct $TESTS_REL/*.swift files." + echo "Use ./scripts/sync-test-wiring --check for a read-only authoring/CI check." +else + # sync-test-wiring only reconciles cmuxTests. Other targets are wired by hand. + echo "sync-test-wiring only reconciles cmuxTests; add the four $TARGET_NAME" + echo "entries above by hand (or in Xcode) for $TESTS_REL/*.swift." +fi +echo "This lint remains the defensive $TARGET_NAME Sources-phase guard." exit 1 diff --git a/tests/test-execution.toml b/tests/test-execution.toml index 4c3ee85770d4..95975669f23b 100644 --- a/tests/test-execution.toml +++ b/tests/test-execution.toml @@ -411,6 +411,10 @@ lane = "linux-guard" path = "tests/test_ci_persistent_mac_compile.py" lane = "linux-guard" +[[test]] +path = "tests/test_ci_cli_product_routing.py" +lane = "linux-guard" + [[test]] path = "tests/test_ci_product_publication.py" lane = "linux-guard" diff --git a/tests/test_app_host_test_products.py b/tests/test_app_host_test_products.py index 6f292fd47ae6..d743f105caac 100644 --- a/tests/test_app_host_test_products.py +++ b/tests/test_app_host_test_products.py @@ -29,7 +29,9 @@ def setUp(self): executable = products / "Debug/cmux DEV.app/Contents/MacOS/cmux DEV" executable.parent.mkdir(parents=True) executable.write_text("binary") - for scheme in ("cmux", "cmux-unit", "cmux-numeric-locale"): + self.cli_bundle = Path("Debug/cmuxCLITests.xctest") + (products / self.cli_bundle).mkdir(parents=True) + for scheme in ("cmux", "cmux-unit", "cmux-numeric-locale", "cmux-cli-tests"): target = { "TestHostPath": "__TESTROOT__/Debug/cmux DEV.app", "TestBundlePath": "__TESTHOST__/Contents/PlugIns/cmuxTests.xctest", @@ -45,6 +47,15 @@ def setUp(self): "UITargetAppPath": "__TESTROOT__/Debug/cmux DEV.app", "DependentProductPaths": [str(products / self.ui_bundle)], }) + if scheme == "cmux-cli-tests": + # A unit-test target without TEST_HOST is loaded by the + # platform's own xctest agent, which is not in Build/Products. + target = { + "TestHostPath": "__PLATFORMS__/MacOSX.platform/Developer/Library/Xcode/Agents/xctest", + "TestBundlePath": "__TESTROOT__/Debug/cmuxCLITests.xctest", + "EnvironmentVariables": {"SOURCE": "/producer/work/cmux/fixtures"}, + "DependentProductPaths": [str(products / self.cli_bundle)], + } # Cover both manifest versions Xcode has shipped. value = {"cmuxTests": target} if scheme == "cmux-unit" else {"TestConfigurations": [{"TestTargets": [target]}]} (products / f"{scheme}_macosx26.5-arm64.xctestrun").write_bytes(plistlib.dumps(value)) @@ -58,12 +69,25 @@ def test_relocation_preserves_nested_bundles_and_publishes_all_manifests(self): self.transfer() current = {**self.identity, "checkout": "/consumer/work/cmux", "developer": "/consumer/Xcode.app/Contents/Developer"} outputs = module.restore(self.consumer, current) - self.assertEqual(set(outputs), {"CMUX_APP_HOST_XCTESTRUN", "CMUX_NUMERIC_LOCALE_XCTESTRUN", "CMUX_UI_XCTESTRUN"}) + self.assertEqual( + set(outputs), + { + "CMUX_APP_HOST_XCTESTRUN", + "CMUX_CLI_TESTS_XCTESTRUN", + "CMUX_NUMERIC_LOCALE_XCTESTRUN", + "CMUX_UI_XCTESTRUN", + }, + ) for path in outputs.values(): value = plistlib.loads(Path(path).read_bytes()) target = list(module.targets(value))[0] self.assertEqual(target["EnvironmentVariables"]["SOURCE"], "/consumer/work/cmux/fixtures") - bundle = self.ui_bundle if "UITargetAppPath" in target else self.bundle + if "UITargetAppPath" in target: + bundle = self.ui_bundle + elif not module.hosted_by_product(target): + bundle = self.cli_bundle + else: + bundle = self.bundle self.assertEqual(target["DependentProductPaths"], [str(self.consumer / "Build/Products" / bundle)]) self.assertTrue(Path(target["DependentProductPaths"][0]).exists()) diff --git a/tests/test_ci_app_host_home_isolation.py b/tests/test_ci_app_host_home_isolation.py index a1715f61fc32..faaa64f091d1 100644 --- a/tests/test_ci_app_host_home_isolation.py +++ b/tests/test_ci_app_host_home_isolation.py @@ -303,6 +303,46 @@ def check_every_app_host_home_is_identified_and_cleaned() -> None: ) +def check_every_test_executing_lane_pins_its_home() -> None: + """Hold every lane that runs XCTest to the same home pinning. + + The sibling guard above keys on `prepare-app-host-home.sh`, so a host-free + lane that runs no app host is invisible to it. One was: `cli-product-tests` + isolated itself with a bare `HOME` in the job environment, which reaches + xcodebuild and stops there -- xcodebuild forwards only TEST_RUNNER_-prefixed + variables into the test process. Every fixture in that lane read the + runner's real home, and on a reused self-hosted runner that is shared, + persistent state between runs. + + CFFIXED_USER_HOME is required alongside HOME because Foundation resolves + NSHomeDirectory() through getpwuid unless it is set, so HOME alone moves + nothing for Swift code. + """ + required = ("TEST_RUNNER_HOME", "TEST_RUNNER_CFFIXED_USER_HOME") + for path, workflow in zip(WORKFLOW_PATHS, WORKFLOWS): + for job_name, job in (workflow.get("jobs") or {}).items(): + for step in job.get("steps") or []: + run = str(step.get("run", "")) + if "xcodebuild test" not in run: + continue + # Enumeration lists test identifiers without running them, so + # it reads no configuration and needs no home of its own. + if "-enumerate-tests" in run: + continue + if "run-app-host-xcodebuild.sh" in run: + continue + where = ( + f"{path.name} job {job_name} step {step.get('name')!r}" + ) + missing = [key for key in required if key not in run] + if missing: + raise SystemExit( + f"FAIL: {where} runs XCTest without {', '.join(missing)}; " + "xcodebuild forwards only TEST_RUNNER_-prefixed variables " + "into the test process" + ) + + def main() -> int: override_fixture = """\ @@ -799,6 +839,7 @@ def main() -> int: ) check_every_app_host_home_is_identified_and_cleaned() + check_every_test_executing_lane_pins_its_home() print("PASS: app-host XCTest receives an isolated launch home") return 0 diff --git a/tests/test_ci_canonical_build_root.py b/tests/test_ci_canonical_build_root.py index 4a5dd16ff515..87596232c733 100644 --- a/tests/test_ci_canonical_build_root.py +++ b/tests/test_ci_canonical_build_root.py @@ -5,6 +5,7 @@ import os import json +import re import subprocess import tempfile import unittest @@ -250,7 +251,25 @@ def test_resolve_build_and_fingerprint_use_canonical_paths(self): capture_output=True, text=True) self.assertEqual(result.returncode, 0, result.stderr) records = [json.loads(line) for line in calls.read_text().splitlines()] - self.assertEqual(len(records), 5) + # Derive the expected calls from the recipe rather than pinning a + # count: one version probe, one resolve, then one build per scheme. + # A hardcoded total silently breaks whenever a scheme is added -- + # cmux-cli-tests did exactly that. + schemes = re.findall( + r"for scheme in ([^;]+); do", + SCRIPT.read_text(encoding="utf-8"), + ) + self.assertEqual(len(schemes), 1, "expected one scheme loop in the recipe") + expected_schemes = schemes[0].split() + self.assertEqual(len(records), 2 + len(expected_schemes)) + # resolve() also passes -scheme (cmux-unit) alongside + # -resolvePackageDependencies; only the build invocations count. + built = [ + args[args.index("-scheme") + 1] + for _cwd, args in records + if "-scheme" in args and "-resolvePackageDependencies" not in args + ] + self.assertEqual(built, expected_schemes) for cwd, args in records: self.assertEqual(cwd, str(root / "src")) if '-clonedSourcePackagesDirPath' in args: diff --git a/tests/test_ci_change_areas.py b/tests/test_ci_change_areas.py index 2f39f4d92d99..75d20c617f0a 100755 --- a/tests/test_ci_change_areas.py +++ b/tests/test_ci_change_areas.py @@ -54,6 +54,7 @@ MACOS_JOBS = ( "macos-compile-admission", "app-host-unit-tests", + "cli-product-tests", "swift-package-tests", "tests-build-and-lag", "release-admission", @@ -1646,8 +1647,10 @@ def test_macos_workflow_job_edits_select_release_only_for_release_jobs() -> None change_areas = module.macos_workflow_change_areas mac_only = areas(macos=True) with_release = areas(macos=True, release_build=True) - for job in ("macos-compile-admission", "app-host-unit-tests", "tests-build-and-lag"): + for job in ("app-host-unit-tests", "tests-build-and-lag"): assert change_areas(real, edit_job(real, job)) == mac_only, job + # Admission builds the product the CLI lane restores, so it also runs that lane. + assert change_areas(real, edit_job(real, "macos-compile-admission")) == areas(macos=True, cli=True) # swift-package-tests produces the helper release-build consumes through # its outputs, and macos-status reports the Release verdict. for job in ("release-admission", "release-build", "swift-package-tests", "macos-status"): @@ -1686,7 +1689,35 @@ def test_macos_workflow_release_feeders_are_derived_from_outputs() -> None: def test_macos_workflow_areas_route_through_classify_files() -> None: path = ".github/workflows/ci-macos.yml" assert module.classify_files([path], macos_workflow_areas=areas(macos=True)) == areas(macos=True) - assert module.classify_files([path]) == areas(macos=True, release_build=True) + assert module.classify_files([path], macos_workflow_areas=areas(macos=True, cli=True)) == areas( + macos=True, cli=True, + ) + # Without a job-by-job comparison every job runs, the CLI lane included. + assert module.classify_files([path]) == areas(macos=True, release_build=True, cli=True) + + +def test_macos_workflow_cli_lane_edits_route_the_cli_lane() -> None: + real = MACOS_WORKFLOW.read_text(encoding="utf-8") + change_areas = module.macos_workflow_change_areas + assert change_areas(real, edit_job(real, "cli-product-tests")).cli + assert change_areas(real, edit_job(real, "macos-compile-admission")).cli + assert not change_areas(real, edit_job(real, "app-host-unit-tests")).cli + + +def test_cli_product_lane_scripts_route_the_cli_lane() -> None: + # Every repository script cli-product-tests runs is a CLI lane input. + real = MACOS_WORKFLOW.read_text(encoding="utf-8") + start = real.index("\n cli-product-tests:\n") + following = re.compile(r"\n [a-z0-9-]+:\n").search(real, start + 5) + block = real[start:following.start() if following else len(real)] + scripts = set(re.findall(r"(scripts/[A-Za-z0-9_./-]+\.(?:py|sh))", block)) + assert "scripts/ci/restore-app-host-test-product.sh" in scripts + # And what the restore script runs in turn. + scripts |= {"scripts/ci/app_host_test_products.py", "scripts/ci/canonical-build-root.sh"} + for script in sorted(scripts): + assert module.classify_files([script]).cli, script + for action in set(re.findall(r"uses: \./(\.github/actions/[A-Za-z0-9_-]+)", block)): + assert module.classify_files([f"{action}/action.yml"]).cli, action def test_workflow_routes_macos_shard_edit_without_release_build() -> None: @@ -2407,11 +2438,13 @@ def test_owned_control_plane_helper_reaches_detector_instead_of_fail_open_guard( ): result, outputs = run_detect_step_for_paths([path]) assert "CI router changed; running all CI areas." not in result.stdout, path + # cli-product-tests restores its product through app_host_test_products.py. + cli = "true" if path == "scripts/ci/app_host_test_products.py" else "false" assert outputs == [ "macos=true", "web=false", "agent_session_web=false", - "cli=false", + f"cli={cli}", "swift_packages=false", "release_build=false", ], path @@ -3145,10 +3178,12 @@ def test_macos_workflow_call_starts_after_cheap_static_gate() -> None: assert ( "needs.changes.outputs.full_suite == 'true' " "|| needs.changes.outputs.unit_suite == 'true' " + "|| needs.changes.outputs.cli == 'true' " "|| needs.changes.outputs.compile_admitted != 'true'" ) in caller for route in ( "macos", + "cli", "full_suite", "unit_suite", "compile_admitted", @@ -4638,7 +4673,7 @@ def test_macos_compile_admission_precedes_expensive_shards() -> None: assert "scripts/ci/compile-app-host-test-product.sh canonical-build" in admission compile_script = (ROOT / "scripts/ci/compile-app-host-test-product.sh").read_text(encoding="utf-8") assert "build-for-testing" in compile_script - assert "for scheme in cmux cmux-unit cmux-numeric-locale; do" in compile_script + assert "for scheme in cmux cmux-unit cmux-numeric-locale cmux-cli-tests; do" in compile_script assert "actions/cache@27d5ce7" in admission or "uses: ./.github/actions/cache-restore" in admission assert "steps.upload-products.outputs.artifact-id" in admission assert "steps.upload-products.outputs.artifact-digest" in admission @@ -5061,7 +5096,15 @@ def test_package_lane_routing_leaves_the_full_suite_jobs_alone() -> None: for job in yaml.safe_load(workflow)["jobs"] if "inputs.full_suite == 'true'" in workflow_job_block(job, MACOS_WORKFLOW) } - assert gated_jobs == full_suite_only | {"swift-package-tests", "macos-compile-admission"}, gated_jobs + assert gated_jobs == full_suite_only | { + "swift-package-tests", "macos-compile-admission", "cli-product-tests" + }, gated_jobs + # Package routing must not independently request the targeted CLI lane. + cli_condition = next( + line for line in workflow_job_block("cli-product-tests", MACOS_WORKFLOW).splitlines() + if line.strip().startswith("if:") + ) + assert "swift_packages" not in cli_condition, cli_condition def test_routed_package_lane_skips_the_release_helper_build() -> None: diff --git a/tests/test_ci_cli_product_routing.py b/tests/test_ci_cli_product_routing.py new file mode 100644 index 000000000000..d7e370eaa1d7 --- /dev/null +++ b/tests/test_ci_cli_product_routing.py @@ -0,0 +1,114 @@ +"""Execute the actual gates for targeted CLI tests, including prior admission.""" +import importlib.util +from itertools import product +import json +import os +from pathlib import Path +import re +import subprocess +import sys +import unittest + +import yaml + +ROOT = Path(__file__).resolve().parents[1] + + +def gate(expression, *, macos, cli, full_suite, compile_admitted, swift_packages="false", unit_suite="false", + unit_in_admission="false"): + routes = dict(macos=macos, cli=cli, full_suite=full_suite, unit_suite=unit_suite, + compile_admitted=compile_admitted, release_build="false", swift_packages=swift_packages, + unit_in_admission=unit_in_admission) + expression = expression.removeprefix("${{").removesuffix("}}").strip() + expression = expression.replace("!cancelled()", "True") + expression = expression.replace("github.event_name", repr("pull_request")) + def resolve(match): + key = match.group() + if key.endswith(".result"): + return repr("success") + return repr(routes[key.rsplit(".", 1)[-1]]) + expression = re.sub(r"needs\.[\w-]+\.(?:result|outputs\.\w+)|inputs\.\w+", resolve, expression) + return eval(expression.replace("&&", " and ").replace("||", " or "), {"__builtins__": {}}) + + +def run_step(job, name, env): + step = next(step for step in job["steps"] if step.get("name") == name) + return subprocess.run(["bash", "-c", step["run"]], cwd=ROOT, + env={**os.environ, **env}, capture_output=True, text=True) + + +class CLIProductRoutingTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.caller = yaml.safe_load((ROOT / ".github/workflows/ci.yml").read_text())["jobs"] + cls.workflow = yaml.safe_load((ROOT / ".github/workflows/ci-macos.yml").read_text()) + cls.jobs = cls.workflow["jobs"] + + def test_actual_conditions_keep_targeted_route_alive_after_prior_admission(self): + for macos, cli, full_suite, unit_suite, admitted, packages, in_admission in product(("false", "true"), repeat=7): + if unit_suite == "true" and admitted == "true": + continue # changes never admits a prior build for a unit-ci run + if in_admission == "true" and unit_suite != "true": + continue # choose_ci_suite only runs suites in admission for a unit run + routes = dict(macos=macos, cli=cli, full_suite=full_suite, unit_suite=unit_suite, + compile_admitted=admitted, swift_packages=packages, unit_in_admission=in_admission) + compile_needed = (macos == "true" or cli == "true") and ( + full_suite == "true" or cli == "true" or admitted != "true") + cli_needed = cli == "true" or (macos == "true" and full_suite == "true") + with self.subTest(**routes): + self.assertEqual(gate(self.caller["macos"]["if"], **routes), compile_needed or packages == "true") + self.assertEqual(gate(self.caller["macos-admission-gate"]["if"], **routes), compile_needed) + self.assertEqual(gate(self.jobs["macos-compile-admission"]["if"], **routes), compile_needed) + self.assertEqual(gate(self.jobs["cli-product-tests"]["if"], **routes), cli_needed) + self.assertEqual(gate(self.jobs["swift-package-tests"]["if"], **routes), + (macos == "true" and full_suite == "true") or packages == "true") + self.assertEqual(gate(self.jobs["app-host-unit-tests"]["if"], **routes), + macos == "true" and (full_suite == "true" or unit_suite == "true") + and in_admission != "true") + self.assertEqual(gate(self.jobs["tests-build-and-lag"]["if"], **routes), + macos == "true" and full_suite == "true") + + def test_required_status_rejects_missing_targeted_cli_work(self): + job = self.jobs["macos-status"] + for macos in ("false", "true"): + inputs = dict(macos=macos, cli="true", full_suite="false", compile_admitted="true", release_build="false") + needs = {name: {"result": "skipped"} for name in job["needs"]} + for name in ("macos-compile-admission", "cli-product-tests"): + needs[name]["result"] = "success" + env = dict(MACOS_INPUTS=json.dumps(inputs), MACOS_NEEDS=json.dumps(needs)) + result = run_step(job, "Check routed macOS jobs", env) + self.assertEqual(result.returncode, 0, result.stderr) + for name in ("macos-compile-admission", "cli-product-tests"): + for outcome in ("skipped", "failure", "cancelled"): + broken = {key: dict(value) for key, value in needs.items()} + broken[name]["result"] = outcome + env["MACOS_NEEDS"] = json.dumps(broken) + result = run_step(job, "Check routed macOS jobs", env) + self.assertNotEqual(result.returncode, 0, (macos, name, outcome)) + + def test_outer_tests_gate_requires_targeted_workflow(self): + job = self.caller["tests"] + step = next(step for step in job["steps"] if "macos_work_required" in step.get("run", "")) + for macos in ("false", "true"): + for outcome in ("success", "skipped", "failure", "cancelled"): + needs = { + "changes": {"result": "success", "outputs": dict(macos=macos, cli="true", full_suite="false", compile_admitted="true")}, + "linux-preflight": {"result": "success"}, + "macos": {"result": outcome}, "web": {"result": "skipped"}, + } + env_key = next(key for key, value in step["env"].items() if "toJSON(needs)" in value) + result = run_step(job, step["name"], {env_key: json.dumps(needs)}) + self.assertEqual(result.returncode == 0, outcome == "success", result.stderr) + + def test_test_sources_route_cli_without_widening_unrelated_tests(self): + spec = importlib.util.spec_from_file_location("cli_route_detector", ROOT / "scripts/ci/detect_ci_change_areas.py") + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + for path in ("cmuxCLITests/CLISSHPTYResizeInputTests.swift", "cmuxCLITestSupport/AgentHookTestNotificationPipeline.swift"): + self.assertTrue(module.classify_files([path]).cli, path) + self.assertFalse(module.classify_files(["cmuxTests/AgentNotificationGateTests.swift"]).cli) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_ci_guard_workflow_structure.py b/tests/test_ci_guard_workflow_structure.py index b95de0c2a679..27fa6e36d1f5 100644 --- a/tests/test_ci_guard_workflow_structure.py +++ b/tests/test_ci_guard_workflow_structure.py @@ -16,6 +16,7 @@ "python3 tests/test_e2e_warm_derived_data.py", "python3 tests/test_seed_derived_data.py", "python3 tests/test_ci_product_publication.py", + "python3 tests/test_ci_cli_product_routing.py", ] diff --git a/tests/test_ci_linux_guard_routing.py b/tests/test_ci_linux_guard_routing.py index 47250cc7471a..96981c1765cd 100755 --- a/tests/test_ci_linux_guard_routing.py +++ b/tests/test_ci_linux_guard_routing.py @@ -384,6 +384,13 @@ def test_reusable_workflow_policy_edits_skip_unrelated_guard_jobs(self): }) self.assertEqual(groups, ("preflight", "ci", "quality-determinism")) + def test_host_free_cli_test_sources_reach_the_determinism_lints(self): + for path in ("cmuxTests/ProbeTests.swift", "cmuxCLITests/ProbeTests.swift", + "cmuxCLITestSupport/ProbeSupport.swift"): + with self.subTest(path=path): + _, groups = route_decision([path], macos="true") + self.assertIn("quality-determinism", groups) + def test_native_edit_keeps_source_contracts_without_history_or_cli_guards(self): outputs = route(["Sources/Settings.swift", "CLAUDE.md"], macos="true") self.assertEqual(outputs, { diff --git a/tests/test_ci_parallel_artifact_transport.py b/tests/test_ci_parallel_artifact_transport.py index b45396c37caf..cb86f8a38a27 100644 --- a/tests/test_ci_parallel_artifact_transport.py +++ b/tests/test_ci_parallel_artifact_transport.py @@ -17,6 +17,8 @@ import unittest import zipfile import zlib + +import yaml from unittest import mock from pathlib import Path @@ -95,6 +97,42 @@ def test_restore_step_records_the_transport_it_used(self): self.assertIn('"github-parallel" if parallel_hit else', script) self.assertIn('echo "$EXPECTED_SHA256 $archive" | shasum -a 256 -c -', script) + def test_cli_product_lane_keeps_the_consumer_transport_chain(self): + # cli-product-tests restores the same compiled product without layers, + # so it must keep the same fast sources, route check and cache finalize. + block = job_block("cli-product-tests") + order = ( + "Verify GitHub-hosted route", + "Checkout", + "Try node-local compiled product cache", + "Try trusted fleet peer artifact source", + "Try shared R2 artifact transport", + "Try parallel GitHub artifact transport", + "Download compiled test product", + "Restore compiled test product", + "Finalize node-local compiled product cache", + ) + positions = [block.index(f"- name: {name}\n") for name in order] + self.assertEqual(positions, sorted(positions)) + parallel = step_block(block, "Try parallel GitHub artifact transport") + self.assertIn("steps.r2-products.outputs.hit != 'true'", parallel) + download = step_block(block, "Download compiled test product") + self.assertIn("steps.parallel-products.outputs.hit != 'true'", download) + restore = step_block(block, "Restore compiled test product") + for key in ("CMUX_R2_PRODUCT_HIT", "CMUX_PARALLEL_PRODUCT_HIT", "CMUX_ARTIFACT_R2_RESULT"): + self.assertIn(f"{key}: ", restore) + finalize = step_block(block, "Finalize node-local compiled product cache") + self.assertIn("if: always()", finalize) + self.assertIn("CMUX_NODE_PRODUCT_CACHE_LEASE: ${{ steps.node-products.outputs.lease }}", finalize) + self.assertIn("node_product_cache.py finalize", finalize) + jobs = yaml.safe_load(WORKFLOW)["jobs"] + # Same permissions: the R2 transport needs id-token to mint its token. + self.assertEqual(jobs["cli-product-tests"]["permissions"], jobs["app-host-unit-tests"]["permissions"]) + self.assertEqual( + step_block(block, "Verify GitHub-hosted route"), + step_block(job_block("app-host-unit-tests"), "Verify GitHub-hosted route"), + ) + def test_layer_transport_prefers_parallel_reads_and_keeps_the_stream_fallback(self): source = (ROOT / "scripts/ci/app_host_layer_transport.py").read_text(encoding="utf-8") self.assertIn("import parallel_artifact_download", source) diff --git a/tests/test_ci_pbxproj_test_wiring.sh b/tests/test_ci_pbxproj_test_wiring.sh index e562b845a7e4..8f5bb82ccb08 100755 --- a/tests/test_ci_pbxproj_test_wiring.sh +++ b/tests/test_ci_pbxproj_test_wiring.sh @@ -33,6 +33,20 @@ fi "$LINT" --repo-root "$ROOT_DIR" "$ROOT_DIR/scripts/sync-test-wiring" --repo-root "$ROOT_DIR" --check +# cmuxCLITests is a second test target, so sync-test-wiring (cmuxTests-only) +# cannot see it. Without these the host-free bundle sits outside the repo's +# silent-skip rule: an unwired file there compiles nothing and CI stays green. +# cmuxCLITestSupport/ is compiled into both bundles, so it is checked twice. +# A checkout whose project has no cmuxCLITests target (verify-local's fixture +# repository) has nothing to check; one whose project names the target checks +# it even if a directory is missing, so a deleted directory still fails. +if [ -d "$ROOT_DIR/cmuxCLITests" ] \ + || grep -q 'cmuxCLITests' "$ROOT_DIR/cmux.xcodeproj/project.pbxproj"; then + "$LINT" --repo-root "$ROOT_DIR" --target cmuxCLITests --tests-dir cmuxCLITests + "$LINT" --repo-root "$ROOT_DIR" --target cmuxCLITests --tests-dir cmuxCLITestSupport + "$LINT" --repo-root "$ROOT_DIR" --target cmuxTests --tests-dir cmuxCLITestSupport +fi + # Shared sandbox cleanup. SANDBOX_PARENT="$(mktemp -d)" trap 'rm -rf "$SANDBOX_PARENT"' EXIT diff --git a/tests/test_ci_product_publication.py b/tests/test_ci_product_publication.py index d1904a4e6cb2..fe9443cbf7dd 100644 --- a/tests/test_ci_product_publication.py +++ b/tests/test_ci_product_publication.py @@ -12,7 +12,7 @@ ROOT = Path(__file__).resolve().parents[1] -def condition(expression, *, full_suite, publish="true", unit_suite="false"): +def condition(expression, *, full_suite, publish="true", cli="false", compile_admitted="false", unit_suite="false"): """Evaluate the small boolean subset used by these actual workflow gates.""" expression = expression.removeprefix("${{").removesuffix("}}").strip() expression = expression.replace("!cancelled()", "True") @@ -25,7 +25,9 @@ def value(match): if name.endswith(".outputs.unit_suite") or name == "inputs.unit_suite": return repr(unit_suite) if name.endswith(".outputs.compile_admitted") or name == "inputs.compile_admitted": - return repr("false") + return repr(compile_admitted) + if name.endswith(".outputs.cli") or name == "inputs.cli": + return repr(cli) if name.endswith(".outputs.publish"): return repr(publish) if name == "inputs.unit_in_admission": @@ -52,12 +54,13 @@ def setUpClass(cls): cls.workflow = yaml.safe_load((ROOT / ".github/workflows/ci-macos.yml").read_text()) cls.job = cls.workflow["jobs"]["macos-compile-admission"] - def publication(self, *, full_suite, unit_suite="false", event="pull_request", head="contributor/cmux", repo="manaflow-ai/cmux"): + def publication(self, *, full_suite, cli="false", unit_suite="false", event="pull_request", head="contributor/cmux", repo="manaflow-ai/cmux"): step = next((s for s in self.job["steps"] if s.get("id") == "publish-products"), None) if step is None: return "true" # The previous workflow always packaged and uploaded. self.assertEqual(step["env"], { "PRODUCT_FULL_SUITE": "${{ inputs.full_suite }}", + "PRODUCT_CLI": "${{ inputs.cli }}", "PRODUCT_UNIT_SUITE": "${{ inputs.unit_suite }}", "PRODUCT_EVENT": "${{ github.event_name }}", "PRODUCT_HEAD_REPOSITORY": "${{ github.event.pull_request.head.repo.full_name }}", @@ -66,7 +69,7 @@ def publication(self, *, full_suite, unit_suite="false", event="pull_request", h with tempfile.TemporaryDirectory() as directory: output = Path(directory) / "output" env = dict(os.environ, GITHUB_OUTPUT=str(output), PRODUCT_FULL_SUITE=full_suite, - PRODUCT_UNIT_SUITE=unit_suite, + PRODUCT_CLI=cli, PRODUCT_UNIT_SUITE=unit_suite, PRODUCT_EVENT=event, PRODUCT_HEAD_REPOSITORY=head, PRODUCT_REPOSITORY=repo) subprocess.run(["bash", "-e", "-c", step["run"]], env=env, text=True, capture_output=True, check=True) @@ -75,6 +78,7 @@ def publication(self, *, full_suite, unit_suite="false", event="pull_request", h def test_only_known_compile_only_forks_skip_packaging_and_upload(self): cases = [ ({"full_suite": "false"}, "false"), + ({"full_suite": "false", "cli": "true"}, "true"), ({"full_suite": "false", "unit_suite": "true"}, "true"), ({"full_suite": "true"}, "true"), ({"full_suite": ""}, "true"), @@ -102,7 +106,7 @@ def test_all_actual_artifact_consumers_are_excluded_from_compile_only(self): consumers.append(name) self.assertFalse(condition(job["if"], full_suite="false"), name) self.assertTrue(condition(job["if"], full_suite="true"), name) - self.assertEqual(set(consumers), {"app-host-unit-tests", "tests-build-and-lag"}) + self.assertEqual(set(consumers), {"app-host-unit-tests", "cli-product-tests", "tests-build-and-lag"}) # `unit-ci` admits exactly one consumer under the compile-only policy, # and the product it reads is published for it (see the unit-tier case # in test_only_known_compile_only_forks_skip_packaging_and_upload). @@ -208,7 +212,7 @@ def macos_status(self, compile_admitted, *, full_suite="true", results="success" if s.get("name") == "Check routed macOS jobs" ) needs = {name: {"result": results} for name in self.workflow["jobs"]["macos-status"]["needs"]} - inputs = {"macos": "true", "full_suite": full_suite, "compile_admitted": compile_admitted, "release_build": "true"} + inputs = {"macos": "true", "full_suite": full_suite, "compile_admitted": compile_admitted, "release_build": "true", "cli": "false"} env = {**os.environ, "MACOS_INPUTS": json.dumps(inputs), "MACOS_NEEDS": json.dumps(needs)} return subprocess.run(["bash", "-c", step["run"]], env=env, text=True, capture_output=True) diff --git a/tests/test_ci_test_compilation_cache_seed.sh b/tests/test_ci_test_compilation_cache_seed.sh index 08243f487c7d..4ff4e3f05544 100755 --- a/tests/test_ci_test_compilation_cache_seed.sh +++ b/tests/test_ci_test_compilation_cache_seed.sh @@ -195,6 +195,7 @@ for expected in \ cmux \ cmux-unit \ cmux-numeric-locale \ + cmux-cli-tests \ build-for-testing \ -showBuildTimingSummary \ COMPILATION_CACHE_ENABLE_CACHING=YES \ @@ -206,8 +207,8 @@ for expected in \ exit 1 fi done -if [ "$(grep -c '^---$' "$STUB_XCODEBUILD_ARGS")" -ne 3 ] || [ ! -d "$TMP_DIR/cas" ]; then - echo "FAIL: the build must run all three schemes against an existing CAS directory" +if [ "$(grep -c '^---$' "$STUB_XCODEBUILD_ARGS")" -ne 4 ] || [ ! -d "$TMP_DIR/cas" ]; then + echo "FAIL: the build must run all four schemes against an existing CAS directory" exit 1 fi # `build` compiles no test files: the cmux-unit scheme marks cmuxTests @@ -216,7 +217,7 @@ if grep -Fxq -- build "$STUB_XCODEBUILD_ARGS"; then echo "FAIL: the app-host test product must be compiled with build-for-testing, not build" exit 1 fi -echo "PASS: the build compiles all three schemes for testing with the compilation cache on" +echo "PASS: the build compiles all four schemes for testing with the compilation cache on" if ! grep -Fxq 'build output for cmux' "$TMP_DIR/derived/cmux-build.log" \ || grep -Fq 'build output for cmux-unit' "$TMP_DIR/derived/cmux-build.log"; then echo "FAIL: the warning-budget log must retain only app/UI build output" diff --git a/tests/test_lint_errno_in_test_assertions.py b/tests/test_lint_errno_in_test_assertions.py index abac0e74983c..950afb1907b7 100755 --- a/tests/test_lint_errno_in_test_assertions.py +++ b/tests/test_lint_errno_in_test_assertions.py @@ -253,6 +253,15 @@ def test_scans_only_test_sources(self) -> None: self.assertEqual(status, 0) self.assertIn("1 Swift test files", stdout) + def test_scans_the_host_free_cli_test_sources(self) -> None: + for path in ("cmuxCLITests/ProbeTests.swift", "cmuxCLITestSupport/ProbeSupport.swift"): + with self.subTest(path=path): + status, _, stderr = self.run_lint( + {path: "#expect(read(fd, &byte, 1) == -1)\n#expect(errno == EAGAIN)\n"} + ) + self.assertEqual(status, 1) + self.assertIn(f"{path}:2", stderr) + if __name__ == "__main__": unittest.main()