Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
07008ee
iOS: use appearance-aware active accessory colors
lawrencecchen Sep 19, 2026
dfe77d9
fix: make accessory foreground a UIColor type property
lawrencecchen Sep 19, 2026
2027dca
test: inject tmux SSH transport without mutating Ghostty environment
lawrencecchen Sep 19, 2026
9b33e4c
test: exercise accessory states and keep SSH setup in tests
lawrencecchen Sep 20, 2026
0d67f3c
test: cover secure push failure forwarding from Settings
lawrencecchen Sep 20, 2026
92f3a9a
fix(ios): complete secure push readiness integration
lawrencecchen Sep 20, 2026
b1aae37
test(ios): repair work-queue import and concurrent assertions
lawrencecchen Sep 20, 2026
3258451
test(ios): avoid recursive require macro expansion
lawrencecchen Sep 20, 2026
ab6301d
test: give iOS color and push fixtures realistic state
lawrencecchen Sep 20, 2026
859720b
test: expose namespace lint parsing of Swift literals
lawrencecchen Sep 20, 2026
3fa8103
fix: parse Swift literals before checking namespace ownership
lawrencecchen Sep 20, 2026
4630c97
refactor: give package helpers owners and document callback synchroni…
lawrencecchen Sep 20, 2026
df4b758
refactor: model renderer window visibility as a value
lawrencecchen Sep 20, 2026
0388736
test: cover nested Swift interpolation in namespace lint
lawrencecchen Sep 20, 2026
27571e0
fix: retain Swift string boundaries through interpolation
lawrencecchen Sep 20, 2026
2d95ad7
test: align diagnostic titles and hidden session summaries
lawrencecchen Sep 20, 2026
49177fd
fix: decode diagnostic correlation fields and retain viewer assets
lawrencecchen Sep 20, 2026
8376dcc
fix: remove retired timeout allowance and isolate the button test window
lawrencecchen Sep 20, 2026
57ed345
ci: support focused SwiftPM diagnosis and serialize shell fixtures
lawrencecchen Sep 20, 2026
bf10bc1
fix: match wildcard revoke cleanup and declare coalescing test transp…
lawrencecchen Sep 20, 2026
91b18fc
fix: derive paired Mac presentation identity independently of dial po…
lawrencecchen Sep 20, 2026
f147e33
Merge remote-tracking branch 'origin/main' into feat-ios-active-butto…
lawrencecchen Sep 20, 2026
e4ab1af
fix: isolate settings test ownership and bound cold package verification
lawrencecchen Sep 20, 2026
ac9b7fc
Merge commit '8f6d3c023630fc84cfda232ab611a5fbdd9afe2c' into feat-ios…
lawrencecchen Sep 20, 2026
fc44951
refactor: represent scrollbar presence inputs as a value
lawrencecchen Sep 20, 2026
ca04aa0
test: require attach success before checking workspace selection
lawrencecchen Sep 20, 2026
ad8f694
test: supply stored notification owners and use tagged connection keys
lawrencecchen Sep 20, 2026
d279de4
fix: pass the analytics client ID from its composition owner
lawrencecchen Sep 20, 2026
6c2ac09
Merge commit 'fc7d002eb097c9c07e7bc5d0e1b4a572b9d25d03' into feat-ios…
lawrencecchen Sep 20, 2026
6c919c4
test: complete attach status exchange and bound focused simulator runs
lawrencecchen Sep 20, 2026
015c3fd
test: specify cancellable image deadline behavior
lawrencecchen Sep 20, 2026
3d0239f
fix: give image requests a clock-driven cancellable deadline
lawrencecchen Sep 20, 2026
e0d9248
Merge remote-tracking branch 'origin/main' into feat-ios-active-butto…
lawrencecchen Sep 21, 2026
5fafb41
fix: finish iOS accessory color PR cleanup
lawrencecchen Sep 21, 2026
daeb90d
fix: make iOS release gate capture explicit
lawrencecchen Sep 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 113 additions & 11 deletions .github/workflows/test-ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@ on:
description: "UI test class or class/method, for example cmuxUITests/testSignInPairingAndWorkspaceShell"
required: false
default: ""
swift_package:
description: "Optional SwiftPM package to test instead of the simulator; test_filter selects its suite"
required: false
default: ""
type: choice
options:
- ""
- CMUXMobileCore
- CmuxSyncStore
- CmuxMobilePairedMac
- CmuxMobileChanges
- CmuxMobileShell
- CmuxMobileShellModel
ios_version:
description: "Optional simulator OS version, for example 17.5, 18.5, or 26.3; empty keeps the default"
required: false
Expand Down Expand Up @@ -69,15 +82,15 @@ jobs:
fi
BASE_MERGE_BASE="$(git merge-base "$BASE_SHA" "$HEAD_SHA")"
git diff --name-only "$BASE_MERGE_BASE" "$HEAD_SHA" > /tmp/changed-files.txt
if grep -Eq '^(ios/|Packages/Shared/|Packages/iOS/|Sources/Mobile/|vendor/stack-auth-swift-sdk-prerelease/|scripts/lint-ios-package-conventions\.sh$)' /tmp/changed-files.txt; then
if grep -Eq '^(ios/|Packages/Shared/|Packages/iOS/|Sources/Mobile/|vendor/stack-auth-swift-sdk-prerelease/|scripts/lint-ios-package-conventions\.sh$|scripts/lint_swift_namespaces\.py$|scripts/swift_source_mask\.py$|scripts/tests/test_ios_package_conventions\.py$)' /tmp/changed-files.txt; then
echo "should_run=true" >> "$GITHUB_OUTPUT"
else
echo "No iOS-owned files changed; skipping iOS tests."
echo "should_run=false" >> "$GITHUB_OUTPUT"
fi
# The conventions lint (free-function ban, namespace-type rule, ...)
# covers every package, so it runs for any Packages/ change too.
if grep -Eq '^(ios/|Packages/|Sources/Mobile/|vendor/stack-auth-swift-sdk-prerelease/|scripts/lint-ios-package-conventions\.sh$|scripts/lint-ios-package-conventions-baseline\.txt$|scripts/lint-namespace-types-baseline\.txt$)' /tmp/changed-files.txt; then
if grep -Eq '^(ios/|Packages/|Sources/Mobile/|vendor/stack-auth-swift-sdk-prerelease/|scripts/lint-ios-package-conventions\.sh$|scripts/lint_swift_namespaces\.py$|scripts/swift_source_mask\.py$|scripts/tests/test_ios_package_conventions\.py$|scripts/lint-ios-package-conventions-baseline\.txt$|scripts/lint-namespace-types-baseline\.txt$)' /tmp/changed-files.txt; then
echo "should_lint=true" >> "$GITHUB_OUTPUT"
else
echo "No package-owned files changed; skipping conventions lint."
Expand All @@ -99,6 +112,7 @@ jobs:

