Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 14 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ jobs:
- name: Validate Python test harness syntax
run: git ls-files 'tests/*.py' 'tests_v2/*.py' 'scripts/*.py' | xargs python3 -m py_compile

- name: Set up Python 3.9 for nightly prune compatibility
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.9"

- name: Validate nightly prune Python compatibility
run: PYTHON_BIN=python3.9 bash ./tests/test_ci_nightly_prune_python_compat.sh

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mid-job Python downgrades later steps

Low Severity

Adding actions/setup-python with 3.9 in the middle of workflow-guard-tests makes python3 point at 3.9 for every later step in that job, not only the nightly prune check. Steps that used to run on the runner’s default interpreter now run on 3.9 instead.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ce14d5d. Configure here.


- name: Validate activation benchmark scrollback sizing
run: python3 tests/test_perf_activation_scrollback_sizing.py

Expand Down Expand Up @@ -345,7 +353,8 @@ jobs:
run: |
set -euo pipefail
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
xcodebuild -project cmux.xcodeproj -scheme cmux-unit -configuration Debug \
scripts/ci/run-app-host-xcodebuild.sh \
-project cmux.xcodeproj -scheme cmux-unit -configuration Debug \
-derivedDataPath "$CMUX_DERIVED_DATA_PATH" \
-clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \
-disableAutomaticPackageResolution \
Expand All @@ -367,7 +376,8 @@ jobs:
# unconditional gate.
set -euo pipefail
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
xcodebuild -project cmux.xcodeproj -scheme cmux-unit -configuration Debug \
scripts/ci/run-app-host-xcodebuild.sh \
-project cmux.xcodeproj -scheme cmux-unit -configuration Debug \
-derivedDataPath "$CMUX_DERIVED_DATA_PATH" \
-clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \
-disableAutomaticPackageResolution \
Expand All @@ -386,7 +396,8 @@ jobs:
# regression an unconditional gate.
set -euo pipefail
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
xcodebuild -project cmux.xcodeproj -scheme cmux-unit -configuration Debug \
scripts/ci/run-app-host-xcodebuild.sh \
-project cmux.xcodeproj -scheme cmux-unit -configuration Debug \
-derivedDataPath "$CMUX_DERIVED_DATA_PATH" \
-clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \
-disableAutomaticPackageResolution \
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
public import Darwin
public import Foundation

extension SocketControlSettings {
/// The default socket path for the current build variant (before override handling).
public static func defaultSocketPath(
bundleIdentifier: String?,
environment: [String: String] = ProcessInfo.processInfo.environment,
isDebugBuild: Bool,
currentUserID: uid_t = getuid(),
probeStableDefaultPathEntry: (String) -> StableDefaultSocketPathEntry = inspectStableDefaultSocketPathEntry
) -> String {
if isDebugBuild,
isBareDebugBundleIdentifier(
bundleIdentifier,
baseDebugBundleIdentifier: baseDebugBundleIdentifier
),
launchTag(environment: environment) == nil,
environment["CMUX_SOCKET_PATH"]?.isEmpty != false,
let xctestPath = xctestDebugSocketPath(environment: environment) {
return xctestPath
}

return SocketPathMarkerFiles.defaultSocketPath(
bundleIdentifier: bundleIdentifier,
environment: environment,
isDebugBuild: isDebugBuild,
stableSocketPath: resolvedStableDefaultSocketPath(
currentUserID: currentUserID,
probeStableDefaultPathEntry: probeStableDefaultPathEntry
),
baseDebugBundleIdentifier: baseDebugBundleIdentifier
)
}
}

private func isBareDebugBundleIdentifier(
_ bundleIdentifier: String?,
baseDebugBundleIdentifier: String
) -> Bool {
bundleIdentifier?.trimmingCharacters(in: .whitespacesAndNewlines) == baseDebugBundleIdentifier
}

