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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ jobs:
- name: Validate auxiliary window close shortcut lint
run: ./tests/test_ci_auxiliary_window_close_shortcuts.sh

- name: Validate pbxproj test-wiring lint
run: ./tests/test_ci_pbxproj_test_wiring.sh

# Paused: stale-base merge races (two PRs each fitting the budget can
# overshoot when merged back-to-back without rebasing). CodeRabbit and
# Greptile already flag large-file growth on PRs. Re-enable by uncommenting
Expand Down
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,8 @@ The app has a **Debug** menu in the macOS menu bar (only in DEBUG builds). Use i
- **Shortcut policy:** Every new cmux-owned keyboard shortcut must be added to `KeyboardShortcutSettings`, visible/editable in Settings, supported in `~/.config/cmux/cmux.json`, and documented in the keyboard shortcut and configuration docs.
- **Snapshot boundary for list subtrees.** In any SwiftUI panel whose `body` contains a `LazyVStack` / `LazyHStack` / `List` / `ForEach` of rows, no view below that boundary may hold a reference to an `ObservableObject` / `@Observable` store (no `@ObservedObject`, `@EnvironmentObject`, `@StateObject`, `@Bindable`, or even a plain `let store: SomeStore` property). Rows and drop-gaps receive immutable value snapshots plus closure action bundles only. Violating this reintroduces the "orthogonal @Published change invalidates every row and thrashes `LazyLayoutViewCache`" class of 100% CPU spin loop that hit the Sessions panel and the workspace sidebar (https://github.com/manaflow-ai/cmux/issues/2586). Reference pattern: `IndexSectionActions` / `SectionGapActions` / `SessionSearchFn` in `Sources/SessionIndexView.swift`.
- **No state mutation inside view-body computations.** A function called from `body` (directly or through a helper) must not write `@Published` state, schedule a `Task { @MainActor in store.x = … }`, or `DispatchQueue.main.async` a store write. That creates a re-render feedback loop and pegs the main thread (same root-cause family as the snapshot-boundary rule). State-changing work triggered by "new data appeared" belongs in a `reload()` completion, a `didSet`, or a property-observer — never in the projection that feeds `ForEach`.
- **Foundation, SwiftUI, AttributeGraph, and WebKit semantics change silently between macOS major versions.** A function that "obviously" returns the same value on every macOS is not a reliable assumption. Concrete case from https://github.com/manaflow-ai/cmux/issues/4529: `URL(fileURLWithPath: "/").deletingLastPathComponent().path` returns `"/.."` on macOS 14 and 15 but `"/"` on macOS 26 — Apple silently fixed the underlying CFURL normalization. The repo's `macos-26` CI and every maintainer's dev machine were on the fixed-behavior side; every reporter on the issue was on the broken side. Always test on the reporter's macOS before declaring a user-reported repro disproven. AWS M4 Pro builders (`cmux-aws-mac`, `cmux-aws-m4pro`, `aws-m4pro-1..6`) are pre-provisioned on macOS 15.7.4 and the preferred empirical-repro path; see the `regression-hunt` skill in the cmuxterm-hq sibling repo for the full playbook.
- **Test files in `cmuxTests/` must be wired into `cmux.xcodeproj/project.pbxproj`.** A `.swift` file added to the worktree without a matching `PBXFileReference` + `PBXSourcesBuildPhase` entry is silently ignored by Xcode and never compiles or runs on CI. Both `xcodebuild test -only-testing:cmuxTests/<TestClass>` and bot reviews pass with "Executed 0 tests" — so the missing wiring is indistinguishable from a clean two-commit red/green regression test until a real user hits the bug. The `workflow-guard-tests` job runs `./scripts/lint-pbxproj-test-wiring.sh` to catch this at PR time; surfaced during the https://github.com/manaflow-ai/cmux/issues/4529 investigation against https://github.com/manaflow-ai/cmux/pull/4536. Add via Xcode (drag the file into the cmuxTests target) or hand-edit the four pbxproj entries; reference any wired sibling like `TabManagerUnitTests.swift` as a template.

## Test quality policy

Expand Down
8 changes: 8 additions & 0 deletions cmux.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,8 @@
B7F9A602B7F9A602B7F9A602 /* WindowChromeMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7F9A603B7F9A603B7F9A603 /* WindowChromeMetrics.swift */; };
B7F9A604B7F9A604B7F9A604 /* RightSidebarChromeStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7F9A605B7F9A605B7F9A605 /* RightSidebarChromeStyle.swift */; };
B6BF3DC98DB1495E57900199 /* TabManagerUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42092CDB2109E250F7F2A76E /* TabManagerUnitTests.swift */; };
8A3392FE64E0605D942213D1 /* SessionIndexViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42D69572C8D276745E502B94 /* SessionIndexViewTests.swift */; };
385C6BA7E78DB87460E5D930 /* SidebarMarkdownRendererTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1C3F1DBF6BF5D7223C4A30C /* SidebarMarkdownRendererTests.swift */; };
B8F266236A1A3D9A45BD840F /* SidebarResizeUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 818DBCD4AB69EB72573E8138 /* SidebarResizeUITests.swift */; };
C0DE34020000000000000005 /* HelpMenuUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE34020000000000000006 /* HelpMenuUITests.swift */; };
B8F266246A1A3D9A45BD840F /* SidebarHelpMenuUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F266256A1A3D9A45BD840F /* SidebarHelpMenuUITests.swift */; };
Expand Down Expand Up @@ -607,6 +609,8 @@
A9D9000000000000000F0016 /* markdown-viewer */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = folder; path = "markdown-viewer"; sourceTree = "<group>"; };
FEEDC0DEC0DEC0DEC0DE0002 /* FeedCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedCoordinatorTests.swift; sourceTree = "<group>"; };
42092CDB2109E250F7F2A76E /* TabManagerUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabManagerUnitTests.swift; sourceTree = "<group>"; };
42D69572C8D276745E502B94 /* SessionIndexViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionIndexViewTests.swift; sourceTree = "<group>"; };
F1C3F1DBF6BF5D7223C4A30C /* SidebarMarkdownRendererTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarMarkdownRendererTests.swift; sourceTree = "<group>"; };
43430FA5929121E2EAAB3091 /* AuthEnvironment.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AuthEnvironment.swift; sourceTree = "<group>"; };
C3677001000000000000002 /* CmuxSSHURLRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxSSHURLRequestTests.swift; sourceTree = "<group>"; };
491751CE2321474474F27DCF /* TerminalControllerSocketSecurityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalControllerSocketSecurityTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1628,6 +1632,8 @@
B09C007F42697761B5F1A2AB /* OmnibarAndToolsTests.swift */,
D2C075029771815DD5DA1332 /* NotificationAndMenuBarTests.swift */,
42092CDB2109E250F7F2A76E /* TabManagerUnitTests.swift */,
42D69572C8D276745E502B94 /* SessionIndexViewTests.swift */,
F1C3F1DBF6BF5D7223C4A30C /* SidebarMarkdownRendererTests.swift */,
14A7DC53B9CA33BE2A421711 /* WorkspacePullRequestSidebarTests.swift */,
FEEDC0DEC0DEC0DEC0DE0002 /* FeedCoordinatorTests.swift */,
1D301919B10F22B8708E8883 /* WorkspaceManualUnreadTests.swift */,
Expand Down Expand Up @@ -2415,6 +2421,8 @@
C2B6A97D1F2E4C71A8B9D001 /* BrowserOmnibarPerformanceSupportTests.swift in Sources */,
734F49D37E543DD01C2F4FEF /* NotificationAndMenuBarTests.swift in Sources */,
B6BF3DC98DB1495E57900199 /* TabManagerUnitTests.swift in Sources */,
8A3392FE64E0605D942213D1 /* SessionIndexViewTests.swift in Sources */,
385C6BA7E78DB87460E5D930 /* SidebarMarkdownRendererTests.swift in Sources */,
DCC935C5F55C1DCB33E25521 /* WorkspacePullRequestSidebarTests.swift in Sources */,
FEEDC0DEC0DEC0DEC0DE0001 /* FeedCoordinatorTests.swift in Sources */,
0F2C25F9170130F8DC09DD1B /* WorkspaceManualUnreadTests.swift in Sources */,
Expand Down
5 changes: 5 additions & 0 deletions cmuxTests/SessionIndexViewTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,11 @@ private extension SessionAgent {
return .rovodev
case .hermesAgent:
return .hermesAgent(source: nil, model: nil, hermesHome: nil)
case .registered:
// Registered (Vault) agents aren't exercised by these tests; if a
// future test reaches this branch, point them at the missing
// helper instead of silently returning a misleading default.
fatalError("defaultSpecificsForTesting does not support .registered SessionAgent; extend the helper when adding registered-agent coverage")
}
}
}
Expand Down
169 changes: 169 additions & 0 deletions scripts/lint-pbxproj-test-wiring.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
#!/usr/bin/env bash
# Lint: every Swift file under cmuxTests/ must be wired into
# cmux.xcodeproj/project.pbxproj.
#
# A test file added to the worktree but not registered as a PBXFileReference +
# PBXSourcesBuildPhase entry in project.pbxproj is silently ignored by Xcode and
# never compiles or runs on CI. Both bot reviews and
# `xcodebuild test -only-testing:cmuxTests/<TestClass>` pass with
# "Executed 0 tests" — so missing wiring is indistinguishable from a passing
# regression test until a real user hits the bug the test was supposed to catch.
#
# Originally surfaced during the https://github.com/manaflow-ai/cmux/issues/4529
# investigation, where SessionIndexJSONLStreamTests.swift on
# https://github.com/manaflow-ai/cmux/pull/4536 looked like a clean two-commit
# red/green test fix but never actually ran on CI.
#
# Usage:
# ./scripts/lint-pbxproj-test-wiring.sh [--repo-root <path>]
#
# Exit codes:
# 0 — all test files wired correctly (or no test files present)
# 1 — at least one test file is missing pbxproj wiring
# 2 — invocation error (e.g. project.pbxproj not found)