- name: Lint iOS package conventions
run: |
python3 scripts/tests/test_ios_package_conventions.py
# Mechanical enforcement of the modular-refactor conventions over the
# iOS line (no singletons / Combine / locks / timer hacks / KVO /
# namespace-enums) plus the repo-wide namespace-type rule (no
Expand All @@ -112,10 +126,23 @@ jobs:
needs: detect-ios-changes
if: >-
${{ needs.detect-ios-changes.outputs.should_run == 'true'
&& !(github.event_name == 'workflow_dispatch' && inputs.test_filter != '') }}
&& (!(github.event_name == 'workflow_dispatch' && inputs.test_filter != '') || inputs.swift_package != '') }}
runs-on: ${{ (!inputs.runner || inputs.runner == 'auto') && (vars.MACOS_RUNNER_IOS || 'blacksmith-6vcpu-macos-26') || inputs.runner }}
timeout-minutes: 10
# Focused runs build the shell and its macros from a cold SwiftPM cache.
timeout-minutes: ${{ inputs.swift_package != '' && 25 || 10 }}
env:
SWIFT_TEST_FILTER: ${{ inputs.test_filter }}
FOCUSED_SWIFT_PACKAGE: ${{ inputs.swift_package }}
steps:
- name: Validate package selection
env:
SELECTED_PACKAGE: ${{ inputs.swift_package }}
run: |
case "$SELECTED_PACKAGE" in
""|CMUXMobileCore|CmuxSyncStore|CmuxMobilePairedMac|CmuxMobileChanges|CmuxMobileShell|CmuxMobileShellModel) ;;
*) echo "::error::Unsupported Swift package: $SELECTED_PACKAGE"; exit 1 ;;
esac