private func xctestDebugSocketPath(environment: [String: String]) -> String? {
let indicators = [
"XCTestSessionIdentifier",
"XCTestConfigurationFilePath",
"XCTestBundlePath",
"XCInjectBundle",
"XCInjectBundleInto",
"DYLD_INSERT_LIBRARIES",
]
guard let source = indicators.compactMap({ key -> String? in
guard let value = environment[key]?.trimmingCharacters(in: .whitespacesAndNewlines),
!value.isEmpty else {
return nil
}
if key == "DYLD_INSERT_LIBRARIES",
!value.contains("libXCTest") {
return nil
}
return value
}).first else {
return nil
}

let hash = source.utf8.reduce(UInt64(0xcbf29ce484222325)) { partial, byte in
(partial ^ UInt64(byte)) &* 0x100000001b3
}
return "/tmp/cmux-xctest-\(String(hash, radix: 16)).sock"
}
Original file line number Diff line number Diff line change
Expand Up @@ -344,26 +344,6 @@ public struct SocketControlSettings {
}
}

/// The default socket path for the current build variant (before override handling).
public static func defaultSocketPath(
bundleIdentifier: String?,
environment: [String: String] = ProcessInfo.processInfo.environment,
isDebugBuild: Bool,
currentUserID: uid_t = getuid(),
probeStableDefaultPathEntry: (String) -> StableDefaultSocketPathEntry = inspectStableDefaultSocketPathEntry
) -> String {
SocketPathMarkerFiles.defaultSocketPath(
bundleIdentifier: bundleIdentifier,
environment: environment,
isDebugBuild: isDebugBuild,
stableSocketPath: resolvedStableDefaultSocketPath(
currentUserID: currentUserID,
probeStableDefaultPathEntry: probeStableDefaultPathEntry
),
baseDebugBundleIdentifier: baseDebugBundleIdentifier
)
}

/// The per-user stable socket path (`cmux-<uid>.sock` in ``CmuxStateDirectory``, `/tmp` fallback).
public static func userScopedStableSocketPath(currentUserID: uid_t = getuid()) -> String {
stableSocketDirectoryURL()?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,95 @@ import CmuxSettings
)
#expect(path == "/tmp/cmux-custom.sock")
}

@Test func bareDebugXCTestLaunchUsesScopedSocketFallback() {
let environment = [
"XCTestConfigurationFilePath": "/tmp/Test-cmux-unit-2026.06.17.xctestconfiguration",
]
let path = SocketControlSettings.socketPath(
environment: environment,
bundleIdentifier: "com.cmuxterm.app.debug",
isDebugBuild: true,
currentUserID: 501,
probeStableDefaultPathEntry: { _ in .missing }
)
let defaultPath = SocketControlSettings.defaultSocketPath(
bundleIdentifier: "com.cmuxterm.app.debug",
environment: environment,
isDebugBuild: true,
currentUserID: 501,
probeStableDefaultPathEntry: { _ in .missing }
)
#expect(path.hasPrefix("/tmp/cmux-xctest-"))
#expect(path.hasSuffix(".sock"))
#expect(path != "/tmp/cmux-debug.sock")
#expect(path == defaultPath)
}

@Test func explicitSocketOverrideStillWinsUnderXCTest() {
let path = SocketControlSettings.socketPath(
environment: [
"CMUX_SOCKET_PATH": "/tmp/cmux-forced.sock",
"XCTestConfigurationFilePath": "/tmp/Test-cmux-unit-2026.06.17.xctestconfiguration",
],
bundleIdentifier: "com.cmuxterm.app.debug",
isDebugBuild: true,
currentUserID: 501,
probeStableDefaultPathEntry: { _ in .missing }
)
#expect(path == "/tmp/cmux-forced.sock")
}

@Test func dyldOnlyXCTestLaunchUsesScopedSocketFallback() {
let path = SocketControlSettings.socketPath(
environment: [
"DYLD_INSERT_LIBRARIES": "/Applications/Xcode.app/Contents/Developer/usr/lib/libXCTestSwiftSupport.dylib",
],
bundleIdentifier: "com.cmuxterm.app.debug",
isDebugBuild: true,
currentUserID: 501,
probeStableDefaultPathEntry: { _ in .missing }
)
#expect(path.hasPrefix("/tmp/cmux-xctest-"))
#expect(path.hasSuffix(".sock"))
#expect(path != "/tmp/cmux-debug.sock")
}

