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
51 changes: 51 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,57 @@ jobs:
${{ runner.temp }}/fm-herdr/default-server.log
if-no-files-found: warn

tests-herdr-macos-focus:
name: Behavior tests (Herdr macOS focus)
runs-on: macos-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Require macOS focus and Herdr tools
run: |
set -eu
command -v jq >/dev/null || { echo "::error::jq is required"; exit 1; }
command -v python3 >/dev/null || { echo "::error::python3 is required"; exit 1; }
command -v swift >/dev/null || { echo "::error::swift is required"; exit 1; }
command -v osascript >/dev/null || { echo "::error::osascript is required"; exit 1; }
- name: Install pinned Herdr and Treehouse
run: |
set -eu
bin/fm-install-herdr.sh "$RUNNER_TEMP/bin"
bin/fm-install-treehouse.sh "$RUNNER_TEMP/bin"
echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
- name: Start default Herdr session for fleet-state tripwire
run: |
set -eu
mkdir -p "$RUNNER_TEMP/fm-herdr-macos"
nohup herdr server >"$RUNNER_TEMP/fm-herdr-macos/default-server.log" 2>&1 &
echo $! >"$RUNNER_TEMP/fm-herdr-macos/default-server.pid"
attempt=0
while [ "$attempt" -lt 150 ]; do
running=$(herdr status --json 2>/dev/null | jq -r '.server.running // false' || echo false)
if [ "$running" = true ]; then
echo "default Herdr session is running"
exit 0
fi
sleep 0.2
attempt=$((attempt + 1))
done
echo "::error::default Herdr server did not become ready"
cat "$RUNNER_TEMP/fm-herdr-macos/default-server.log" || true
exit 1
- name: Run projected spawn macOS activation regression
env:
FM_REQUIRE_MACOS_FOCUS_AUDIT: 1
run: tests/fm-backend-herdr-presentation-e2e.test.sh
- name: Stop default Herdr server
if: always()
run: |
if [ -f "$RUNNER_TEMP/fm-herdr-macos/default-server.pid" ]; then
kill "$(cat "$RUNNER_TEMP/fm-herdr-macos/default-server.pid")" 2>/dev/null || true
fi