- name: Validate Tart runner identity
if: ${{ startsWith((!inputs.runner || inputs.runner == 'auto') && (vars.MACOS_RUNNER_IOS || 'blacksmith-6vcpu-macos-26') || inputs.runner, 'tart-') }}
env:
Expand Down Expand Up @@ -156,39 +183,105 @@ jobs:
xcodebuild -version

- name: Run CMUXMobileCore package tests
if: ${{ inputs.swift_package == '' || inputs.swift_package == 'CMUXMobileCore' }}
run: |
swift test --package-path Packages/Shared/CMUXMobileCore
set -euo pipefail
args=(--package-path Packages/Shared/CMUXMobileCore)
if [ -n "${FOCUSED_SWIFT_PACKAGE:-}" ]; then
args+=(--no-parallel)
fi
if [ -n "${SWIFT_TEST_FILTER:-}" ]; then
args+=(--filter "$SWIFT_TEST_FILTER")
fi
log_path="$RUNNER_TEMP/CMUXMobileCore-tests.log"
swift test "${args[@]}" 2>&1 | tee "$log_path"
./scripts/ci/require_selected_test_execution.sh "$log_path" "${SWIFT_TEST_FILTER:-CMUXMobileCore}"

- name: Run CmuxSyncStore package tests
if: ${{ inputs.swift_package == '' || inputs.swift_package == 'CmuxSyncStore' }}
run: |
set -euo pipefail
# Local-first sync store (raw SQLite3). Headless SwiftPM, no GhosttyKit
# or app-target dependency, so it runs as a real CI gate here.
swift test --package-path Packages/Shared/CmuxSyncStore
args=(--package-path Packages/Shared/CmuxSyncStore)
if [ -n "${FOCUSED_SWIFT_PACKAGE:-}" ]; then
args+=(--no-parallel)
fi
if [ -n "${SWIFT_TEST_FILTER:-}" ]; then
args+=(--filter "$SWIFT_TEST_FILTER")
fi
log_path="$RUNNER_TEMP/CmuxSyncStore-tests.log"
swift test "${args[@]}" 2>&1 | tee "$log_path"
./scripts/ci/require_selected_test_execution.sh "$log_path" "${SWIFT_TEST_FILTER:-CmuxSyncStore}"

- name: Run CmuxMobilePairedMac package tests
if: ${{ inputs.swift_package == '' || inputs.swift_package == 'CmuxMobilePairedMac' }}
run: |
swift test --package-path Packages/iOS/CmuxMobilePairedMac
set -euo pipefail
args=(--package-path Packages/iOS/CmuxMobilePairedMac)
if [ -n "${FOCUSED_SWIFT_PACKAGE:-}" ]; then
args+=(--no-parallel)
fi
if [ -n "${SWIFT_TEST_FILTER:-}" ]; then
args+=(--filter "$SWIFT_TEST_FILTER")
fi
log_path="$RUNNER_TEMP/CmuxMobilePairedMac-tests.log"
swift test "${args[@]}" 2>&1 | tee "$log_path"
./scripts/ci/require_selected_test_execution.sh "$log_path" "${SWIFT_TEST_FILTER:-CmuxMobilePairedMac}"

- name: Run CmuxMobileChanges package tests
if: ${{ inputs.swift_package == '' || inputs.swift_package == 'CmuxMobileChanges' }}
run: |
swift test --package-path Packages/iOS/CmuxMobileChanges
set -euo pipefail
args=(--package-path Packages/iOS/CmuxMobileChanges)
if [ -n "${FOCUSED_SWIFT_PACKAGE:-}" ]; then
args+=(--no-parallel)
fi
if [ -n "${SWIFT_TEST_FILTER:-}" ]; then
args+=(--filter "$SWIFT_TEST_FILTER")
fi
log_path="$RUNNER_TEMP/CmuxMobileChanges-tests.log"
swift test "${args[@]}" 2>&1 | tee "$log_path"
./scripts/ci/require_selected_test_execution.sh "$log_path" "${SWIFT_TEST_FILTER:-CmuxMobileChanges}"