@Test func xctestSocketFallbackHashesFullPath() {
let first = SocketControlSettings.socketPath(
environment: [
"XCTestConfigurationFilePath": "/tmp/first/Test-cmux-unit.xctestconfiguration",
],
bundleIdentifier: "com.cmuxterm.app.debug",
isDebugBuild: true,
currentUserID: 501,
probeStableDefaultPathEntry: { _ in .missing }
)
let second = SocketControlSettings.socketPath(
environment: [
"XCTestConfigurationFilePath": "/tmp/second/Test-cmux-unit.xctestconfiguration",
],
bundleIdentifier: "com.cmuxterm.app.debug",
isDebugBuild: true,
currentUserID: 501,
probeStableDefaultPathEntry: { _ in .missing }
)
#expect(first.hasPrefix("/tmp/cmux-xctest-"))
#expect(second.hasPrefix("/tmp/cmux-xctest-"))
#expect(first != second)
}

@Test func taggedDebugXCTestLaunchStillUsesTaggedSocket() {
let path = SocketControlSettings.socketPath(
environment: [
"CMUX_TAG": "ci-split-theme",
"XCTestConfigurationFilePath": "/tmp/Test-cmux-unit-2026.06.17.xctestconfiguration",
],
bundleIdentifier: "com.cmuxterm.app.debug",
isDebugBuild: true,
currentUserID: 501,
probeStableDefaultPathEntry: { _ in .missing }
)
#expect(path == "/tmp/cmux-debug-ci-split-theme.sock")
}
}
48 changes: 48 additions & 0 deletions scripts/ci/run-app-host-xcodebuild.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
set -euo pipefail

if [ "$#" -eq 0 ]; then
echo "usage: $0 <xcodebuild args...>" >&2
exit 2
fi
log_dir="${RUNNER_TEMP:-/tmp}"
log_stem="${log_dir%/}/cmux-app-host-xcodebuild-${CMUX_TAG:-untagged}"
max_attempts="${CMUX_APP_HOST_XCODEBUILD_ATTEMPTS:-2}"

attempt=1
while [ "$attempt" -le "$max_attempts" ]; do
log_path="${log_stem}-attempt-${attempt}.log"
set +e
scripts/ci/xcodebuild_noninteractive.py xcodebuild "$@" 2>&1 | tee "$log_path"
status=${PIPESTATUS[0]}
set -e

if grep -Fq 'path = "/tmp/cmux-debug.sock"' "$log_path"; then
echo "FAIL: app-host used default debug socket instead of an XCTest-scoped socket" >&2
exit 1
fi

if grep -Fq 'SocketControlServer: Listening on /tmp/cmux-debug.sock' "$log_path"; then
echo "FAIL: app-host listener used default debug socket instead of an XCTest-scoped socket" >&2
exit 1
fi

if [ "$status" -ne 0 ]; then
if [ "$attempt" -lt "$max_attempts" ] && grep -Fq 'The test runner hung before establishing connection.' "$log_path"; then
echo "Retrying app-host xcodebuild after XCTest startup hang (attempt $attempt/$max_attempts)" >&2
pkill -x "cmux DEV" || true
attempt=$((attempt + 1))
continue
fi
exit "$status"
fi

if ! grep -Eq 'SocketControlServer: Listening on |message = "socket.listener.start"' "$log_path"; then
echo "FAIL: app-host xcodebuild output did not include socket listener evidence" >&2
exit 1
fi

exit 0
done

exit 1
2 changes: 2 additions & 0 deletions scripts/prune_nightly_release_assets.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#!/usr/bin/env python3

from __future__ import annotations

import argparse
import json
import subprocess
Expand Down
23 changes: 23 additions & 0 deletions tests/test_ci_nightly_prune_python_compat.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
SCRIPT="$ROOT_DIR/scripts/prune_nightly_release_assets.py"
PYTHON_BIN="${PYTHON_BIN:-python3}"

"$PYTHON_BIN" -m py_compile "$SCRIPT"
"$PYTHON_BIN" - "$SCRIPT" <<'PY'
import importlib.util
import pathlib
import sys

script = pathlib.Path(sys.argv[1])
spec = importlib.util.spec_from_file_location("nightly_prune_compat", script)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
sys.modules[spec.name] = module
spec.loader.exec_module(module)
assert callable(module.load_release)
PY

echo "PASS: nightly prune script is compatible with older macOS runner Python"
Loading