set -euo pipefail

REPO_ROOT=""
while [ "$#" -gt 0 ]; do
case "$1" in
--repo-root)
REPO_ROOT="$2"
shift 2
;;
-h|--help)
sed -n '1,25p' "$0" | sed 's/^# *//'
exit 0
;;
*)
echo "Unknown argument: $1" >&2
exit 2
;;
esac
done

if [ -z "$REPO_ROOT" ]; then
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd -P)"
fi

PBXPROJ="$REPO_ROOT/cmux.xcodeproj/project.pbxproj"
TESTS_DIR="$REPO_ROOT/cmuxTests"

if [ ! -f "$PBXPROJ" ]; then
echo "lint-pbxproj-test-wiring: not found: $PBXPROJ" >&2
echo " (run from the cmux repo root or pass --repo-root)" >&2
exit 2
fi
if [ ! -d "$TESTS_DIR" ]; then
echo "lint-pbxproj-test-wiring: not found: $TESTS_DIR" >&2
exit 2
fi

# Locate the cmuxTests PBXNativeTarget and resolve its Sources build phase
# UUID. We then slice out just that build phase block and look for files inside
# it — which is exactly the set of files Xcode compiles into cmuxTests.
#
# Targeting the cmuxTests Sources phase specifically (instead of the whole
# pbxproj) catches three failure modes:
# 1. File missing entirely (no `<file>.swift in Sources` anywhere).
# 2. File has a PBXFileReference + group child but no PBXBuildFile /
# Sources phase entry (in the project tree but not a member of any
# target).
# 3. File is a member of the wrong target (e.g. cmuxUITests or cmux). Its
# `<file>.swift in Sources` lines exist in the pbxproj, so a global grep
# would pass, but they are not inside the cmuxTests Sources block.
# `/* cmuxTests */ = {` appears twice in a typical pbxproj: once for the
# PBXGroup that holds the test files, and once for the PBXNativeTarget. We
# 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 = "" }
capture { buf = buf $0 "\n" }
capture && /^[[:space:]]*\};[[:space:]]*$/ {
if (buf ~ /isa = PBXNativeTarget;/) {
print buf
exit
}
capture = 0
buf = ""
}
' "$PBXPROJ")"