- name: Run CmuxMobileShell package tests
if: ${{ inputs.swift_package == '' || inputs.swift_package == 'CmuxMobileShell' }}
run: |
set -euo pipefail
# iOS shell replay/liveness regressions live in this package target.
# Keep them under the iOS package gate so PR CI enforces the mobile
# terminal mirror behavior without relying on local-only SwiftPM runs.
swift test --package-path Packages/iOS/CmuxMobileShell
args=(--package-path Packages/iOS/CmuxMobileShell)
if [ -n "${FOCUSED_SWIFT_PACKAGE:-}" ]; then
args+=(--no-parallel)
fi
if [ -n "${SWIFT_TEST_FILTER:-}" ]; then
args+=(--filter "$SWIFT_TEST_FILTER")
fi
log_path="$RUNNER_TEMP/CmuxMobileShell-tests.log"
swift test "${args[@]}" 2>&1 | tee "$log_path"
./scripts/ci/require_selected_test_execution.sh "$log_path" "${SWIFT_TEST_FILTER:-CmuxMobileShell}"

- name: Run CmuxMobileShellModel package tests
if: ${{ inputs.swift_package == '' || inputs.swift_package == 'CmuxMobileShellModel' }}
run: |
set -euo pipefail
# Pure shell-model queue and ordering regressions live in this package.
# Keep them beside the shell package gate so input framing stays covered.
swift test --package-path Packages/iOS/CmuxMobileShellModel
args=(--package-path Packages/iOS/CmuxMobileShellModel)
if [ -n "${FOCUSED_SWIFT_PACKAGE:-}" ]; then
args+=(--no-parallel)
fi
if [ -n "${SWIFT_TEST_FILTER:-}" ]; then
args+=(--filter "$SWIFT_TEST_FILTER")
fi
log_path="$RUNNER_TEMP/CmuxMobileShellModel-tests.log"
swift test "${args[@]}" 2>&1 | tee "$log_path"
./scripts/ci/require_selected_test_execution.sh "$log_path" "${SWIFT_TEST_FILTER:-CmuxMobileShellModel}"