# Aggregate per-lane timing into one summary artifact for critical-path review.
tests-timing-aggregate:
name: Behavior timing aggregate
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,11 @@ Its header and `--help` own the flags, family labels, lanes, and changed-file ma
Portable shard balance evidence lives in `docs/fm-test-portable-shards.md`.
Local no-mistakes Test stays intent-targeted and must not wire `commands.test` to `--all` or a `tests/*.test.sh` walk.
Family selection is the ordinary local path; `--all` is deliberate full regression only.
CI owns broad regression across required portable parallel shards, the portable serial lane, the Herdr lane, lint, invariants, the coverage guard, and stock macOS Bash compatibility in [`.github/workflows/ci.yml`](.github/workflows/ci.yml).
CI owns broad regression across required portable parallel shards, the portable serial lane, required Herdr coverage, lint, invariants, the coverage guard, and stock macOS Bash compatibility in [`.github/workflows/ci.yml`](.github/workflows/ci.yml).
Use `bin/fm-test-run.sh --help` for lane names, `--jobs` rules, and required gate-skip flags when reproducing a lane locally.
Discover tests by listing `tests/*.test.sh`: each is a self-contained bash script named `<subject>.test.sh`, and its header comment describes what it covers, so pass one to `bin/fm-test-run.sh` to focus on a subject with canonical timing output.
Tests that need a real optional backend or an explicit opt-in (real herdr/zellij/cmux smoke tests, the live Pi regression) skip themselves and print the tool or environment gate needed to enable them, so the portable suite remains safe on machines without those tools.
The [Herdr backend guide](docs/herdr-backend.md#destructive-lab-safety) owns the lane's isolation boundary, while [runtime backend verification](docs/verification/runtime-backends.md#herdr) owns active empirical evidence; live harness credential tests remain opt-in.
The [Herdr backend guide](docs/herdr-backend.md#destructive-lab-safety) owns the Herdr tests' isolation boundary, while [runtime backend verification](docs/verification/runtime-backends.md#herdr) owns active empirical evidence; live harness credential tests remain opt-in.

## Questions

Expand Down
6 changes: 4 additions & 2 deletions docs/herdr-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ An auto-detected Herdr spawn prints an opt-out notice.
Spawn stops before creating a Herdr container or acquiring a task worktree when `herdr`, `jq`, or the protocol floor is unavailable.
No separate first-run provisioning is required.

The required CI lane uses the pinned installers in `bin/fm-install-herdr.sh` and `bin/fm-install-treehouse.sh`.
Required Herdr CI coverage uses the pinned installers in `bin/fm-install-herdr.sh` and `bin/fm-install-treehouse.sh`.
Those script headers own release assets, checksums, download bounds, and post-install gates.
Real harness credential tests remain opt-in rather than part of default CI.

Expand All @@ -43,6 +43,7 @@ Routine supervision uses `bin/fm-peek.sh <id>` and `FM_HOME=<home> bin/fm-send.s

Workspace and tab creation use `--no-focus`.
The first workspace in a completely empty Herdr session must become focused because no prior target exists, but later task creation does not intentionally steal focus.
Herdr workspace and tab focus is distinct from macOS frontmost-app activation.

Herdr does not enforce workspace or tab label uniqueness.
Firstmate adopts the first workspace matching its derived home label and refuses duplicate task tabs inside it.
Expand Down Expand Up @@ -80,6 +81,7 @@ A foreign, ambiguous, detached, or manually interleaved child makes ordering ski
Fresh projected ordering failure never fails the task spawn.
On that fresh path, Firstmate does not retry projection, adopt, reuse, close, delete, or rename anything in response to an unavailable method, lock contention, ambiguous socket, lost response, failed move, or verification mismatch.
The worker remains on the ordinary flat or Herdr-current-order path.
The complete projected spawn must also preserve the macOS frontmost app.

Normal task metadata remains the sole endpoint authority after creation.
Cleanup closes only the exact recorded task pane and never calls `workspace close`.
Expand Down Expand Up @@ -123,7 +125,7 @@ Operational compromises:
- Regaining a dedicated space after degradation requires stopping the flat task, manually checking the stale projection, and clearing its journal before a genuinely fresh launch.
- The visible token is only a restart-stable correlator and never substitutes for the exact binding.

`tests/fm-backend-herdr-presentation-e2e.test.sh` covers multi-home ordering, concurrency, lock contention, legacy coexistence, focus preservation, exact same-identity restart replacement, ambiguous bindings and tokens, and exact-pane cleanup through the guarded lab path.
`tests/fm-backend-herdr-presentation-e2e.test.sh` covers multi-home ordering, concurrency, lock contention, legacy coexistence, logical focus and macOS app-activation preservation, exact same-identity restart replacement, ambiguous bindings and tokens, and exact-pane cleanup through the guarded lab path.
`tests/fm-herdr-session-cleanup.test.sh` covers every discovery, ownership, topology, process, locking, revalidation, focus, retirement, and continue-on-error boundary.
`tests/fm-herdr-session-cleanup-e2e.test.sh` covers the restored-shell cleanup in a guarded non-default named lab; [`verification/runtime-backends.md`](verification/runtime-backends.md#per-home-and-presentation-topology) owns the active versioned evidence.

Expand Down
17 changes: 17 additions & 0 deletions docs/verification/runtime-backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,23 @@ HERDR_LAB_HELPER=bin/fm-herdr-lab.sh \

Observed guarantee: one exact home-local, journal-correlated, one-tab and one-pane childless idle shell was closed after restoration while the exact non-target focus and default fleet session remained unchanged, and a repeat run was a no-op.

#### macOS app activation

A guarded non-default presentation lab on 2026-07-28 used Herdr 0.7.3 protocol 16 to separate Herdr's logical workspace and tab focus from macOS frontmost-app activation.
Directly observed workspace creation, tab creation, pane execution, and raw projection `workspace.move` operations neither changed `NSWorkspace.frontmostApplication` nor emitted activation for a different bundle identifier.
The same lab retained the existing exact active-workspace and active-tab assertions, so the evidence did not attribute the apparent focus steal to either Firstmate's projection path or the exercised Herdr core operations.

The active macOS regression is:

```sh
FM_REQUIRE_MACOS_FOCUS_AUDIT=1 \
tests/fm-backend-herdr-presentation-e2e.test.sh
```

The test starts an `NSWorkspace.didActivateApplicationNotification` watcher before the complete projected spawn and keeps it active through a two-second settle window.
It fails if any activation has an empty or different bundle identifier from the initially frontmost app.
The required macOS Herdr-focus CI job makes an unavailable activation audit a hard failure, while non-macOS runs retain the logical focus checks without claiming app-activation coverage.

### Composer and operational input

Real captures verified these active distinctions:
Expand Down
102 changes: 102 additions & 0 deletions tests/fm-backend-herdr-presentation-e2e.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ HERDR_CALL_LOG="$TMP_ROOT/herdr-calls.log"
TREEHOUSE_CALL_LOG="$TMP_ROOT/treehouse-calls.log"
MOVE_CALL_LOG="$TMP_ROOT/workspace-move-calls.log"
FOCUS_AUDIT_LOG="$TMP_ROOT/focus-audit.log"
MACOS_ACTIVATION_LOG="$TMP_ROOT/macos-activation.log"
MACOS_ACTIVATION_READY="$TMP_ROOT/macos-activation.ready"
MACOS_ACTIVATION_WATCHER="$TMP_ROOT/macos-activation-watcher.swift"
ACTIVE_SEEDED_CONTROL="$TMP_ROOT/active-seeded-control"
POST_CREATE_ABORT_CONTROL="$TMP_ROOT/post-create-abort-control"
mkdir -p "$FAKEBIN"
Expand All @@ -37,6 +40,56 @@ REAL_MOVER="$ROOT/bin/backends/herdr-workspace-move.py"
export REAL_HERDR REAL_TREEHOUSE REAL_MOVER HERDR_CALL_LOG TREEHOUSE_CALL_LOG MOVE_CALL_LOG FOCUS_AUDIT_LOG HERDR_ORIGINAL_PATH HERDR_LAB_HELPER
export ACTIVE_SEEDED_CONTROL POST_CREATE_ABORT_CONTROL TMP_ROOT

# Herdr's logical workspace/tab focus is distinct from macOS app activation.
# The projection regression observes activation events across the full spawn.
MACOS_ACTIVATION_AUDIT_ENABLED=0
if [ "$(uname -s)" = Darwin ] \
&& command -v osascript >/dev/null 2>&1 \
&& command -v swift >/dev/null 2>&1 \
&& [ -n "$(osascript -l JavaScript -e 'ObjC.import("AppKit"); $.NSWorkspace.sharedWorkspace.frontmostApplication.bundleIdentifier.js' 2>/dev/null)" ]; then
MACOS_ACTIVATION_AUDIT_ENABLED=1
fi

cat > "$MACOS_ACTIVATION_WATCHER" <<'SWIFT'
import AppKit
import Foundation

guard CommandLine.arguments.count == 3 else {
exit(64)
}

let logPath = CommandLine.arguments[1]
let readyPath = CommandLine.arguments[2]

func appendEvent(_ kind: String, _ bundleIdentifier: String?) {
let line = "\(kind)\t\(bundleIdentifier ?? "")\n"
guard let data = line.data(using: .utf8),
let handle = FileHandle(forWritingAtPath: logPath) else {
exit(1)
}
handle.seekToEndOfFile()
handle.write(data)
try? handle.close()
}

FileManager.default.createFile(atPath: logPath, contents: Data())
let workspace = NSWorkspace.shared
_ = workspace.notificationCenter.addObserver(
forName: NSWorkspace.didActivateApplicationNotification,
object: nil,
queue: .main
) { notification in
let application = notification.userInfo?[NSWorkspace.applicationUserInfoKey]
as? NSRunningApplication
appendEvent("activated", application?.bundleIdentifier)
}
appendEvent("initial", workspace.frontmostApplication?.bundleIdentifier)
guard FileManager.default.createFile(atPath: readyPath, contents: Data()) else {
exit(1)
}
RunLoop.main.run()
SWIFT

# Log every production-adapter call, remove its already-validated trailing
# session flag, and send the operation through the lab helper so that helper
# remains the sole process which appends the real trailing session flag.
Expand Down Expand Up @@ -259,8 +312,14 @@ export HERDR_SESSION="$HERDR_LAB_SESSION" HERDR_LAB_SESSION
LAB_READY=0
RECORDED_WORKTREES=""
LOCK_CONTENTION_OWNER_PID=
MACOS_ACTIVATION_WATCH_PID=
cleanup_all() {
local wt
if [ -n "$MACOS_ACTIVATION_WATCH_PID" ]; then
kill "$MACOS_ACTIVATION_WATCH_PID" 2>/dev/null || true
wait "$MACOS_ACTIVATION_WATCH_PID" 2>/dev/null || true
MACOS_ACTIVATION_WATCH_PID=
fi
if [ -n "$LOCK_CONTENTION_OWNER_PID" ]; then
kill "$LOCK_CONTENTION_OWNER_PID" 2>/dev/null || true
wait "$LOCK_CONTENTION_OWNER_PID" 2>/dev/null || true
Expand All @@ -282,6 +341,11 @@ EOF
}
trap cleanup_all EXIT

if [ "${FM_REQUIRE_MACOS_FOCUS_AUDIT:-0}" = 1 ] \
&& [ "$MACOS_ACTIVATION_AUDIT_ENABLED" != 1 ]; then
fail "required macOS NSWorkspace activation audit is unavailable"
fi

PATH="$HERDR_ORIGINAL_PATH" \
"$HERDR_LAB_HELPER" provision "$HERDR_LAB_SESSION" \
|| fail "could not provision the isolated Herdr lab"
Expand Down Expand Up @@ -321,6 +385,42 @@ assert_focus_is() { # <expected> <case-name>

focus_audit_line_count() { wc -l < "$FOCUS_AUDIT_LOG" | tr -d '[:space:]'; }

start_macos_activation_watch() {
local attempt=0
[ "$MACOS_ACTIVATION_AUDIT_ENABLED" = 1 ] || return 0
: > "$MACOS_ACTIVATION_LOG"
rm -f "$MACOS_ACTIVATION_READY"
swift "$MACOS_ACTIVATION_WATCHER" \
"$MACOS_ACTIVATION_LOG" "$MACOS_ACTIVATION_READY" \
> "$TMP_ROOT/macos-activation-watcher.out" 2>&1 &
MACOS_ACTIVATION_WATCH_PID=$!
while [ ! -e "$MACOS_ACTIVATION_READY" ] && [ "$attempt" -lt 100 ]; do
kill -0 "$MACOS_ACTIVATION_WATCH_PID" 2>/dev/null \
|| fail "macOS activation watcher exited before readiness: $(cat "$TMP_ROOT/macos-activation-watcher.out")"
sleep 0.1
attempt=$((attempt + 1))
done
[ -e "$MACOS_ACTIVATION_READY" ] \
|| fail "macOS activation watcher did not become ready"
}

assert_macos_activation_preserved() { # <case-name>
local case_name=$1 initial changed
[ "$MACOS_ACTIVATION_AUDIT_ENABLED" = 1 ] || return 0
sleep 2
kill "$MACOS_ACTIVATION_WATCH_PID" 2>/dev/null \
|| fail "macOS activation watcher was not running through the settle window"
wait "$MACOS_ACTIVATION_WATCH_PID" 2>/dev/null || true
MACOS_ACTIVATION_WATCH_PID=
initial=$(awk -F '\t' '$1 == "initial" { print $2; exit }' "$MACOS_ACTIVATION_LOG")
[ -n "$initial" ] || fail "$case_name activation watcher did not capture the initial macOS app"
changed=$(awk -F '\t' -v initial="$initial" '
$1 == "activated" && ($2 == "" || $2 != initial) { print $0 }
' "$MACOS_ACTIVATION_LOG")
[ -z "$changed" ] \
|| fail "$case_name activated a different macOS app during spawn or settle: $changed"
}

assert_raw_presentation_mutations_preserved_since() { # <line-count> <case-name>
local start=$1 case_name=$2 changed
changed=$(sed -n "$((start + 1)),\$p" "$FOCUS_AUDIT_LOG" | awk -F '\t' '
Expand Down Expand Up @@ -520,8 +620,10 @@ assert_focus_is "$CAPTAIN_FOCUS" "focused secondmate fixture"
: > "$TREEHOUSE_CALL_LOG"
: > "$HOME_DIR/config/herdr-presentation-spaces"
SHAPE_FOCUS_AUDIT_START=$(focus_audit_line_count)
start_macos_activation_watch
spawn_task shape "$HOME_DIR" "$PROJECT_DIR" > "$TMP_ROOT/on.out" 2> "$TMP_ROOT/on.err" \
|| fail "projected spawn failed: $(cat "$TMP_ROOT/on.err")"
assert_macos_activation_preserved "projected spawn"
assert_focus_is "$CAPTAIN_FOCUS" "projected spawn"
assert_raw_presentation_mutations_preserved_since "$SHAPE_FOCUS_AUDIT_START" "projected spawn"
ON_META="$TMP_ROOT/on.meta"
Expand Down
6 changes: 6 additions & 0 deletions tests/fm-install-herdr.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ test_cleanup_only_targets_job_owned_lab_sessions() {

test_ci_wires_installers_and_required_lane() {
assert_grep 'tests-herdr:' "$CI" "CI must define the required Herdr Behavior job"
assert_grep 'tests-herdr-macos-focus:' "$CI" \
"CI must define the required macOS Herdr focus job"
assert_grep 'FM_REQUIRE_MACOS_FOCUS_AUDIT: 1' "$CI" \
"macOS Herdr CI must require the NSWorkspace activation audit"
assert_grep 'fm-backend-herdr-presentation-e2e.test.sh' "$CI" \
"macOS Herdr CI must run the projected spawn regression"
assert_grep 'fm-install-herdr.sh' "$CI" "CI must call the Herdr installer"
assert_grep 'fm-install-treehouse.sh' "$CI" "CI must call the Treehouse installer"
assert_grep 'fm-herdr-ci-cleanup.sh snapshot' "$CI" "CI must snapshot sessions before the suite"
Expand Down
Loading