if [ -z "$tests_target_block" ]; then
echo "lint-pbxproj-test-wiring: could not locate cmuxTests PBXNativeTarget in $PBXPROJ" >&2
exit 2
fi

# Xcode UUIDs are conventionally 24 uppercase hex chars, but hand-edited
# pbxprojs occasionally use 24-char identifiers that include other uppercase
# letters or digits. Match both.
tests_sources_uuid="$(printf '%s\n' "$tests_target_block" \
| grep -oE '[A-Z0-9]{24} /\* Sources \*/' \
| head -n 1 \
| awk '{print $1}')"

if [ -z "$tests_sources_uuid" ]; then
echo "lint-pbxproj-test-wiring: cmuxTests target has no Sources build phase reference" >&2
exit 2
fi

# Slice the PBXSourcesBuildPhase block whose UUID matches the cmuxTests
# target's Sources phase reference. The block begins with the UUID/Sources
# header and ends at the next standalone "};" line.
tests_sources_block="$(awk -v uuid="$tests_sources_uuid" '
$0 ~ uuid " /\\* Sources \\*/ = \\{" { capture = 1 }
capture { print }
capture && /^[[:space:]]*\};[[:space:]]*$/ { exit }
' "$PBXPROJ")"

if [ -z "$tests_sources_block" ]; then
echo "lint-pbxproj-test-wiring: could not slice cmuxTests Sources build phase (uuid=$tests_sources_uuid)" >&2
exit 2
fi

missing=()
checked=0

while IFS= read -r -d '' file; do
base="$(basename "$file")"
checked=$((checked + 1))
# Look for the file's entry inside the cmuxTests Sources phase only.
#
# Match the full PBX comment `/* <base> in Sources */` as a fixed string
# (grep -F) so we don't get a false positive when `<base>` is a substring
# of another wired file. Example: `SearchIndexTests.swift` is a suffix of
# `SettingsSearchIndexTests.swift`; without these anchors, removing the
# former from the Sources phase would still match the latter and the lint
# would pass.
if ! printf '%s\n' "$tests_sources_block" | grep -qF -- "/* $base in Sources */"; then
missing+=("$base")
fi
done < <(find "$TESTS_DIR" -maxdepth 1 -type f -name '*.swift' -print0)

if [ "${#missing[@]}" -eq 0 ]; then
echo "lint-pbxproj-test-wiring: ok (checked $checked test files)"
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"
for entry in "${missing[@]}"; do
echo " - $entry"
done
echo ""
echo "Each cmuxTests/<file>.swift must be wired into cmux.xcodeproj/project.pbxproj"
echo "as a full target member of cmuxTests:"
echo " 1. a PBXBuildFile entry (line ends with '<file>.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 " (line ends with '<file>.swift in Sources */,')"
echo ""
echo "This lint slices the cmuxTests 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 "flagged here."
echo ""
echo "Add via Xcode (drag the file into the cmuxTests target) or hand-edit"
echo "the four blocks (see any wired sibling test as a template)."
exit 1
Loading
Loading