ios-simulator:
needs: detect-ios-changes
if: ${{ needs.detect-ios-changes.outputs.should_run == 'true' }}
if: ${{ needs.detect-ios-changes.outputs.should_run == 'true' && inputs.swift_package == '' }}
# Blacksmith, not the bare `macos-26` label (which our self-hosted fleet
# carries). iOS simulator XCTest runs inside the Simulator, not as a
# foregrounded Mac app, so the Blacksmith macOS foreground limitation that
Expand Down Expand Up @@ -421,6 +514,15 @@ jobs:
if [ -n "${TEST_FILTER:-}" ]; then
if [[ "$TEST_FILTER" == cmuxUITests/* ]]; then
XCODEBUILD_ARGS+=(-testPlan cmux-ui)
else
# Keep focused package assertions bounded on virtualized iOS
# runners; failed sysdiagnose collection can otherwise take 10m.
XCODEBUILD_ARGS+=(
-collect-test-diagnostics never
-test-timeouts-enabled YES
-default-test-execution-time-allowance 60
-maximum-test-execution-time-allowance 120
)
fi
XCODEBUILD_ARGS+=(-only-testing:"$TEST_FILTER")
else
Expand Down
4 changes: 2 additions & 2 deletions CLI/CMUXCLI+RestoreAdmission.swift
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,12 @@ extension CMUXCLI {
&& error.v2Retryable
}

/// `AgentRestoreAdmissionRetry.response` with the CLI's error classifier.
/// `AgentRestoreAdmissionRetry().response` with the CLI's error classifier.
static func response(
onRetry: (Int) -> Void = { _ in },
sending send: () throws -> [String: Any]
) throws -> [String: Any] {
try AgentRestoreAdmissionRetry.response(
try AgentRestoreAdmissionRetry().response(
onRetry: onRetry,
isRetryable: isRetryable,
sending: send
Expand Down
2 changes: 1 addition & 1 deletion CLI/CMUXCLI+TmuxCompatSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ extension CMUXCLI {
}

func prependPathEntries(_ newEntries: [String], to currentPath: String?) -> String {
CmuxPathEnvironment.prependingUniqueEntries(newEntries, to: currentPath)
CmuxPathEnvironment().prependingUniqueEntries(newEntries, to: currentPath)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ public import Foundation
/// Shared interpretation of HTTP `Retry-After` for every automatic client
/// retry owner. A server directive is a minimum wait, never a replacement for
/// a longer local backoff.
public enum CmxRetryAfterPolicy {
public static let defaultRateLimitSeconds = 60
public struct CmxRetryAfterPolicy: Sendable {
public init() {}

public let defaultRateLimitSeconds = 60

/// Parses either delta-seconds or an HTTP-date. Invalid, zero, and expired
/// directives return nil so callers can apply their documented fallback.
public static func seconds(
public func seconds(
from value: String?,
now: Date = Date()
) -> Int? {
Expand All @@ -34,7 +36,7 @@ public enum CmxRetryAfterPolicy {
return nil
}

public static func seconds(
public func seconds(
from response: HTTPURLResponse,
now: Date = Date(),
defaultSeconds: Int? = nil
Expand All @@ -45,7 +47,7 @@ public enum CmxRetryAfterPolicy {
) ?? defaultSeconds.flatMap { $0 > 0 ? $0 : nil }
}

public static func delay(
public func delay(
localSeconds: TimeInterval,
retryAfterSeconds: Int?
) -> TimeInterval {
Expand All @@ -54,15 +56,15 @@ public enum CmxRetryAfterPolicy {

/// Floating point rounds Int.max up to 2^63. Saturate that conversion
/// without shortening ordinary server directives or trapping on restore.
public static func roundedUpSeconds(_ seconds: TimeInterval) -> Int {
public func roundedUpSeconds(_ seconds: TimeInterval) -> Int {
guard seconds > 0 else { return 0 }
let rounded = seconds.rounded(.up)
return rounded >= Double(Int.max) ? Int.max : Int(rounded)
}

/// Sleep in representable chunks while preserving the entire requested
/// wait. Converting an arbitrary server delay to UInt64 nanoseconds traps.
public static func sleep(
public func sleep(
seconds: TimeInterval,
using sleeper: @Sendable (TimeInterval) async throws -> Void = {
try await Task<Never, Never>.sleep(for: .seconds($0))
Expand Down Expand Up @@ -121,7 +123,7 @@ public actor CmxRetryAfterGate {

public func remainingSeconds() -> Int? {
guard let deadline else { return nil }
let remaining = CmxRetryAfterPolicy.roundedUpSeconds(deadline - now())
let remaining = CmxRetryAfterPolicy().roundedUpSeconds(deadline - now())
if remaining <= 0 {
self.deadline = nil
return nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@ import Foundation
/// The git SHA and dev tag are signed bundle metadata, not runtime input. The
/// helper keeps iOS and macOS reports comparable and applies the same bounded
/// sanitization at their shared boundary.
public enum DiagnosticBuildStamp {
extension DiagnosticReport {
/// Returns a bounded `name version (build) tag sha` stamp from bundle data.
public static func make(
public static func buildStamp(
infoDictionary: [String: Any]?,
fallbackName: String = "cmux"
) -> String {
let info = infoDictionary ?? [:]
let name = nonEmptyString(info["CFBundleName"]) ?? fallbackName
let version = nonEmptyString(info["CFBundleShortVersionString"]) ?? "?"
let build = nonEmptyString(info["CFBundleVersion"]) ?? "?"
let tag = nonEmptyString(info["CMUXDevTag"])
let sha = nonEmptyString(info["CMUXGitSHA"]).map {
let name = buildStampNonEmptyString(info["CFBundleName"]) ?? fallbackName
let version = buildStampNonEmptyString(info["CFBundleShortVersionString"]) ?? "?"
let build = buildStampNonEmptyString(info["CFBundleVersion"]) ?? "?"
let tag = buildStampNonEmptyString(info["CMUXDevTag"])
let sha = buildStampNonEmptyString(info["CMUXGitSHA"]).map {
String($0.prefix(12))
}

Expand All @@ -30,7 +30,7 @@ public enum DiagnosticBuildStamp {
return DiagnosticReport.sanitizeBuildStamp(result)
}

private static func nonEmptyString(_ value: Any?) -> String? {
private static func buildStampNonEmptyString(_ value: Any?) -> String? {
guard let value = value as? String else { return nil }
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
Expand Down
Loading
Loading