diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 79b5d0acf..388f1ab46 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -30,11 +30,11 @@ answerable for how the whole library is tested. ## Building -You need JDK 25 or newer and tmux on `PATH`. Nothing else — Gradle provisions -the toolchain, and the library has no runtime dependencies. The workspace CLI -adds its own runtime dependencies; its terminal regression tests also require -Python 3 for the standard-library PTY driver and Bash on `PATH` for interactive -completion tests. On macOS, install Bash with Homebrew; the system Bash lacks +You need JDK 25 or newer and tmux on `PATH`. Gradle provisions the toolchain, +and the library has no runtime dependencies. Native terminal test fixtures need +Python 3 for its standard-library PTY driver. The workspace CLI adds its own +runtime dependencies; its interactive completion tests also need Bash on +`PATH`. On macOS, install Bash with Homebrew; the system Bash lacks the completion test's Readline hooks. `./gradlew` is the only supported entry point. A locally installed `gradle` is @@ -232,7 +232,7 @@ toolchain and `options.release` in version matrix in the CI workflow, which builds on 25 and 27, and the claim `README.md` makes under `Requirements`. -**tmux 3.2a through 3.7c is the supported range**, and it is not a claim: the +**tmux 3.3a through 3.7c is the supported range**, and it is not a claim: the whole real-tmux suite runs against every one of those releases, and each lane checks it really ran the tmux it is named after. Moving the range means moving `workflows/tmux-matrix.yml` and the README together. diff --git a/.github/WRITING.md b/.github/WRITING.md index 2652b0b9f..3a458fd04 100644 --- a/.github/WRITING.md +++ b/.github/WRITING.md @@ -28,7 +28,7 @@ Four things near the top are compatibility claims, and they move together: exported identifiers without a deprecation period, pin an exact version, not recommended for production. `CHANGELOG.md` and the `Status` section say the same thing in the same words. -- **The requirements are JDK 25 or newer, and tmux 3.2a through 3.7c.** The tmux +- **The requirements are JDK 25 or newer, and tmux 3.3a through 3.7c.** The tmux range is not a claim — the matrix runs every lane of it — so it may not drift from `workflows/tmux-matrix.yml`. - **Coordinates are group `io.github.libtmux`, imported through @@ -120,7 +120,7 @@ Order it capability, then consequence, then compatibility: > 0.0.1-alpha.7 adds streaming capture, and rejects `windowId` at pane scope > rather than ignoring it. Pass `scope: window` to read at window scope. JDK 25 -> and tmux 3.2a through 3.7c are unchanged. +> and tmux 3.3a through 3.7c are unchanged. The title is plain — the version, optionally preceded by `libtmux for Java`. Never "we are excited to announce"; state what shipped. @@ -321,7 +321,7 @@ Bad (Delete): Good (Keep): ```java -// tmux < 3.2 reports the pane ID only after the command completes, +// Some releases report the pane ID only after the command completes, // so this query must stay separate. ``` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 680104eb9..dae088c32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,6 +79,17 @@ jobs: run: | python3 -m venv "$RUNNER_TEMP/workspace-python" "$RUNNER_TEMP/workspace-python/bin/python" -m pip install 'tmuxp==1.74.0' + # Source-overlay qualification: released tmuxp still pins libtmux~=0.61. + # Reviewed PR780 supplies optional policy; these are not a released pair. + git init --quiet "$RUNNER_TEMP/python-libtmux-core" + git -C "$RUNNER_TEMP/python-libtmux-core" fetch --quiet --depth=1 \ + https://github.com/tmux-python/libtmux.git 54b277bb673e208820fc9c795abe4444e2ded272 + git -C "$RUNNER_TEMP/python-libtmux-core" checkout --quiet --detach \ + 54b277bb673e208820fc9c795abe4444e2ded272 + PYTHONPATH="$RUNNER_TEMP/python-libtmux-core/src" "$RUNNER_TEMP/workspace-python/bin/python" -c \ + 'import sys, libtmux; from pathlib import Path; from libtmux.common import TMUX_MIN_VERSION, TMUX_OPTIONAL_FEATURE_POLICY_VERSION as policy; assert type(policy) is int and policy >= 2, policy; assert TMUX_MIN_VERSION == "3.3a", TMUX_MIN_VERSION; assert Path(libtmux.__file__).resolve() == Path(sys.argv[1]).resolve() / "libtmux" / "__init__.py", libtmux.__file__; print(TMUX_MIN_VERSION, policy, libtmux.__file__)' \ + "$RUNNER_TEMP/python-libtmux-core/src" + echo "PYTHONPATH=$RUNNER_TEMP/python-libtmux-core/src" >> "$GITHUB_ENV" echo "TMUX_WORKSPACE_TEST_PYTHON=$RUNNER_TEMP/workspace-python/bin/python" >> "$GITHUB_ENV" # Runs every check task instead of stopping at the first failure, so one run names them all. diff --git a/.github/workflows/clojure-compatibility.yml b/.github/workflows/clojure-compatibility.yml new file mode 100644 index 000000000..0e2b9aeec --- /dev/null +++ b/.github/workflows/clojure-compatibility.yml @@ -0,0 +1,246 @@ +name: Clojure compatibility + +on: + push: + branches: [master] + pull_request: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name == 'pull_request' && github.ref || github.sha }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +permissions: + contents: read + +jobs: + check: + name: Clojure checks (JDK ${{ matrix.java }}, Clojure ${{ matrix.clojure }}) + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + java: ['25', '27'] + clojure: ['1.12.4', '1.12.6'] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + - name: Install tmux + run: | + sudo apt-get update + sudo apt-get install --no-install-recommends -y tmux + tmux -V + + - uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6 + with: + distribution: temurin + # Gradle runs on 25 in every lane (last entry is JAVA_HOME); the lane's JDK runs the Clojure. + java-version: | + ${{ matrix.java }} + 25 + + - uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 + with: + validate-wrappers: true + + - name: Run Clojure checks and publish local artifacts + run: >- + ./gradlew check clojureDependencyMetadata platformCoversEveryPublishedModule + publishToMavenLocal --rerun-tasks --stacktrace + -PlibtmuxJavaVersion=${{ matrix.java }} + -PlibtmuxClojureVersion=${{ matrix.clojure }} + + - name: No tmux server outlived the suite + if: always() + run: | + left="$(pgrep -c tmux || true)" + if [ "${left:-0}" -ne 0 ]; then + echo "::error::$left tmux server(s) survived the suite" + for p in $(pgrep tmux); do tr '\0' ' ' < "/proc/$p/cmdline"; echo; done + exit 1 + fi + echo "no tmux server survived the suite" + + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + if: failure() + with: + name: reports-clojure-jdk${{ matrix.java }}-${{ matrix.clojure }} + path: '**/build/reports/**' + retention-days: 7 + + tmux: + name: Clojure tmux ${{ matrix.tmux }} (JDK ${{ matrix.java }}, Clojure ${{ matrix.clojure }}) + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + java: ['25', '27'] + clojure: ['1.12.4', '1.12.6'] + tmux: ['3.3a', '3.4', '3.5', '3.6', '3.7', '3.7a', '3.7b', '3.7c'] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + - name: Build tmux dependencies + run: | + sudo apt-get update + sudo apt-get install --no-install-recommends -y \ + build-essential libevent-dev libncurses-dev bison pkg-config + + - name: Restore tmux ${{ matrix.tmux }} + id: tmux-cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 + with: + path: ~/tmux-builds/${{ matrix.tmux }} + key: tmux-${{ matrix.tmux }}-${{ runner.os }}-${{ runner.arch }} + + - name: Build tmux ${{ matrix.tmux }} + if: steps.tmux-cache.outputs.cache-hit != 'true' + env: + LANE: ${{ matrix.tmux }} + run: | + set -eux + work="$(mktemp -d)" + curl -fsSL -o "$work/tmux.tar.gz" \ + "https://github.com/tmux/tmux/releases/download/$LANE/tmux-$LANE.tar.gz" + tar -xzf "$work/tmux.tar.gz" -C "$work" + cd "$work/tmux-$LANE" + ./configure --prefix="$HOME/tmux-builds/$LANE" + make -j"$(nproc)" + make install + + - name: The lane runs the tmux it is named after + env: + LANE: ${{ matrix.tmux }} + run: | + "$HOME/tmux-builds/$LANE/bin/tmux" -V + + - uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6 + with: + distribution: temurin + # Gradle runs on 25 in every lane (last entry is JAVA_HOME); the lane's JDK runs the Clojure. + java-version: | + ${{ matrix.java }} + 25 + + - uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 + with: + validate-wrappers: true + + - name: Run the native Clojure real-tmux suite + env: + LANE: ${{ matrix.tmux }} + run: | + ./gradlew :integration-tests:clojureIntegrationTest --rerun-tasks --stacktrace \ + -PlibtmuxJavaVersion=${{ matrix.java }} \ + -PlibtmuxClojureVersion=${{ matrix.clojure }} \ + "-PlibtmuxTmuxBinary=$HOME/tmux-builds/$LANE/bin/tmux" \ + "-PlibtmuxTmuxExpected=$LANE" + + - name: No tmux server outlived the suite + if: always() + run: | + left="$(pgrep -c tmux || true)" + if [ "${left:-0}" -ne 0 ]; then + echo "::error::$left tmux server(s) survived the suite" + for p in $(pgrep tmux); do tr '\0' ' ' < "/proc/$p/cmdline"; echo; done + exit 1 + fi + echo "no tmux server survived the suite" + + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + if: failure() + with: + name: reports-clojure-tmux-${{ matrix.tmux }}-jdk${{ matrix.java }}-${{ matrix.clojure }} + path: '**/build/reports/**' + retention-days: 7 + + macos: + name: Clojure macOS tmux ${{ matrix.tmux }} (JDK ${{ matrix.java }}, Clojure ${{ matrix.clojure }}) + runs-on: macos-latest + timeout-minutes: 25 + strategy: + fail-fast: false + matrix: + java: ['25', '27'] + clojure: ['1.12.4', '1.12.6'] + tmux: ['3.3a', '3.7c'] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + - name: Build tmux dependencies + run: brew install libevent ncurses bison pkg-config + + - name: Restore tmux ${{ matrix.tmux }} + id: tmux-cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 + with: + path: ~/tmux-builds/${{ matrix.tmux }} + key: tmux-${{ matrix.tmux }}-${{ runner.os }}-${{ runner.arch }} + + - name: Build tmux ${{ matrix.tmux }} + if: steps.tmux-cache.outputs.cache-hit != 'true' + env: + LANE: ${{ matrix.tmux }} + run: | + set -eux + work="$(mktemp -d)" + curl -fsSL -o "$work/tmux.tar.gz" \ + "https://github.com/tmux/tmux/releases/download/$LANE/tmux-$LANE.tar.gz" + tar -xzf "$work/tmux.tar.gz" -C "$work" + cd "$work/tmux-$LANE" + libevent_prefix="$(brew --prefix libevent)" + ncurses_prefix="$(brew --prefix ncurses)" + export PKG_CONFIG_PATH="$libevent_prefix/lib/pkgconfig:$ncurses_prefix/lib/pkgconfig" + export CPPFLAGS="-I$ncurses_prefix/include" + export LDFLAGS="-L$ncurses_prefix/lib" + ./configure --prefix="$HOME/tmux-builds/$LANE" --disable-utf8proc + make -j"$(sysctl -n hw.logicalcpu)" + make install + + - name: The lane runs the tmux it is named after + env: + LANE: ${{ matrix.tmux }} + run: | + "$HOME/tmux-builds/$LANE/bin/tmux" -V + + - uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6 + with: + distribution: temurin + # Gradle runs on 25 in every lane (last entry is JAVA_HOME); the lane's JDK runs the Clojure. + java-version: | + ${{ matrix.java }} + 25 + + - uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 + with: + validate-wrappers: true + + - name: Run the native Clojure smoke and lifecycle suite + env: + LANE: ${{ matrix.tmux }} + run: | + ./gradlew :integration-tests:clojureIntegrationTest --rerun-tasks --stacktrace \ + -PlibtmuxJavaVersion=${{ matrix.java }} \ + -PlibtmuxClojureVersion=${{ matrix.clojure }} \ + "-PlibtmuxTmuxBinary=$HOME/tmux-builds/$LANE/bin/tmux" \ + "-PlibtmuxTmuxExpected=$LANE" + + - name: No tmux server outlived the suite + if: always() + run: | + left="$(pgrep -c tmux || true)" + if [ "${left:-0}" -ne 0 ]; then + echo "::error::$left tmux server(s) survived the suite" + for p in $(pgrep tmux); do ps -o command= -p "$p"; done + exit 1 + fi + echo "no tmux server survived the suite" + + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + if: failure() + with: + name: reports-clojure-macos-${{ matrix.tmux }}-jdk${{ matrix.java }}-${{ matrix.clojure }} + path: '**/build/reports/**' + retention-days: 7 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d6ac877fc..b397116de 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -137,6 +137,8 @@ jobs: libtmux-jackson/build/libs/*.jar libtmux-junit5/build/libs/*.jar libtmux-kotlin/build/libs/*.jar + libtmux-clojure/build/libs/*.jar + libtmux-clojure-core-async/build/libs/*.jar libtmux-mcp/build/libs/*.jar libtmux-workspace/build/libs/*.jar libtmux-workspace-cli/build/libs/*.jar diff --git a/.github/workflows/tmux-matrix.yml b/.github/workflows/tmux-matrix.yml index dd4711594..79c3ac8ef 100644 --- a/.github/workflows/tmux-matrix.yml +++ b/.github/workflows/tmux-matrix.yml @@ -1,6 +1,6 @@ name: tmux matrix -# The README claims tmux 3.2a through 3.7c, and says that range is not a claim because the suite runs +# The README claims tmux 3.3a through 3.7c, and says that range is not a claim because the suite runs # against every one of them. This is where that stops depending on someone running it locally. on: @@ -27,7 +27,7 @@ jobs: lanes: # Preview lanes carry their own label: this is the only place a reader sees which entries the # supported range does not include, since the README and the matrix property list only the rest. - name: tmux ${{ matrix.tmux }}${{ matrix.experimental && ' (preview, not supported)' || '' }} + name: tmux ${{ matrix.tmux }}${{ matrix.experimental && ' (preview, not supported)' || '' }}${{ matrix.rejection && ' (must be refused)' || '' }} runs-on: ubuntu-latest timeout-minutes: ${{ matrix.tmux == 'master' && 25 || 15 }} continue-on-error: ${{ matrix.experimental == true }} @@ -38,11 +38,19 @@ jobs: # (its `lanes` list), which is the source of truth. The agree step below fails if they drift # apart. This is also what DocumentationFactsTest reads the README's claimed range from, so a # preview lane belongs in `include` below and never here. - tmux: ['3.2a', '3.3', '3.3a', '3.4', '3.5', '3.6', '3.7', '3.7a', '3.7b', '3.7c'] + tmux: ['3.3a', '3.4', '3.5', '3.6', '3.7', '3.7a', '3.7b', '3.7c'] # Ahead of the supported range, matching the plugin's `previewLanes` list: 3.8-rc2 is a # candidate for a release not yet cut, and master is upstream's development branch, not a # version. Both may fail and neither moves the README's claimed range. include: + # Below the floor, matching the plugin's `rejectionLanes`: the real binary must refuse + # every call and leave a sentinel session, option and buffer untouched. + - tmux: '3.2a' + rejection: true + - tmux: '3.3' + rejection: true + - tmux: '3.8' + experimental: true - tmux: '3.8-rc2' experimental: true - tmux: master @@ -132,6 +140,17 @@ jobs: run: | python3 -m venv "$RUNNER_TEMP/workspace-python" "$RUNNER_TEMP/workspace-python/bin/python" -m pip install 'tmuxp==1.74.0' + # Source-overlay qualification: released tmuxp still pins libtmux~=0.61. + # Reviewed PR780 supplies optional policy; these are not a released pair. + git init --quiet "$RUNNER_TEMP/python-libtmux-core" + git -C "$RUNNER_TEMP/python-libtmux-core" fetch --quiet --depth=1 \ + https://github.com/tmux-python/libtmux.git 54b277bb673e208820fc9c795abe4444e2ded272 + git -C "$RUNNER_TEMP/python-libtmux-core" checkout --quiet --detach \ + 54b277bb673e208820fc9c795abe4444e2ded272 + PYTHONPATH="$RUNNER_TEMP/python-libtmux-core/src" "$RUNNER_TEMP/workspace-python/bin/python" -c \ + 'import sys, libtmux; from pathlib import Path; from libtmux.common import TMUX_MIN_VERSION, TMUX_OPTIONAL_FEATURE_POLICY_VERSION as policy; assert type(policy) is int and policy >= 2, policy; assert TMUX_MIN_VERSION == "3.3a", TMUX_MIN_VERSION; assert Path(libtmux.__file__).resolve() == Path(sys.argv[1]).resolve() / "libtmux" / "__init__.py", libtmux.__file__; print(TMUX_MIN_VERSION, policy, libtmux.__file__)' \ + "$RUNNER_TEMP/python-libtmux-core/src" + echo "PYTHONPATH=$RUNNER_TEMP/python-libtmux-core/src" >> "$GITHUB_ENV" echo "TMUX_WORKSPACE_TEST_PYTHON=$RUNNER_TEMP/workspace-python/bin/python" >> "$GITHUB_ENV" # One lane rather than testTmuxMatrix: each version builds and runs on its own runner, so the diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fbc4cb7b..353bb0a42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,50 @@ production. ## Unreleased +### Added + +- **`Server.captureServer` returns handles from one captured graph.** Sessions, + window links, pane occurrences, clients, and metadata share the same capture; + traversing them issues no additional commands. + +### Changed + +- **Managed control now requires tmux 3.7+ and a pinned socket.** + Ordinary operations keep the 3.3a minimum. Named buffer deletion, rename + and managed paste require 3.4; invalid literal layouts are refused on 3.3a. + Literal percentage splits are refused on exact 3.4, and raw or unnamed + breaks on exact 3.7. Named breaks retain the + requested name; single-pane breaks also work without native `-P` output. + Python extensions require optional-policy v2. See + [migration notes](MIGRATION.md#optional-tmux-requirements). (#68) + +- **Exact tmux 3.7 refuses `.` and `:` in session and window names before + dispatch.** A batch or workspace no longer runs the commands ahead of the + one tmux would reject. (#68) + +- **tmux 3.3a is the minimum across the JVM modules and products.** Upgrade + the running daemon as well as the client. Commands refuse older daemons + before their requested effects; the Python bridge also requires compatible + Python libtmux. Cold starts accept 3.3a binaries renamed on disk. See + [migration notes](MIGRATION.md). (#67) +- **The version check no longer runs a user's `after-display-message` hook.** + The daemon version is read with `run-shell -C`, which runs no `after-*` or + `command-error` hook, instead of `display-message`. (#67) +- **The version check asks a known daemon once.** A call no longer spends a version + probe process of its own; the fence on every dispatch drops the entry when + the daemon has been replaced, and an older replacement raises the version + error rather than `TargetGoneException`. (#67) +- **`TmuxVersion` orders release candidates below their release and parses + strictly.** `3.8-rc < 3.8`, `compareTo` agrees with `equals`, and text such + as `3.3a garbage` or `3.3a-master` is rejected. `atLeast` still counts a + candidate as its release. (#67) + +### Fixed + +- **The Clojure `close-runtime!` waits for scheduled callback cancellation.** + Closing a runtime with finite pending callbacks no longer reports cleanup as + pending before its completion executor can terminate. + ### Documented - **`SplitSpec.Builder.running`, `WindowSpec.Builder.running` and diff --git a/MIGRATION.md b/MIGRATION.md index 7b89dc28f..91e72ecd4 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -8,6 +8,110 @@ API changes that require updates to calling code are recorded here. See A breaking type is named on its own `api-break:` line. Mentioning the type in the prose is not that line. +### tmux 3.3a is the minimum + +Upgrade the tmux daemon to 3.3a or newer. Installing a newer client alone does +not upgrade a daemon that is already running. Finish or save its sessions, +stop that daemon, then start the new tmux binary on the same endpoint. + +Commands through Java, Kotlin and Scala, including MCP tools and native +workspace loading, refuse an older daemon with `UnsupportedFeatureException` +before sending the requested operation. Raw commands and batches follow the +same rule. Standalone `cmd("-V")` and `cmd("-h")` remain available for executable +diagnostics; their results do not authorize a daemon operation. Unknown full +version tokens fail with `MalformedResponseException`; a candidate for 3.3a +does not satisfy the released 3.3a minimum. + +Ordinary command requests carry an atomic version guard. Managed control +acquisition requires tmux 3.7 or newer and pins the socket before `-C`. +The identity is checked through that pin before attaching; replacement of the +original pathname cannot redirect initialization to a new daemon. +These checks require native built-in command semantics. A tmux configuration +that replaces commands such as `if-shell` or `display-message` through +`command-alias` is outside this qualification. + +The first session on an unused endpoint checks the configured binary before +starting a daemon. Reads leave an unused endpoint empty; cleanup of an absent +daemon remains harmless. Optional features retain their own version +requirements. Python extensions and the Python shell use +`Server.requireCompatibleTmux()` before starting Python. This validates the +selected daemon or executable without starting a daemon; later Java operations +still run their own version check. The bridge also requires Python libtmux +with the matching core support change: its declared `TMUX_MIN_VERSION` must be +3.3a or newer, and its own dispatcher must enforce that floor on each command. +The bridge also requires the integer +`libtmux.common.TMUX_OPTIONAL_FEATURE_POLICY_VERSION >= 2`. It checks both +requirements in the runtime probe and again in the actual child, before +builders, plugins or shell code run. Policy version 1 is refused. Until the +[Python feature change][python-features] is released, use an interpreter +loading that coordinated source; upgrading +tmuxp alone does not supply a command-level version check or the optional guards. + +CI qualifies tmuxp 1.74.0 with the [reviewed Python libtmux source][python-source] +on `PYTHONPATH`. It verifies the imported source path, declared +floor and optional-policy marker. Released tmuxp 1.74.0 pins +libtmux 0.61, so this source overlay is not a coherent installed package pair; +that dependency update remains part of the coordinated tmuxp change. + +[python-core]: https://github.com/tmux-python/libtmux/pull/779 +[python-features]: https://github.com/tmux-python/libtmux/pull/780 +[python-source]: https://github.com/tmux-python/libtmux/tree/54b277bb673e208820fc9c795abe4444e2ded272 + +### Optional tmux requirements + +Ordinary subprocess operations, polling, MCP tools and native workspace +building keep the 3.3a core floor. The selected daemon determines eligibility, +even when the configured client is newer. A refused literal batch runs none +of its commands, including a preceding command that would otherwise succeed. + +| Operation | Requirement or repair | +| --- | --- | +| Managed control, watches, mirrors and live views | Stable tmux 3.7+ and a carrier-owned pinned socket. Older daemons are refused before control initialization. | +| Exact named buffer deletion and rename | tmux 3.4+. On 3.3a, a missing name can select an unrelated buffer. | +| `Pane.paste(text)` | tmux 3.4+ before creating a temporary buffer, because failure cleanup needs exact named deletion. | +| Direct `paste-buffer -d -b name` | Retained on 3.3a; native lookup already rejects a missing name. | +| Literal `select-layout` on 3.3a | Invalid operands are refused before every batch effect. Presets, unique prefixes and serialized layouts remain available; `-o` requires a serialized operand. | +| Percentage splitting | Typed `SplitSpec.percent` retains `-l N%`. Literal `split-window -p` is refused on exact 3.4. | +| Session and window names on exact 3.7 | A name containing `.` or `:` is refused before dispatch (`IllegalArgumentException`), including workspace window names; 3.7a and later keep such names. | +| `break-pane` on exact 3.7 | Literal calls and unnamed typed calls are refused. Use `Pane.breakOut(name)` or upgrade to 3.7a+. | +| Single-pane `breakOut` before 3.6 | The typed call reads the pane's actual post-command context; native success does not print `-P` output there. | +| Captured `run-shell` output | Existing 3.5 floor remains; 3.3a and 3.4 lose the requested output. Effect-only shell commands remain available. | + +Named `breakOut` on exact 3.7 applies the name after the pane moves, because +that release ignores `-n` in its multi-pane branch. A later rename failure can +leave the pane in its new window. An unnamed single-pane break on other +supported releases preserves the existing window name. + +Custom `ControlCarrier` implementations must implement `pinSocket` in their +own execution realm. Its alias must retain the socket inode in a private +directory for the attachment's lifetime; replacing the original pathname must +not redirect it. The default method refuses control acquisition. Ordinary +commands through an existing custom transport are unaffected. The built-in +local carrier uses a short private hard link on the socket's filesystem and +refuses before `-C` if no valid alias can be created. Closing an attachment or +receiving EOF removes only its owned alias and directory; the carrier remains +borrowed. One timeout budget covers qualification and attachment. + +Literal command words retain their existing encoding, including semicolons +used as data. Feature guards inspect the literal commands a batch represents, +without interpreting user-authored `if-shell` bodies, `source-file` contents, +shell programs or configured aliases. Those opaque programs retain native +semantics and are outside the managed optional-feature guarantee. + +Control-positive matrix cases run from 3.7. Lower lanes keep ordinary +operations and explicit before-effect refusal assertions. Documentation +control examples compile on every lane and assert the refusal below that +feature's floor. + +### Version ordering and parsing + +`TmuxVersion.compareTo` now orders `next-X.Y < X.Y-rc < X.Y-rcN < X.Y < X.Ya`, +so it agrees with `equals`; a candidate no longer compares equal to its release. +Use `atLeast` for a feature gate, which counts a candidate as its release, and +`sameRelease` for a gate on one release's own defect. `TmuxVersion.parse` accepts +only what tmux has reported (`3.3a`, `3.8-rc2`, `next-3.9`) and throws +`IllegalArgumentException` for anything else, such as `3.3a garbage`. + ## 0.0.1-alpha.16 ### `CommandResult.stdout()` keeps carriage returns diff --git a/README.md b/README.md index 1883ddf2b..0fc818260 100644 --- a/README.md +++ b/README.md @@ -455,9 +455,10 @@ whole whatever the locale. Only `Pane.currentPath()` still needs a UTF-8 locale, because a `Path` in this JVM cannot name the directory; `currentPathText()` reads it anywhere. -tmux 3.2a through 3.7c. That range is not a claim: the whole real-tmux suite runs -against every one of those releases, and each lane checks it really ran the tmux -it is named after. +tmux 3.3a through 3.7c for ordinary operations. Every supported release has a +matrix lane that checks the selected daemon. Managed control requires stable +tmux 3.7 or newer and a carrier that can pin its socket. Other feature +requirements and replacements are in the [migration notes](MIGRATION.md#optional-tmux-requirements). ```console $ ./gradlew testTmuxMatrix -PlibtmuxMatrix=/path/to/tmux/builds diff --git a/benchmarks/src/test/java/io/github/libtmux/benchmark/OperationBenchmark.java b/benchmarks/src/test/java/io/github/libtmux/benchmark/OperationBenchmark.java index 04f67ffc9..3afe865d6 100644 --- a/benchmarks/src/test/java/io/github/libtmux/benchmark/OperationBenchmark.java +++ b/benchmarks/src/test/java/io/github/libtmux/benchmark/OperationBenchmark.java @@ -864,22 +864,23 @@ private String render( .append(" windows, asked for three ways. This is the whole of the answer to ") .append("per-command process cost, so it leads.\n\n"); table(out, "strategy", grouping); - out.append("\nEvery row pays the same four commands for the handle it starts from and the ") - .append("count it ends with, so the ratio between them understates what grouping ") - .append("saves: the work itself is 60 commands against one.\n"); + out.append("\nEvery row also pays for the handle it starts from and the count it ends with. ") + .append("Each ordinary request has its own version probe; grouping reduces ") + .append("the number of those requests as well as the data commands.\n"); out.append("\n## Reading the hierarchy\n\n") .append("`windows()` and `snapshot()` both throw when capture fails. They read who the ") .append("server is, then run the four listings as one group fenced against that ") - .append("answer. Two commands, whatever the hierarchy holds.\n\n"); + .append("answer. Two data requests and their version probes, whatever the hierarchy holds.\n\n"); table(out, "read", reading); out.append("\n## Narrow reads\n\n") .append("Five sessions, one wanted. A lookup by name, by pane id, or by a filter tmux can ") .append("apply reads who the server is, then lists only the sessions it wants, with their ") - .append("windows and panes, as one fenced group: two commands, the same as a snapshot, ") + .append("windows and panes, as one fenced group: two data requests and their version probes, ") + .append("the same as a snapshot, ") .append("over less of the server. A pane or window condition is looped over each ") - .append("session inside tmux, so no probe comes first.\n\n"); + .append("session inside tmux, so no separate pane-existence probe comes first.\n\n"); table(out, "read", narrowing); out.append("\nThe same reads against fifty sessions of three windows each. The commands do not ") .append("change; what a whole-server read pays for is the rows.\n\n"); @@ -923,8 +924,8 @@ private String render( .append("between releases, so names come from the listing and values from ") .append("`show-options -v`, batched.\n\n"); table(out, "read", options); - out.append("\nThat is the cost: one option is one command, and a listing is two whatever ") - .append("its size, until it outgrows what one command may carry.\n"); + out.append("\nOne option is one data request, and a listing is two whatever its size, until ") + .append("it outgrows what one command may carry. Each request has a version probe.\n"); out.append("\n## One command, two transports\n\n") .append("The same read, `display-message -p \"#{session_name}\"`, sent ") @@ -970,7 +971,8 @@ private String render( .append("| p95 | %s |%n".formatted(micros(following.p95Nanos()))) .append("| commands the mirror dispatched per change | %.1f |%n" .formatted(following.dispatchesPerChange())) - .append("\nA rebuild is a snapshot, two commands. A new window is announced more than once, ") + .append("\nA rebuild is a snapshot: two data requests and their version probes. ") + .append("A new window is announced more than once, ") .append("and announcements that arrive during a rebuild fold into one more, so a change ") .append("costs one or two rebuilds rather than one per announcement.\n"); diff --git a/build-logic/conventions/src/main/kotlin/io/github/libtmux/buildlogic/GenerateScalaDocumentationSuite.kt b/build-logic/conventions/src/main/kotlin/io/github/libtmux/buildlogic/GenerateScalaDocumentationSuite.kt index 41f51d165..d451b8df2 100644 --- a/build-logic/conventions/src/main/kotlin/io/github/libtmux/buildlogic/GenerateScalaDocumentationSuite.kt +++ b/build-logic/conventions/src/main/kotlin/io/github/libtmux/buildlogic/GenerateScalaDocumentationSuite.kt @@ -26,7 +26,8 @@ import org.gradle.api.tasks.TaskAction * Each fence carries a directive on the line before it, ``: * `sync` runs the code against a fresh tmux, `io` runs a Cats `IO` to completion within the * 15-second deadline every documented snippet has, `reject` asserts the code fails to compile with - * `detail` in the error, and `build` marks an sbt build fragment the consumer checks exercise + * `detail` in the error, `io-control` also checks refusal below tmux 3.7, and + * `build` marks an sbt build fragment the consumer checks exercise * instead. An unclassified fence fails the build, and so * does a directive no fence follows. A final inventory test pins each document's digest and fence * count, so a document edited after the suite was generated cannot pass on the old code. @@ -131,6 +132,14 @@ abstract class GenerateScalaDocumentationSuite : DefaultTask() { val invoke = snippet.objectName + ".run(fixture.config)" val body = when (snippet.mode) { "sync" -> "io.github.libtmux.scaladsl.fixture.OwnedTmux.use { fixture => $invoke }" + "io-control" -> + "io.github.libtmux.scaladsl.fixture.OwnedTmux.use { fixture =>\n" + + "val program = $invoke.timeout(_root_.scala.concurrent.duration.FiniteDuration(15, java.util.concurrent.TimeUnit.SECONDS))\n" + + "if (fixture.supportsControl) program.unsafeRunSync()(using _root_.cats.effect.unsafe.IORuntime.global)\n" + + "else {\n" + + "intercept[io.github.libtmux.exception.UnsupportedFeatureException](program.unsafeRunSync()(using _root_.cats.effect.unsafe.IORuntime.global))\n" + + "assert(fixture.server.clients().isEmpty(), \"a refused snippet attached a client\")\n" + + "}\n}" "io" -> "io.github.libtmux.scaladsl.fixture.OwnedTmux.use { fixture =>\n" + "val evaluated = new java.util.concurrent.atomic.AtomicBoolean(false)\n" + @@ -175,7 +184,7 @@ $cases val IGNORED = setOf(".git", ".gradle", ".bsp", ".metals", ".idea", "target", "build", "node_modules") val OPENING = Regex("^ {0,3}(`{3,}|~{3,})[ \\t]*([^\\s`]*).*$") val UNSUPPORTED_SCALA_FENCE = Regex("(?i)^[ \\t>]*(?:`{3,}|~{3,})[ \\t]*(?:scala\\S*|sbt)(?:[ \\t].*)?$") - val DIRECTIVE = Regex("^$") + val DIRECTIVE = Regex("^$") fun relative(root: File, file: File): String = root.toPath().relativize(file.toPath()).toString().replace('\\', '/') diff --git a/build-logic/conventions/src/main/kotlin/libtmux.clojure-library.gradle.kts b/build-logic/conventions/src/main/kotlin/libtmux.clojure-library.gradle.kts new file mode 100644 index 000000000..f187fa834 --- /dev/null +++ b/build-logic/conventions/src/main/kotlin/libtmux.clojure-library.gradle.kts @@ -0,0 +1,166 @@ +import org.gradle.api.tasks.JavaExec +import org.gradle.api.tasks.SourceSetContainer +import org.gradle.jvm.toolchain.JavaToolchainService + +plugins { id("libtmux.published-library") } + +val clojureLibraries = extensions.getByType().named("libs") +val clojureSourceSets = extensions.getByType() +val clojureJavaToolchains = extensions.getByType() +val clojureJavaVersion = providers.gradleProperty("libtmuxJavaVersion").getOrElse("25").toInt() +val clojureVersion = + providers.gradleProperty("libtmuxClojureVersion") + .getOrElse(clojureLibraries.findVersion("clojure").orElseThrow().requiredVersion) + +clojureSourceSets.named("main") { resources.srcDir("src/main/clojure") } +clojureSourceSets.named("test") { + resources.srcDir("src/test/clojure") + resources.srcDir(rootProject.file("gradle/clojure-runner")) + resources.exclude("agent/**") +} + +dependencies { "api"("org.clojure:clojure:$clojureVersion") } + +val namespaceGuardAsm = configurations.create("clojureNamespaceGuardAsm") { + isCanBeConsumed = false + isCanBeResolved = true +} +dependencies { add(namespaceGuardAsm.name, "org.ow2.asm:asm:9.9.1") } +val compileNamespaceGuard = tasks.register("compileClojureNamespaceGuard") { + source(rootProject.fileTree("gradle/clojure-runner/agent") { include("**/*.java") }) + classpath = namespaceGuardAsm + destinationDirectory.set(layout.buildDirectory.dir("namespace-guard/classes")) + javaCompiler.set(clojureJavaToolchains.compilerFor { + languageVersion.set(JavaLanguageVersion.of(21)) + }) + options.release.set(21) +} +val namespaceGuardBootstrap = tasks.register("clojureNamespaceGuardBootstrap") { + from(compileNamespaceGuard.flatMap { it.destinationDirectory }) { + include("libtmux/internal/loadguard/LoadGuard.class") + } + archiveFileName.set("namespace-guard-bootstrap.jar") + destinationDirectory.set(layout.buildDirectory.dir("namespace-guard")) +} +val namespaceGuardAsmJar = tasks.register("clojureNamespaceGuardAsm") { + from(provider { namespaceGuardAsm.map { zipTree(it) } }) { + exclude("META-INF/MANIFEST.MF", "module-info.class") + } + archiveFileName.set("namespace-guard-asm.jar") + destinationDirectory.set(layout.buildDirectory.dir("namespace-guard")) +} +val namespaceGuardAgent = tasks.register("clojureNamespaceGuardAgent") { + from(compileNamespaceGuard.flatMap { it.destinationDirectory }) { + exclude("libtmux/internal/loadguard/LoadGuard.class") + } + manifest.attributes( + "Premain-Class" to "libtmux.internal.loadguard.NamespaceAgent", + "Can-Retransform-Classes" to "true", + ) + archiveFileName.set("namespace-guard-agent.jar") + destinationDirectory.set(layout.buildDirectory.dir("namespace-guard")) +} + +val declaredClojureTests = provider { extra["clojureTestNamespaces"] as String } +val discoveredClojureTests = provider { + fileTree("src/test/clojure") { include("**/*_test.clj") } + .files + .map { source -> + source.relativeTo(file("src/test/clojure")) + .invariantSeparatorsPath + .removeSuffix(".clj") + .replace('_', '-') + .replace('/', '.') + } + .sorted() +} + +val clojureNamespaceLoad = tasks.register("clojureNamespaceLoad") { + group = "verification" + description = "Loads public Clojure namespaces without reflection warnings or resource creation." + dependsOn(tasks.named("testClasses"), namespaceGuardAgent, namespaceGuardBootstrap, namespaceGuardAsmJar) + classpath = clojureSourceSets.named("test").get().runtimeClasspath + mainClass.set("clojure.main") + args("-m", "libtmux.internal.test-runner", "load") + jvmArgs("-Dclojure.main.report=stderr", "-Xshare:off") + javaLauncher.set(clojureJavaToolchains.launcherFor { + languageVersion.set(JavaLanguageVersion.of(clojureJavaVersion)) + }) + doFirst { + jvmArgs("-javaagent:${namespaceGuardAgent.get().archiveFile.get().asFile}=" + + namespaceGuardBootstrap.get().archiveFile.get().asFile + File.pathSeparator + + namespaceGuardAsmJar.get().archiveFile.get().asFile) + } +} + +val clojureNamespaceGuardTest = tasks.register("clojureNamespaceGuardTest") { + group = "verification" + description = "Proves namespace loading rejects executor, process and socket creation." + dependsOn(tasks.named("testClasses"), namespaceGuardAgent, namespaceGuardBootstrap, namespaceGuardAsmJar) + classpath = clojureSourceSets.named("test").get().runtimeClasspath + mainClass.set("clojure.main") + args("-m", "libtmux.internal.test-runner", "guard-probes") + jvmArgs("-Xshare:off") + javaLauncher.set(clojureJavaToolchains.launcherFor { + languageVersion.set(JavaLanguageVersion.of(clojureJavaVersion)) + }) + doFirst { + jvmArgs("-javaagent:${namespaceGuardAgent.get().archiveFile.get().asFile}=" + + namespaceGuardBootstrap.get().archiveFile.get().asFile + File.pathSeparator + + namespaceGuardAsmJar.get().archiveFile.get().asFile) + } +} + +val clojureTest = tasks.register("clojureTest") { + group = "verification" + description = "Runs the module's native Clojure unit tests." + dependsOn(tasks.named("testClasses")) + classpath = clojureSourceSets.named("test").get().runtimeClasspath + mainClass.set("clojure.main") + args("-m", "libtmux.internal.test-runner", "test") + jvmArgs("-Dclojure.main.report=stderr") + javaLauncher.set(clojureJavaToolchains.launcherFor { + languageVersion.set(JavaLanguageVersion.of(clojureJavaVersion)) + }) + doFirst { + systemProperty("libtmux.clojure.test-namespaces", declaredClojureTests.get()) + } +} + +val clojureTestInventory = tasks.register("clojureTestInventory") { + group = "verification" + description = "Fails when a native Clojure test namespace is omitted from the runner." + inputs.files(fileTree("src/test/clojure") { include("**/*_test.clj") }) + doLast { + val declared = declaredClojureTests.get().split(',').sorted() + require(declared == discoveredClojureTests.get()) { + "configured Clojure tests $declared do not match source tests ${discoveredClojureTests.get()}" + } + } +} + +val prepareClojureTestClasspath = tasks.register("prepareClojureTestClasspath") { + group = "build setup" + description = "Writes the reusable native Clojure test classpath." + dependsOn(tasks.named("testClasses")) + val destination = layout.buildDirectory.file("clojure/test-classpath.txt") + inputs.files(clojureSourceSets.named("test").map { it.runtimeClasspath }) + outputs.file(destination) + doLast { + destination.get().asFile.apply { + parentFile.mkdirs() + writeText(clojureSourceSets.named("test").get().runtimeClasspath.asPath + "\n") + } + } +} + +tasks.named("check") { + dependsOn(clojureNamespaceLoad, clojureNamespaceGuardTest, clojureTest, clojureTestInventory) +} + +tasks.withType().configureEach { + isPreserveFileTimestamps = false + isReproducibleFileOrder = true + dirPermissions { unix("755") } + filePermissions { unix("644") } +} diff --git a/build-logic/conventions/src/main/kotlin/libtmux.tmux-matrix.gradle.kts b/build-logic/conventions/src/main/kotlin/libtmux.tmux-matrix.gradle.kts index 1f99e51ad..91a4b1a71 100644 --- a/build-logic/conventions/src/main/kotlin/libtmux.tmux-matrix.gradle.kts +++ b/build-logic/conventions/src/main/kotlin/libtmux.tmux-matrix.gradle.kts @@ -8,17 +8,22 @@ // Declared so the source-set accessors resolve; the module already has it via the library plugin. plugins { java } -val lanes = listOf("3.2a", "3.3", "3.3a", "3.4", "3.5", "3.6", "3.7", "3.7a", "3.7b", "3.7c") +val lanes = listOf("3.3a", "3.4", "3.5", "3.6", "3.7", "3.7a", "3.7b", "3.7c") // Ahead of the supported range: previews of what tmux ships next, run so a break shows up before the // release that makes it real rather than after. Allowed to fail, and left out of testTmuxMatrix and // the README's claimed range on purpose — 3.8-rc2 is a candidate for a release not yet cut, and master // is upstream's development branch, not a version at all. -val previewLanes = listOf("3.8-rc2", "master") +val previewLanes = listOf("3.8", "3.8-rc2", "master") + +// Below the floor: these run only the version check cases against the real binary, which must refuse +// every call and leave a sentinel session, option and buffer as it was. They are part of the matrix +// because a refusal that a fake alone vouches for is not one. +val rejectionLanes = listOf("3.2a", "3.3") val matrix = providers.gradleProperty("libtmuxMatrix") -fun registerLane(lane: String) = +fun registerLane(lane: String, rejection: Boolean = false) = tasks.register("test-tmux-$lane") { group = "verification" description = "Runs the real-tmux tests against tmux $lane." @@ -28,7 +33,20 @@ fun registerLane(lane: String) = // "locale" joins the excludes for the same reason "carrier" is here: it needs a JVM // started differently — one whose platform encoding is not UTF-8 — so running it in an // ordinary fork asserts nothing and fails saying so. - useJUnitPlatform { excludeTags("fixture", "carrier", "benchmark", "locale") } + useJUnitPlatform { + excludeTags("fixture", "carrier", "benchmark", "locale") + // Ordinary 3.3a+ operations still run; managed control has its own 3.7 floor. + if (lane in listOf("3.3a", "3.4", "3.5", "3.6")) { + excludeTags("control") + } + } + + // By class, not by tag: the Scala suites run under another engine that ignores tags. + if (rejection) { + filter.includeTestsMatching("io.github.libtmux.it.VersionCheckLaneIntegrationTest") + // Every module has the lane task, and only integration-tests has the class. + filter.isFailOnNoMatchingTests = false + } // Resolved lazily: the task is registered whether or not a matrix exists, so asking for // it without one fails loudly instead of the lane silently not existing. @@ -55,11 +73,12 @@ fun registerLane(lane: String) = } } -val laneTasks = lanes.map(::registerLane) -previewLanes.forEach(::registerLane) +val laneTasks = lanes.map { registerLane(it) } +previewLanes.forEach { registerLane(it) } +val rejectionTasks = rejectionLanes.map { registerLane(it, rejection = true) } rootProject.tasks.maybeCreate("testTmuxMatrix").apply { group = "verification" description = "Runs every real-tmux test against every supported tmux release." - dependsOn(laneTasks) + dependsOn(laneTasks, rejectionTasks) } diff --git a/build.gradle.kts b/build.gradle.kts index 3f3182bdc..937315fab 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -97,11 +97,49 @@ val kotlinStaysDownstream = } } +val clojureDependencyMetadata = + tasks.register("clojureDependencyMetadata") { + group = "verification" + description = "Checks deps.edn against the checked-in development dependency baseline." + val depsEdn = layout.projectDirectory.file("deps.edn") + val gradleProperties = layout.projectDirectory.file("gradle.properties") + inputs.files(depsEdn, gradleProperties) + doLast { + val declared = depsEdn.asFile.readText() + val clojureVersion = libs.versions.clojure.get() + val coreAsyncVersion = libs.versions.core.async.get() + val developmentVersion = + gradleProperties.asFile.useLines { lines -> + lines.first { it.startsWith("libtmuxVersion=") }.substringAfter('=') + } + val expected = + mapOf( + "org.clojure/clojure" to clojureVersion, + "org.clojure/core.async" to coreAsyncVersion, + "io.github.libtmux/libtmux" to developmentVersion, + "io.github.libtmux/libtmux-junit5" to developmentVersion, + ) + expected.forEach { (coordinate, version) -> + val declaredVersions = + Regex("${Regex.escape(coordinate)}\\s+\\{:mvn/version\\s+\"([^\"]+)\"") + .findAll(declared) + .map { it.groupValues[1] } + .toList() + require(declaredVersions.isNotEmpty()) { + "deps.edn must declare $coordinate at the development baseline $version" + } + require(declaredVersions.all { it == version }) { + "every deps.edn declaration of $coordinate must use the development baseline $version; found $declaredVersions" + } + } + } + } + tasks.register("check") { group = "verification" description = "Every gate that must hold before publication." dependsOn(subprojects.filter { it.buildFile.exists() }.map { "${it.path}:check" }) - dependsOn(platformCoversEveryPublishedModule, kotlinStaysDownstream) + dependsOn(platformCoversEveryPublishedModule, kotlinStaysDownstream, clojureDependencyMetadata) // An included build's tasks are not this build's subprojects, so its tests would otherwise // never run in the gate: the generators every facade compiles against, the conventions' own // helpers, and the Doclet's lint. diff --git a/deps.edn b/deps.edn new file mode 100644 index 000000000..24aca2bfc --- /dev/null +++ b/deps.edn @@ -0,0 +1,24 @@ +{:paths ["libtmux-clojure/src/main/clojure"] + :deps {org.clojure/clojure {:mvn/version "1.12.4"} + io.github.libtmux/libtmux {:mvn/version "0.0.1-alpha.17-SNAPSHOT"}} + :aliases + {:test + {:extra-paths ["libtmux-clojure/src/test/clojure" + "gradle/clojure-runner"] + :main-opts ["-m" "libtmux.internal.test-runner" "test" + "libtmux-clojure/src/test/clojure"]} + :adapter-test + {:extra-paths ["libtmux-clojure-core-async/src/main/clojure" + "libtmux-clojure-core-async/src/test/clojure" + "gradle/clojure-runner"] + :extra-deps {org.clojure/core.async {:mvn/version "1.8.741"}} + :main-opts ["-m" "libtmux.internal.test-runner" "test" + "libtmux-clojure-core-async/src/test/clojure"]} + :integration-test + {:extra-paths ["integration-tests/src/test/clojure" + "libtmux-clojure-core-async/src/main/clojure" + "gradle/clojure-runner"] + :extra-deps {io.github.libtmux/libtmux-junit5 {:mvn/version "0.0.1-alpha.17-SNAPSHOT"} + org.clojure/core.async {:mvn/version "1.8.741"}} + :main-opts ["-m" "libtmux.internal.test-runner" "test" + "integration-tests/src/test/clojure"]}}} diff --git a/docs/README.md b/docs/README.md index 5230d5565..a5dd41de9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -118,6 +118,7 @@ Say otherwise with an HTML comment directly above the fence: | directive | means | | --- | --- | | *(none)* | compiles, and runs against live tmux | +| `` | runs on tmux 3.7+; older daemons must refuse before attaching | | `` | runs, and must fail with exactly that | | `` | the compiler must reject it, with a diagnostic containing that phrase | | `` | compiles; not run, for the stated reason | @@ -282,3 +283,8 @@ that, and each row says where it looks. Not `docs/decisions` or `docs/internals`: those are dated or historical records. Holding them to today's API would either break the build or quietly rewrite history, and neither is what a record is for. + +Control-specific Scala effects use `scala-io-control` in place of `scala-io`. +They compile on every lane, run normally from tmux 3.7, and must raise +`UnsupportedFeatureException` before attachment on older daemons. Java's +`control` directive checks the same boundary. diff --git a/docs/build.gradle.kts b/docs/build.gradle.kts index 54195bb50..844f6a7ad 100644 --- a/docs/build.gradle.kts +++ b/docs/build.gradle.kts @@ -59,7 +59,7 @@ tasks.withType().configureEach { // build-logic's deeper tree included. val sources = rootProject.fileTree(root) { - include("**/*.java", "**/*.kt", "**/*.kts", "**/*.scala") + include("**/*.java", "**/*.kt", "**/*.kts", "**/*.scala", "**/*.clj") exclude("**/build/**", "**/target/**", ".gradle/**", "**/.gradle/**") } diff --git a/docs/guide/batching-and-chaining.md b/docs/guide/batching-and-chaining.md index 7bc9ea375..1b47a66f1 100644 --- a/docs/guide/batching-and-chaining.md +++ b/docs/guide/batching-and-chaining.md @@ -2,23 +2,24 @@ Every snippet here is executed by `DocumentationSnippetsTest`. -Each call starts one tmux process, and that process is most of what a call costs. -A batch and a chain both put several commands into one invocation. +An ordinary request starts one tmux process to check the daemon's version and +another to run its commands. A batch and a chain put several commands into that +second invocation. ## Which path for which job | you want | use | tmux processes | | --- | --- | --- | -| one change or read, typed | a handle's method: `session.rename`, `pane.sendKeys` | one | +| one change or read, typed | a handle's method: `session.rename`, `pane.sendKeys` | one tmux process, after one version probe per daemon | | a command no method covers | `server.cmd(...)` to inspect the answer, `server.run(...)` to require success | one | -| several independent commands, each answered | `batch()` | one, however many | -| steps where each acts on what the last made | `chain()` | one | +| several independent commands, each answered | `batch()` | one, however many commands | +| steps where each acts on what the last made | `chain()` | one for dispatch, plus any capability reads | | to know when something in a pane has happened | `Pane.awaitText`, `Channel.await`, or `Pane.run` | [measured](../benchmarks/operation-costs.md#push-against-poll) | -| output as it arrives | `server.control(session)`, then a subscription | one attached client | +| output as it arrives | `server.control(session)`, then a subscription | one attached client, with startup probes | -The same twenty windows cost 64 processes made one at a time and 5 as a batch or -a chain: [`operation-costs.md`](../benchmarks/operation-costs.md#collapsing-round-trips) -has the rows. +Creating twenty windows one at a time costs one process each; as a batch or a +chain it costs one for the lot. [`operation-costs.md`](../benchmarks/operation-costs.md#collapsing-round-trips) +has the measured rows. ## A batch: several commands, each with its own outcome diff --git a/docs/guide/clojure-reference.md b/docs/guide/clojure-reference.md new file mode 100644 index 000000000..60e648286 --- /dev/null +++ b/docs/guide/clojure-reference.md @@ -0,0 +1,145 @@ +# Clojure captured-value reference + +`libtmux.data/from-capture` creates every value in this reference. `snapshot!` +and the `*!` listing functions acquire one captured graph; `data/windows`, +`data/panes`, `data/active-window` and `data/active-pane` traverse it without +I/O. Scalar access is ordinary Clojure keyword lookup and is also local. + +Every owning map has its documented key, even when the value is `nil` because +tmux did not report an optional value. A missing relationship means the parent +or target was absent from an otherwise successful capture. Failed acquisition +throws; it is never an empty graph. + +`data/data` removes every `:tmux/ref`. The ref is the only effect authority; +editing `:session/id`, `:window/id` or `:pane/id` changes a map, not tmux. +`data/same-entity?` compares `:tmux/identity`. `data/same-link?` also compares +the window placement in `:tmux/link`; it does not establish a daemon generation +or detect an unseen identical unlink-and-relink replacement. + + + +## Shared keys + +- `:tmux/ref` is the Java handle. `data/data` removes it; effects and + `FilterExpr` need it, while identity does not. +- `:tmux/identity` is always-present endpoint, server-PID, kind and physical-ID + data. `same-entity?` compares it. +- `:tmux/link` exists only on windows and panes. It holds session, window and + index occurrence context; `same-link?` compares it. +- `:tmux/realm` and `:tmux/server` are always-present identity text. Neither + has a Java filter field. +- `:tmux/pid` is an integer or `nil`; the key stays present when tmux does not + report a daemon or pane PID. It has no Java filter field. +- `:tmux/kind` is always-present `:server`, `:session`, `:window`, `:pane` or + `:client` identity data. It has no Java filter field. +- `:tmux/id` is always-present physical identity text. Editing it cannot + retarget an effect, and it has no Java filter field. +- `:tmux/captured-at` is a root-only `Instant`; `data/data` writes ISO-8601 + text. `:tmux/version` is root-only text or `nil` when unavailable. Neither + has a Java filter field. + +## Root and sessions + +- `:tmux/sessions`, `:tmux/windows`, `:tmux/panes` and `:tmux/clients` are + root-only captured vectors. They have no root Java relation. +- `:session/id` and `:session/name` are always-present text. `Session_/id` and + `Session_/name` support Java filtering. +- `:session/attached?` is an always-present Boolean. `Session_/attached` + supports Java filtering. +- `:session/windows` is an always-present local vector relation. `data/windows` + traverses it and `Session_/windows` is the canonical Java relation. + +## Windows and panes + +- `:window/id` is always-present underlying-window text across links. + `Window_/id` supports Java filtering. +- `:window/index` is the always-present index within `:tmux/link`. + `Window_/index` supports Java filtering. +- `:window/name` is always-present text. `Window_/name` supports Java + filtering. +- `:window/active?` is an always-present Boolean for this link's session. + `Window_/active` supports Java filtering. +- `:window/linked?` is an always-present Boolean stating that the underlying + window has another link. `Window_/linked` supports Java filtering. +- `:window/width` and `:window/height` are always-present captured cell sizes; + `:window/layout` is always-present layout text. They have no Java fields. +- `:window/panes` is an always-present local vector relation. `data/panes` + traverses it and `Window_/panes` is the canonical Java relation. +- `:pane/id` is always-present physical-pane text; `Pane_/id` supports Java + filtering. `:pane/index` is an always-present window index; `Pane_/index` + supports Java filtering. +- `:pane/active?` is an always-present Boolean for the captured window. + `Pane_/active` supports Java filtering. +- `:pane/current-command` and `:pane/current-path` are always-present tmux + text. `Pane_/command` and `Pane_/path` support Java filtering. +- `:pane/floating?` is Boolean or `nil` when tmux does not report it. The key + stays present and it has no Java field. +- `:pane/width` and `:pane/height` are always-present cell sizes; `Pane_/width` + and `Pane_/height` support Java filtering. +- `:pane/left` and `:pane/top` are always-present captured positions; + `Pane_/left` and `Pane_/top` support Java filtering. +- `:pane/title` is always-present text; `Pane_/title` supports Java filtering. +- `:pane/pid` is an integer or `nil` when tmux does not report it. The key stays + present and it has no Java field. +- `:pane/edges` is an always-present map with unqualified Boolean edge keys. + It has no Java field. + +## Clients + +- `:client/name` is always-present tmux client text; `Client_/name` supports + Java filtering. +- `:client/session` is a session map or `nil` when no session is attached. The + key stays present and `Client_/session` is the canonical Java relation. +- `:client/active-window` and `:client/active-pane` are a captured map or + `nil` when no attached session supplies them. Their keys stay present and + they have no Java fields. + +Java expressions always evaluate the preserved Java ref through +`data/predicate` or `data/matching`; they never evaluate edited or detached +maps. `Pane_` has no registered pane-to-window relation, so use captured +traversal for that direction instead of inventing a canonical Java relation. diff --git a/docs/guide/clojure.md b/docs/guide/clojure.md new file mode 100644 index 000000000..1cc24d801 --- /dev/null +++ b/docs/guide/clojure.md @@ -0,0 +1,371 @@ +# Clojure + +`libtmux-clojure` is a JVM façade over the Java library. It keeps tmux command +encoding, transport, server identity and compatibility in Java, while exposing +captured persistent Clojure values and ordinary collection operations. + +Every Clojure fence in reader-facing documentation is discovered and run against +an owned tmux server. A fence must open and close its own client with +`with-open` +and end with one `;; =>` EDN result. The test fixture supplies +`libtmux.docs.socket` and `libtmux.docs.binary`; applications supply their +socket path and use their selected binary or the default `tmux` command. + +## Values, links and projection + +`sessions!`, `windows!`, `panes!` and `clients!` acquire a graph. `data/windows` +and `data/panes` traverse that graph without I/O. A window occurrence has a +contextual `:tmux/link`; `data/same-link?` includes the session, window and +index, while `data/same-entity?` compares only the physical identity. + +`nil` means an optional relationship or lookup was absent in a successful +capture. Failed acquisition throws an `ExceptionInfo`; it is never represented +as an empty collection. `data/data` returns a detached EDN projection without +the Java refs required for effects or Java filter evaluation. + +## Resource scopes and laziness + +Acquisition returns eager captured values. A lazy `filter` or `map` over those +values reads only Clojure data, so it can escape the `with-open` scope. An I/O +operation such as `capture!` is different: realize it inside the scope that +owns its server. Do not return `(map #(tm/capture! (:tmux/ref %)) panes)` from +`with-open`; use `mapv`, `into`, or another eager reduction before closing. + + +```clojure +(require '[libtmux.core :as tm] + '[libtmux.data :as data]) + +(let [socket-path (java.nio.file.Path/of + (System/getProperty "libtmux.docs.socket") + (make-array String 0)) + {:keys [lazy-names captures]} + (with-open [server (tm/open! {:socket-path socket-path})] + (let [session (tm/new-session! server {:name "docs-lazy"}) + pane (first (data/panes (first (data/windows session))))] + {:lazy-names (->> (tm/sessions! server) + (filter #(= "docs-lazy" (:session/name %))) + (map :session/name)) + :captures (mapv #(tm/capture! (:tmux/ref %) {:from :history}) + [pane])}))] + [(vec lazy-names) (count captures)]) +;; => [["docs-lazy"] 1] +``` + + +```clojure +(require '[libtmux.core :as tm] + '[libtmux.data :as data]) + +(let [socket-path (java.nio.file.Path/of + (System/getProperty "libtmux.docs.socket") + (make-array String 0))] + (with-open [server (tm/open! {:socket-path socket-path})] + (let [session (tm/new-session! server {:name "docs-projection"})] + (data/data (select-keys session [:tmux/ref :session/name]))))) +;; => {:session/name "docs-projection"} +``` + +## Native filtering and Java expressions + +Use `filter`, `map`, `into` and transducers for unrestricted local Clojure +logic. Java `FilterExpr` remains available when an expression must use its +canonical Java fields and relationship semantics. `data/predicate` returns an +IFn. `data/matching` returns a transducer with one argument and a lazy sequence +with an expression and collection. Both evaluate preserved `:tmux/ref` values, +not fields edited in a Clojure map. + + +```clojure +(require '[libtmux.core :as tm] + '[libtmux.data :as data]) +(import '[io.github.libtmux Session_]) + +(let [socket-path (java.nio.file.Path/of + (System/getProperty "libtmux.docs.socket") + (make-array String 0))] + (with-open [server (tm/open! {:socket-path socket-path})] + (tm/new-session! server {:name "docs-query"}) + (->> (tm/sessions! server) + (data/matching (.is (Session_/name) "docs-query")) + (mapv :session/name)))) +;; => ["docs-query"] +``` + +Use `data/exactly-one` when zero or many candidates is an error, and +`data/one-or-none` when absence is allowed. Establish cardinality before any +effect. + +## Effects, errors and execution modes + +Every effect in `libtmux.core` takes a Java ref from `:tmux/ref`; a captured map +is not an editable remote object. `new-session!`, `rename!`, `split-pane!` and +`refresh!` return new captured values when they can recapture the result. Void +operations return `nil`. + +Validation errors include `:tmux/phase :validation` and +`:tmux/dispatch :not-dispatched`. Transport and mutation failures retain the +operation, dispatch certainty and whether effects are possible. Inspect the +stable subset with `core/error-summary`; do not parse exception messages. + +`batch!` and `chain!` use fresh Java groups. Their per-position results can be +`:complete`, `:failed`, `:skipped` or `:unknown`; neither operation promises a +transaction or rollback. A group that can wait needs `{:kind :wait}` so its +release can reserve capacity. + +### Bounded capture work and cancellation + +`libtmux.async/runtime!` bounds asynchronous work. Submit each capture as a +task, then dereference its result after the runtime has admitted it. Task +submission does not add another tmux transport or alter the captured values. + + +```clojure +(require '[libtmux.async :as async] + '[libtmux.core :as tm]) + +(let [socket-path (java.nio.file.Path/of + (System/getProperty "libtmux.docs.socket") + (make-array String 0))] + (with-open [server (tm/open! {:socket-path socket-path}) + runtime (async/runtime! {:max-running 3})] + (tm/new-session! server {:name "docs-parallel"}) + (let [tasks (mapv (fn [_] (async/submit! runtime #(tm/snapshot! server))) + (range 2))] + (mapv #(boolean (some (fn [session] + (= "docs-parallel" (:session/name session))) + (:tmux/sessions @%))) + tasks)))) +;; => [true true] +``` + +`cancel!` is explicit. A cancellation accepted after a task starts reports +unknown dispatch certainty, because the enclosing tmux operation might have +started. Use a separate `:wait` task for operations that wait for a release. + + +```clojure +(require '[libtmux.async :as async] + '[libtmux.core :as tm]) +(import '[java.time Duration]) + +(let [socket-path (java.nio.file.Path/of + (System/getProperty "libtmux.docs.socket") + (make-array String 0))] + (with-open [server (tm/open! {:socket-path socket-path}) + runtime (async/runtime! {})] + (let [started (promise) + task (async/submit! runtime {:kind :wait} + #(do (deliver started true) + (tm/await-channel! server "docs-cancel" + (Duration/ofSeconds 20))))] + @started + (async/cancel! task) + (try + @task + (catch clojure.lang.ExceptionInfo failure + [(:tmux/error (ex-data failure)) + (:tmux/dispatch (ex-data failure))]))))) +;; => [:cancelled :unknown] +``` + +### Layouts and literal input + +Creation and layout functions modify tmux and return fresh observations when +they create a target. `apply-layout!` accepts tmux's explicit layout text. +`send-literal!` writes characters without treating them as tmux key names; +`send-keys!` takes an explicit key vector. Their complete return does not mean +the program in the pane has finished processing input. + + +```clojure +(require '[libtmux.core :as tm] + '[libtmux.data :as data]) + +(let [socket-path (java.nio.file.Path/of + (System/getProperty "libtmux.docs.socket") + (make-array String 0))] + (with-open [server (tm/open! {:socket-path socket-path})] + (let [session (tm/new-session! server {:name "docs-layout"}) + window (tm/new-window! (:tmux/ref session) + {:name "docs-layout-window" :detached? true}) + pane (first (data/panes window)) + _ (tm/apply-layout! (:tmux/ref window) (:window/layout window)) + split (tm/split-pane! (:tmux/ref pane) + {:direction :right :percent 30})] + [(:window/name window) (= (:tmux/link pane) (:tmux/link split))]))) +;; => ["docs-layout-window" true] +``` + + +```clojure +(require '[clojure.string :as str] + '[libtmux.core :as tm] + '[libtmux.data :as data]) + +(let [socket-path (java.nio.file.Path/of + (System/getProperty "libtmux.docs.socket") + (make-array String 0))] + (with-open [server (tm/open! {:socket-path socket-path})] + (let [session (tm/new-session! server {:name "docs-literal"}) + pane (first (data/panes (first (data/windows session))))] + (tm/send-literal! (:tmux/ref pane) "printf docs-literal") + (tm/send-keys! (:tmux/ref pane) ["Enter"]) + (boolean (some #(str/includes? % "docs-literal") + (tm/capture! (:tmux/ref pane) {:from :history})))))) +;; => true +``` + +### Contextual targeting + +A linked window keeps the same physical window identity but has another +occurrence context. Effects use its preserved ref, so an unlinked occurrence +does not silently target a remaining link in another session. + + +```clojure +(require '[libtmux.core :as tm] + '[libtmux.data :as data]) + +(let [socket-path (java.nio.file.Path/of + (System/getProperty "libtmux.docs.socket") + (make-array String 0))] + (with-open [server (tm/open! {:socket-path socket-path})] + (let [first-session (tm/new-session! server {:name "docs-link-first"}) + second-session (tm/new-session! server {:name "docs-link-second"}) + window (first (data/windows first-session))] + (tm/link-window! (:tmux/ref window) (:tmux/ref second-session)) + (let [linked (first (filter #(= (:window/id window) (:window/id %)) + (data/windows + (tm/refresh! (:tmux/ref second-session)))))] + [(data/same-entity? window linked) (data/same-link? window linked)])))) +;; => [true false] +``` + +## Lifecycle, streaming and optional adapters + +`libtmux.async/runtime!` owns bounded JVM workers. Close it, or use `with-open`; +closing a borrowed server or executor is never an adapter side effect. Tasks +have explicit admission, cancellation and deadline results, and expose a Java +`CompletionStage` for Manifold or other JVM integration. + +Control needs tmux 3.7 or newer. On an older daemon `attach!` is +refused before any client exists, with `:tmux/error` `:tmux/unsupported-feature` +and `:tmux/dispatch` `:not-dispatched`; the rest of the façade keeps the 3.3a +minimum. Control observations have an explicit owner and terminal result. A +bounded consumer can lose events: the next read is a `:gap` naming how many were +dropped, ahead of the events that remain, and an adapter loss stays distinct +from it. Text subscriptions are decoded per pane by the Java client, so a +character cut between two chunks arrives whole. +Do not model a subscription as an unbounded lazy sequence, and do not perform +blocking observation work in a core.async `go` block. Add the optional +`io.github.libtmux/libtmux-clojure-core-async` dependency only for +`libtmux.core-async`. + +An empty slow stream reports a timeout without becoming terminal. A later event +remains readable from the same owned stream. + + +```clojure +(require '[libtmux.control :as control] + '[libtmux.core :as tm]) +(import '[java.lang AutoCloseable] + '[java.time Duration]) + +(let [socket-path (java.nio.file.Path/of + (System/getProperty "libtmux.docs.socket") + (make-array String 0))] + (with-open [server (tm/open! {:socket-path socket-path + :binary (System/getProperty + "libtmux.docs.binary")}) + connection (control/attach! + (:tmux/ref (first (tm/sessions! server))) + (Duration/ofSeconds 2)) + stream (control/subscribe! connection)] + (let [initial (control/next! stream (Duration/ofMillis 25)) + pane (first (:reply/lines + (control/send! connection + ["new-window" "-P" "-F" "#{pane_id}" + (str "stty -echo; printf docs-slow;" + " read stop")]))) + event (control/next! stream (Duration/ofMillis 900))] + [(= :timeout (:stream/status initial)) + (= pane (:pane/id event))]))) +;; => [true true] +``` + + +```clojure +(require '[clojure.core.async :as async] + '[libtmux.control :as control] + '[libtmux.core :as tm] + '[libtmux.core-async :as adapter]) +(import '[java.time Duration]) + +(let [socket-path (java.nio.file.Path/of + (System/getProperty "libtmux.docs.socket") + (make-array String 0))] + (with-open [server (tm/open! {:socket-path socket-path + :binary (System/getProperty + "libtmux.docs.binary")}) + connection (control/attach! + (:tmux/ref (first (tm/sessions! server))) + (Duration/ofSeconds 2))] + (let [owner (adapter/observe! connection) + pane (first (:reply/lines + (control/send! connection + ["new-window" "-P" "-F" "#{pane_id}" + (str "stty -echo; printf docs-async;" + " read stop")]))) + timeout (async/timeout 900) + [event port] (async/alts!! [(adapter/events owner) timeout] + :priority true)] + (async/close! (adapter/events owner)) + [(and (identical? port (adapter/events owner)) (= pane (:pane/id event))) + (:reason (async/ [true :adapter-stopped] +``` + +### Manifold CompletionStage recipe + +Manifold remains a consumer dependency. Add +`manifold/manifold {:mvn/version "0.5.0"}` to the application, then adapt the +public `CompletionStage`; it does not become a `libtmux` dependency. `then!` +uses the runtime's bounded callback executor. A Manifold deferred observes the +stage; cancelling that deferred does not cancel the task, so call `cancel!` +explicitly when ownership requires it. + + +```clojure +(require '[libtmux.async :as async] + '[libtmux.core :as tm] + '[manifold.deferred :as deferred]) +(import '[java.util.concurrent CompletionStage]) + +(let [socket-path (java.nio.file.Path/of + (System/getProperty "libtmux.docs.socket") + (make-array String 0))] + (with-open [server (tm/open! {:socket-path socket-path}) + runtime (async/runtime! {})] + (tm/new-session! server {:name "docs-manifold"}) + (let [task (async/submit! runtime #(tm/snapshot! server)) + stage (async/then! runtime task + #(boolean (some (fn [session] + (= "docs-manifold" + (:session/name session))) + (:tmux/sessions %)))) + observed (deferred/->deferred stage)] + [(instance? CompletionStage stage) @observed]))) +;; => [true true] +``` + +## Compatibility and Java interop + +The façade supports JDK 25 or newer, the floor of the Java core it wraps, and the repository's tmux 3.3a through 3.7c +matrix; control needs tmux 3.7 or newer. It is JVM-only: it does not run in ClojureScript, Babashka or a native +image. Existing Java handles, `FilterExpr`, the optional Jackson codec and Java +async types remain directly usable when the Clojure functions do not cover an +operation. + +[`clojure-reference.md`](clojure-reference.md) lists every captured key and +relation, its availability, identity meaning and Java filter support. diff --git a/docs/guide/filtering.md b/docs/guide/filtering.md index 56dfbf17c..ad22d3471 100644 --- a/docs/guide/filtering.md +++ b/docs/guide/filtering.md @@ -109,9 +109,9 @@ use a targeted listing when the name or id is safe to put in a format, and a whole-server capture otherwise. `sessions(FilterExpr)`, `windows(FilterExpr)`, and `panes(FilterExpr)` send a safe expression as `list-sessions -f`, `list-windows -f`, or `list-panes -f`, and still apply the expression to what -comes back. Each costs two tmux commands, as a snapshot does, and reads only the -sessions it keeps. A relation, or an expression tmux cannot apply, reads the -whole server. +comes back. Each sends two data requests and their version probes, as a snapshot +does, and reads only the sessions it keeps. A relation, or an expression tmux +cannot apply, reads the whole server. Filtering a list already in hand still issues no commands. ## Writing an expression down diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index 79da1dfbd..4fc5347ba 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -211,8 +211,10 @@ just created. ## Watching output -A control client stays attached and pushes terminal output as it happens: +A control client requires tmux 3.7 or newer and a carrier that can pin +its socket. It stays attached and pushes terminal output as it happens: + ```java // Given: Server server, Session session try (ControlClient client = server.control(session); diff --git a/docs/guide/mcp.md b/docs/guide/mcp.md index befa29af2..e04d033bf 100644 --- a/docs/guide/mcp.md +++ b/docs/guide/mcp.md @@ -171,7 +171,9 @@ invocation, and everything at or below it waits until it is finished. ## Being told, instead of asking -tmux can push. A control client that has attached is told when a window appears +tmux can push. Managed control requires tmux 3.7+ and a carrier-owned +pinned socket. The MCP polling tools retain the core 3.3a minimum. A control +client that has attached is told when a window appears or a session is renamed, and `refresh-client -B` registers a format tmux re-expands on its own one-second timer and reports **only when the value differs**. diff --git a/docs/guide/operating-a-service.md b/docs/guide/operating-a-service.md index e7a7c3d7f..dfe42db79 100644 --- a/docs/guide/operating-a-service.md +++ b/docs/guide/operating-a-service.md @@ -51,7 +51,7 @@ try (Server observed = Server.open(config.toBuilder().observer(reports::add).bui } OperationReport last = reports.get(reports.size() - 1); -last.verbs(); // → [display-message] +last.verbs(); // → [if-shell, display-message] last.certainty(); // → COMPLETE last.exitCode(); // → OptionalInt[0] ``` @@ -64,6 +64,11 @@ line counts, and at most 240 characters of stderr in `boundedError`, which stack trace and otherwise ignored; the command's own result is unaffected, and nothing is retried. +The version check emits its own `run-shell` probe report. The probe fires none +of a user's `after-*` or `command-error` hooks, so checking a call's daemon has no +effect a hook could see. The request's report includes the `if-shell` version +guard before the requested command. + A report's `id` counts calls on one transport, so two reports can be told apart; it is not a request id for tracing. None is needed: the observer runs on the thread that made the call, so whatever that thread carries - a logging MDC, @@ -74,7 +79,7 @@ an OpenTelemetry context, a thread local - is there to read: ThreadLocal request = new ThreadLocal<>(); Map seen = new ConcurrentHashMap<>(); ServerConfig traced = - config.toBuilder().observer(report -> seen.put(report.verbs().get(0), request.get())).build(); + config.toBuilder().observer(report -> seen.put(report.verbs().getLast(), request.get())).build(); try (Server server = Server.open(traced)) { request.set("req-42"); server.cmd("display-message", "-p", "hi"); diff --git a/docs/guide/scala/compatibility.md b/docs/guide/scala/compatibility.md index d44c29f72..1d05a6261 100644 --- a/docs/guide/scala/compatibility.md +++ b/docs/guide/scala/compatibility.md @@ -10,9 +10,9 @@ the facades' unit suites, their real-tmux suites, the documented Scala examples and the runnable programs on JDK 25 and 27 on Linux, and on macOS. Every lane of the [tmux matrix][tmux-matrix] runs the real-tmux suites against -its own tmux: `3.2a`, `3.3`, `3.3a`, `3.4`, `3.5`, `3.6`, `3.7`, `3.7a`, `3.7b` -and `3.7c`. The Scala fixture takes the lane's tmux the same way the Java one -does. +its own tmux: `3.3a`, `3.4`, `3.5`, `3.6`, `3.7`, `3.7a`, `3.7b` and `3.7c`. +The Scala fixture takes the lane's tmux the same way the Java one does. The +facades inherit Java's version check and reject older daemons. ## Artifacts @@ -46,8 +46,15 @@ release for MiMa or `tasty-mima` to compare against. ## Inherited feature boundaries -The facade preserves Java's version guards. [Named buffer deletion][buffers] +The facade preserves Java's version guards. Managed control, FS2/Ox +observations and live views require tmux 3.7+ and a carrier-owned +pinned socket. Ordinary operations remain available from 3.3a. Control-positive +cases run only on eligible lanes; lower lanes assert before-effect refusal. + +[Named buffer deletion][buffers] rejects tmux before 3.4 because those versions can delete the wrong buffer. +The same boundary covers named rename and managed-paste cleanup. Direct named +`paste-buffer -d` retains its native exact lookup on 3.3a. [`Shell.capturing`][java-shell] rejects tmux 3.3a and 3.4, which lose the requested output. Java's tmux matrix asserts these unsupported results rather than skip the contract or substitute an empty successful result. diff --git a/docs/guide/scala/getting-started.md b/docs/guide/scala/getting-started.md index 7a2697970..2519f80e9 100644 --- a/docs/guide/scala/getting-started.md +++ b/docs/guide/scala/getting-started.md @@ -1,6 +1,6 @@ # Getting started -The Scala facades need Scala 3.9 and JDK 25 or newer, and tmux 3.2a through +The Scala facades need Scala 3.9 and JDK 25 or newer, and tmux 3.3a through 3.7c on the machine they drive. ## Install diff --git a/docs/guide/scala/ownership.md b/docs/guide/scala/ownership.md index af1d685ca..f1391fed8 100644 --- a/docs/guide/scala/ownership.md +++ b/docs/guide/scala/ownership.md @@ -84,7 +84,9 @@ its owner. [`Control.attach`][control] on a captured session owns a separate process attachment to that capture's process, started by the session's transport. -`Control.attachUnfenced` on a config and session id does not check the process. +Both forms require tmux 3.7+ and a carrier-owned pinned socket before +control initialization. `Control.attachUnfenced` selects the daemon answering +at call time; it does not require a previously captured incarnation. Releasing either stops its admitted requests before closing the attachment and preserves the daemon. diff --git a/docs/guide/scala/streaming.md b/docs/guide/scala/streaming.md index 857e9b39c..92ad5c5dd 100644 --- a/docs/guide/scala/streaming.md +++ b/docs/guide/scala/streaming.md @@ -1,6 +1,7 @@ # Streaming -The Cats module adapts Java control subscriptions to FS2. Acquire an attachment, +The Cats module adapts Java control subscriptions to FS2. Managed control +requires tmux 3.7+ and a carrier-owned pinned socket. Acquire an attachment, then an observation, then start the producer. An observation has one active stream consumer; use FS2's explicit broadcast operations if several consumers need the same values. Starting another reader on the same observation fails. @@ -11,7 +12,7 @@ Here `config` selects an existing server with a session. The subscription is registered before the rename. Evaluating the returned `IO` performs the work; constructing it alone does not. - + ```scala import _root_.cats.effect.IO import io.github.libtmux.control.Notification diff --git a/docs/guide/snapshots-and-handles.md b/docs/guide/snapshots-and-handles.md index 0d530855e..1b36beaa9 100644 --- a/docs/guide/snapshots-and-handles.md +++ b/docs/guide/snapshots-and-handles.md @@ -22,15 +22,17 @@ That is deliberate. tmux offers no transaction across separate listings, so a traversal that re-queried could observe a hierarchy that never existed — a window in one listing and its panes from after it closed. -A capture costs two tmux commands: one asks which server this is, and one runs the -four listings as a group fenced against that answer. Because tmux runs a group -inside the server, the rows cannot come from two of them, and a server replaced +A capture sends two data requests, each with a separate version probe: one asks +which server this is, and one runs the four listings as a group fenced against +that answer. Because tmux runs a group inside the server, the rows cannot come +from two of them, and a server replaced under the capture is refused rather than half-read. "Which server" is its pid and when it started: a pid can be reused, and a handle is refused by a tmux started since on the same one. tmux reports that start in whole seconds, so a server replaced on the same pid within the same second would pass; nothing short of a token planted in the server closes that gap. What that costs is measured in -[`docs/benchmarks/operation-costs.md`](../benchmarks/operation-costs.md). +[`docs/benchmarks/operation-costs.md`](../benchmarks/operation-costs.md), whose +recorded commit predates the extra version probes. `refresh()` is how to look again. Every live listing, finder and snapshot capture throws when it fails. An absent daemon throws @@ -107,10 +109,8 @@ captured.size(); // → 90x20 captured.refresh().size(); // → 100x30 ``` -The first size is set rather than inherited on purpose. A new window's default -is the client's size, less a row for the status line on tmux 3.2a and not on -3.3 or later, so a snippet that printed that default would be telling the truth -on only some of the releases this supports. +The first size is set so the example does not depend on the client's terminal +dimensions. `Session.rename`, `Window.rename`, `Pane.retitle`, and each handle's `refresh` carry `@CheckReturnValue`. Error Prone rejects a call that discards their results. diff --git a/docs/guide/streaming.md b/docs/guide/streaming.md index de6f0d67d..b193a08c5 100644 --- a/docs/guide/streaming.md +++ b/docs/guide/streaming.md @@ -93,9 +93,12 @@ origin. Treat exhaustion as "carry on" rather than as an error. ## Pushed output -A control client stays attached and pushes terminal output as tmux produces it, -rather than being asked: +Managed control requires tmux 3.7 or newer and a carrier that can pin +its socket before initialization. Ordinary polling and channels retain the +3.3a floor. An eligible control client stays attached and pushes terminal +output as tmux produces it: + ```java // Given: Server server, Session session try (ControlClient client = server.control(session); @@ -130,6 +133,7 @@ time on the consuming thread. It ends when the subscription closes, so a timer that closes it bounds the whole read, and closing the stream closes the subscription: + ```java // Given: Server server, Session session try (ControlClient client = server.control(session); @@ -174,6 +178,7 @@ subscription's own bounded buffer keeps filling regardless, and a full buffer still reports its loss as a `Delivery.Gap`, delivered like any other element. Pausing tmux's own push is `pause-after`, further down. + ```java // Given: Server server, Session session try (ControlClient client = server.control(session); @@ -221,6 +226,7 @@ The same client is told about changes as they happen — a window created or renamed, a session switched, a layout moved — without asking. Each arrives as a `ControlEvent`, and `notification()` is its typed reading, for matching on: + ```java // Given: Server server, Session session try (ControlClient client = server.control(session); @@ -251,6 +257,7 @@ A notification says that something changed, not what everything now is. fresh capture, and each capture that differs from the last is published as a numbered view: + ```java // Given: Session session try (ServerMirror mirror = ServerMirror.open(session)) { @@ -282,7 +289,7 @@ itself, which a control-mode line needs, since tmux answers a bare `parse error` for an unquoted `%0:off` typed there directly. The four state words are **two independent pairs**, with different loss -behaviour, measured against tmux 3.2a, 3.7c and next-3.9: +behaviour, measured against tmux 3.7c and next-3.9: | stop → resume | recovers? | output produced while stopped | | --- | --- | --- | @@ -308,7 +315,7 @@ tmux can also pause a pane on its own. After `client.send("refresh-client", "-f", "pause-after=5")`, a pane whose output this client is more than five seconds behind on is paused and announced as `Notification.Pause`; output keeps reaching `subscribeOutput` until then, and `refresh-client -A :continue` -resumes it, announced as `Notification.Continue`. tmux 3.2 and later. +resumes it, announced as `Notification.Continue`. Every supported tmux has it. ## Requests are serialized diff --git a/docs/guide/testing.md b/docs/guide/testing.md index e49229f75..3929b80a0 100644 --- a/docs/guide/testing.md +++ b/docs/guide/testing.md @@ -83,6 +83,11 @@ about, because both are silent: - `pane_floating_flag` does not exist before tmux 3.7, and the format expands to nothing rather than to zero. `Pane.floating()` is an `Optional` that is empty when the running tmux cannot say, rather than defaulting to `false`. -- tmux 3.7 exactly ends the whole server when `break-pane` has to name the new - window itself, and silently discards a name it is given. `Pane.breakOut()` - works around both. +- Exact tmux 3.7 can end the server on an unnamed `break-pane` and ignores an + explicit name in its multi-pane branch. `Pane.breakOut()` refuses there; + `Pane.breakOut(name)` supplies and then reapplies the explicit name. + +Control-positive cases run from tmux 3.7. Lower matrix lanes retain +ordinary operations and assert that control is refused before attachment. +Named buffer and managed-paste cases assert their own 3.4 boundary. These +feature requirements do not change the core 3.3a minimum. diff --git a/docs/internals/tmux-behaviour.md b/docs/internals/tmux-behaviour.md index c0a362058..46ba95245 100644 --- a/docs/internals/tmux-behaviour.md +++ b/docs/internals/tmux-behaviour.md @@ -1,11 +1,27 @@ # tmux behaviour this library codes around Facts about tmux's own commands, measured against the supported version range -(tmux 3.2a through 3.7c unless a row says otherwise), that the implementation +(tmux 3.3a through 3.7c unless a row says otherwise), that the implementation relies on and that are not self-evident from the tmux manual. A decision that weighs alternatives belongs in [`docs/decisions/`](../decisions/) instead; this page is measurements, kept reachable because source comments cite them. +## Executable version banners on tmux 3.3a + +tmux 3.3a prints the configured executable's basename before its version. +Copies and symlinks named `opaque client` and `opaque-link` report +`opaque client 3.3a` and `opaque-link 3.3a`. The cold-start version check accepts that +exact basename or the canonical `tmux` prefix, followed by a complete version +token. Daemon reports remain bare version tokens. + +## Soft-wrapped capture rows + +On tmux 3.3a and 3.7d, `capture-pane -p` inserts a newline where terminal +output wraps across screen rows. With a 70-character prefix in an 80-column +pane, `batch-first-marker` is captured as `batch-firs\nt-marker` despite +arriving intact. `capture-pane -p -J` joins those rows without joining hard +line breaks; typing tests use it when checking delivered text. + ## break-pane on tmux 3.7 `break-pane` gets automatic window naming wrong in two opposite, silent ways, @@ -101,15 +117,19 @@ anything matched filters a capture instead. ## select-layout can crash the server, on 3.3a only -`select-layout` given a string tmux cannot parse — an unknown built-in name or -a malformed serialized layout — returns an error on every lane except 3.3a, -where the server itself exits, taking every session on that socket with it. -Both the built-in names (checked against an enum) and a serialized layout -string (checked by recomputing tmux's own rotate-and-add checksum over the -layout body, four hex digits at the front) are validated before dispatch, so -this library never hands `select-layout` anything it has not already verified -tmux itself would have written or would recognize. `main-horizontal-mirrored` -and `main-vertical-mirrored` only exist from tmux 3.5. +On tmux 3.3a, an unknown layout without a hexadecimal checksum prefix can +end the server and every session on its socket. `layout_parse` leaves the +error cause unset on that path; upstream commit +[`f10854cf`](https://github.com/tmux/tmux/commit/f10854cfc5e43f646b02b3031bfc4bc3f5f6c513) +initializes it, first released in 3.4. A bad checksum already reports an error +on 3.3a. + +Typed layouts validate built-in names and serialized geometry and checksums. +Literal `select-layout` commands also validate operands before every batch +effect on 3.3a. The check accepts built-in prefixes such as `tile`, skips +operands ignored by `-n`, `-p` or `-E`, and requires a serialized operand with +`-o`. A missing operand keeps tmux's native selection or previous-layout +behavior. Mirrored presets require tmux 3.5. ## %output is cut by byte count, not by character diff --git a/docs/parity/test-map.md b/docs/parity/test-map.md index 9b68ea5c9..79b7c29e4 100644 --- a/docs/parity/test-map.md +++ b/docs/parity/test-map.md @@ -45,198 +45,198 @@ under [`tests/`](https://github.com/tmux-python/libtmux/blob/c4a980b/tests) and | Python evidence | contract | Java test kind | Java test | tmux versions | status | |---|---|---|---|---|---| | README.md::README.md[0] | execute `import libtmux` without exception; execute `svr = libtmux.Server()` without exception; execute `svr` and expect `Server(socket_path=/tmp/tmux-.../default)`. | documentation contract test | PythonBehaviorParityTest#README_md_README_md_0_1e0d7220f0 | not tmux-bound | planned parity | -| README.md::README.md[10] | execute `pane.clear()` and expect `Pane(%... ...)`; execute `pane.send_keys("echo 'hello world'", enter=True)` without exception; source-declared `+SKIP`: do not execute `pane.cmd('capture-pane', '-p').stdout # doctest: +SKIP`; its source text shows `["$ echo 'hello world'", 'hello world', '$']` only as illustrative output. | real-tmux documentation contract test | PythonBehaviorParityTest#README_md_README_md_10_eb2cfcf33e | 3.2a through 3.7b | source-declared partial non-execution | -| README.md::README.md[11] | execute `pane.window` and expect `Window(@... ...:..., Session($... ...))`; execute `pane.window.session` and expect `Session($... ...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#README_md_README_md_11_503180862f | 3.2a through 3.7b | planned parity | -| README.md::README.md[12] | execute `socket_path = server.cmd( "display-message", "-p", "-t", session.session_id, "#{socket_path}" ).stdout[0]` without exception; execute `monkeypatch.setenv("TMUX", f"{socket_path},1,{session.session_id}")` without exception; execute `monkeypatch.setenv("TMUX_PANE", pane.pane_id)` without exception; execute `Pane.from_env()` and expect `Pane(%... ...)`; execute `Session.from_env().session_name == session.session_name` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#README_md_README_md_12_db94bdc4ba | 3.2a through 3.7b | planned parity | -| README.md::README.md[1] | execute `server = Server(socket_name='libtmux_doctest')` without exception; execute `server.cmd('display-message', 'hello world')` and expect `<libtmux...>`. | real-tmux documentation contract test | PythonBehaviorParityTest#README_md_README_md_1_79ce674470 | 3.2a through 3.7b | planned parity | -| README.md::README.md[2] | execute `server.cmd('new-session', '-d', '-P', '-F#{session_id}').stdout[0]` and expect `'$...'`. | real-tmux documentation contract test | PythonBehaviorParityTest#README_md_README_md_2_ebabe7cf4c | 3.2a through 3.7b | planned parity | -| README.md::README.md[3] | execute `server.sessions` and expect `[Session($... ...), ...]`. | real-tmux documentation contract test | PythonBehaviorParityTest#README_md_README_md_3_73f42203fb | 3.2a through 3.7b | planned parity | -| README.md::README.md[4] | execute `server.sessions.filter(history_limit='2000')` and expect `[Session($... ...), ...]`. | real-tmux documentation contract test | PythonBehaviorParityTest#README_md_README_md_4_de4cce9671 | 3.2a through 3.7b | planned parity | -| README.md::README.md[5] | execute `server.sessions.get(session_id=session.session_id)` and expect `Session($... ...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#README_md_README_md_5_f14b350217 | 3.2a through 3.7b | planned parity | -| README.md::README.md[6] | execute `session.rename_session('my-session')` and expect `Session($... my-session)`. | real-tmux documentation contract test | PythonBehaviorParityTest#README_md_README_md_6_d19a3309aa | 3.2a through 3.7b | planned parity | -| README.md::README.md[7] | execute `bg_window = session.new_window(attach=False, window_name="bg-work")` without exception; execute `bg_window` and expect `Window(@... ...:bg-work, Session($... ...))`; execute `session.windows.filter(window_name__startswith="bg")` and expect `[Window(@... ...:bg-work, Session($... ...))]`; execute `session.windows.get(window_name__startswith="bg")` and expect `Window(@... ...:bg-work, Session($... ...))`; execute `bg_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#README_md_README_md_7_adfdedf79b | 3.2a through 3.7b | planned parity | -| README.md::README.md[8] | execute `pane = window.split(attach=False)` without exception; execute `pane` and expect `Pane(%... Window(@... ...:..., Session($... ...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#README_md_README_md_8_b0888fc33a | 3.2a through 3.7b | planned parity | -| README.md::README.md[9] | execute `pane.send_keys('echo hello')` without exception; execute `pane.send_keys('echo hey', enter=False)` without exception; execute `pane.enter()` and expect `Pane(%... ...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#README_md_README_md_9_acbb4d6008 | 3.2a through 3.7b | planned parity | +| README.md::README.md[10] | execute `pane.clear()` and expect `Pane(%... ...)`; execute `pane.send_keys("echo 'hello world'", enter=True)` without exception; source-declared `+SKIP`: do not execute `pane.cmd('capture-pane', '-p').stdout # doctest: +SKIP`; its source text shows `["$ echo 'hello world'", 'hello world', '$']` only as illustrative output. | real-tmux documentation contract test | PythonBehaviorParityTest#README_md_README_md_10_eb2cfcf33e | 3.3a through 3.7c | source-declared partial non-execution | +| README.md::README.md[11] | execute `pane.window` and expect `Window(@... ...:..., Session($... ...))`; execute `pane.window.session` and expect `Session($... ...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#README_md_README_md_11_503180862f | 3.3a through 3.7c | planned parity | +| README.md::README.md[12] | execute `socket_path = server.cmd( "display-message", "-p", "-t", session.session_id, "#{socket_path}" ).stdout[0]` without exception; execute `monkeypatch.setenv("TMUX", f"{socket_path},1,{session.session_id}")` without exception; execute `monkeypatch.setenv("TMUX_PANE", pane.pane_id)` without exception; execute `Pane.from_env()` and expect `Pane(%... ...)`; execute `Session.from_env().session_name == session.session_name` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#README_md_README_md_12_db94bdc4ba | 3.3a through 3.7c | planned parity | +| README.md::README.md[1] | execute `server = Server(socket_name='libtmux_doctest')` without exception; execute `server.cmd('display-message', 'hello world')` and expect `<libtmux...>`. | real-tmux documentation contract test | PythonBehaviorParityTest#README_md_README_md_1_79ce674470 | 3.3a through 3.7c | planned parity | +| README.md::README.md[2] | execute `server.cmd('new-session', '-d', '-P', '-F#{session_id}').stdout[0]` and expect `'$...'`. | real-tmux documentation contract test | PythonBehaviorParityTest#README_md_README_md_2_ebabe7cf4c | 3.3a through 3.7c | planned parity | +| README.md::README.md[3] | execute `server.sessions` and expect `[Session($... ...), ...]`. | real-tmux documentation contract test | PythonBehaviorParityTest#README_md_README_md_3_73f42203fb | 3.3a through 3.7c | planned parity | +| README.md::README.md[4] | execute `server.sessions.filter(history_limit='2000')` and expect `[Session($... ...), ...]`. | real-tmux documentation contract test | PythonBehaviorParityTest#README_md_README_md_4_de4cce9671 | 3.3a through 3.7c | planned parity | +| README.md::README.md[5] | execute `server.sessions.get(session_id=session.session_id)` and expect `Session($... ...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#README_md_README_md_5_f14b350217 | 3.3a through 3.7c | planned parity | +| README.md::README.md[6] | execute `session.rename_session('my-session')` and expect `Session($... my-session)`. | real-tmux documentation contract test | PythonBehaviorParityTest#README_md_README_md_6_d19a3309aa | 3.3a through 3.7c | planned parity | +| README.md::README.md[7] | execute `bg_window = session.new_window(attach=False, window_name="bg-work")` without exception; execute `bg_window` and expect `Window(@... ...:bg-work, Session($... ...))`; execute `session.windows.filter(window_name__startswith="bg")` and expect `[Window(@... ...:bg-work, Session($... ...))]`; execute `session.windows.get(window_name__startswith="bg")` and expect `Window(@... ...:bg-work, Session($... ...))`; execute `bg_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#README_md_README_md_7_adfdedf79b | 3.3a through 3.7c | planned parity | +| README.md::README.md[8] | execute `pane = window.split(attach=False)` without exception; execute `pane` and expect `Pane(%... Window(@... ...:..., Session($... ...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#README_md_README_md_8_b0888fc33a | 3.3a through 3.7c | planned parity | +| README.md::README.md[9] | execute `pane.send_keys('echo hello')` without exception; execute `pane.send_keys('echo hey', enter=False)` without exception; execute `pane.enter()` and expect `Pane(%... ...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#README_md_README_md_9_acbb4d6008 | 3.3a through 3.7c | planned parity | | docs/api/libtmux.neo.md::docs/api/libtmux.neo.md[0] | execute `import libtmux` without exception. | documentation contract test | PythonBehaviorParityTest#docs_api_libtmux_neo_md_docs_api_libtmux_neo_md_0_30d3d26530 | not tmux-bound | planned parity | -| docs/api/libtmux.neo.md::docs/api/libtmux.neo.md[10] | execute `pane.pane_current_command` and expect `'...'`; execute `type(pane.pane_current_command)` and expect `<class 'str'>`; execute `pane.pane_height` and expect `'...'`; execute `pane.pane_width` and expect `'...'`; execute `pane.pane_index` and expect `'0'`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_api_libtmux_neo_md_docs_api_libtmux_neo_md_10_64f6b0dcbb | 3.2a through 3.7b | planned parity | +| docs/api/libtmux.neo.md::docs/api/libtmux.neo.md[10] | execute `pane.pane_current_command` and expect `'...'`; execute `type(pane.pane_current_command)` and expect `<class 'str'>`; execute `pane.pane_height` and expect `'...'`; execute `pane.pane_width` and expect `'...'`; execute `pane.pane_index` and expect `'0'`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_api_libtmux_neo_md_docs_api_libtmux_neo_md_10_64f6b0dcbb | 3.3a through 3.7c | planned parity | | docs/api/libtmux.neo.md::docs/api/libtmux.neo.md[1] | execute `import libtmux` without exception; execute `t = libtmux.Server()` without exception; execute `t` and expect `Server(socket_path=/tmp/tmux-.../default)`. | documentation contract test | PythonBehaviorParityTest#docs_api_libtmux_neo_md_docs_api_libtmux_neo_md_1_6866ce96a6 | not tmux-bound | planned parity | -| docs/api/libtmux.neo.md::docs/api/libtmux.neo.md[2] | execute `session = server.sessions[0]` without exception; execute `session` and expect `Session($1 libtmux_...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_api_libtmux_neo_md_docs_api_libtmux_neo_md_2_c14aa4141f | 3.2a through 3.7b | planned parity | -| docs/api/libtmux.neo.md::docs/api/libtmux.neo.md[3] | execute `session.session_name` and expect `'libtmux_...'`; execute `session.session_id` and expect `'$1'`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_api_libtmux_neo_md_docs_api_libtmux_neo_md_3_f64b65ac31 | 3.2a through 3.7b | planned parity | +| docs/api/libtmux.neo.md::docs/api/libtmux.neo.md[2] | execute `session = server.sessions[0]` without exception; execute `session` and expect `Session($1 libtmux_...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_api_libtmux_neo_md_docs_api_libtmux_neo_md_2_c14aa4141f | 3.3a through 3.7c | planned parity | +| docs/api/libtmux.neo.md::docs/api/libtmux.neo.md[3] | execute `session.session_name` and expect `'libtmux_...'`; execute `session.session_id` and expect `'$1'`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_api_libtmux_neo_md_docs_api_libtmux_neo_md_3_f64b65ac31 | 3.3a through 3.7c | planned parity | | docs/api/libtmux.neo.md::docs/api/libtmux.neo.md[4] | execute `from libtmux.neo import Obj` without exception; execute `sorted(Obj.__dataclass_fields__)[:3]` and expect `['active_window_index', 'alternate_saved_x', 'alternate_saved_y']`. | documentation contract test | PythonBehaviorParityTest#docs_api_libtmux_neo_md_docs_api_libtmux_neo_md_4_2076265703 | not tmux-bound | planned parity | -| docs/api/libtmux.neo.md::docs/api/libtmux.neo.md[5] | execute `session.session_windows` and expect `'...'`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_api_libtmux_neo_md_docs_api_libtmux_neo_md_5_7b844bc7cc | 3.2a through 3.7b | planned parity | -| docs/api/libtmux.neo.md::docs/api/libtmux.neo.md[6] | execute `window = session.active_window` without exception; execute `window` and expect `Window(@1 ...:..., Session($1 ...))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_api_libtmux_neo_md_docs_api_libtmux_neo_md_6_ec88d14a85 | 3.2a through 3.7b | planned parity | -| docs/api/libtmux.neo.md::docs/api/libtmux.neo.md[7] | execute `window.window_name` and expect `'...'`; execute `window.window_id` and expect `'@1'`; execute `window.window_height` and expect `'...'`; execute `window.window_width` and expect `'...'`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_api_libtmux_neo_md_docs_api_libtmux_neo_md_7_a017914423 | 3.2a through 3.7b | planned parity | -| docs/api/libtmux.neo.md::docs/api/libtmux.neo.md[8] | execute `window.window_panes` and expect `'1'`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_api_libtmux_neo_md_docs_api_libtmux_neo_md_8_e39239d3c5 | 3.2a through 3.7b | planned parity | -| docs/api/libtmux.neo.md::docs/api/libtmux.neo.md[9] | execute `pane = window.active_pane` without exception; execute `pane` and expect `Pane(%1 Window(@1 ...:..., Session($1 libtmux_...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_api_libtmux_neo_md_docs_api_libtmux_neo_md_9_e4665aaa3b | 3.2a through 3.7b | planned parity | -| docs/api/testing/pytest-plugin/fixtures.md::docs/api/testing/pytest-plugin/fixtures.md[0] | execute `created_session = server.new_session(session_name="my-session")` without exception; execute `created_session is not None` and expect `True`; execute `created_session.kill()` without exception; execute `created_window = session.new_window(window_name="test")` without exception; execute `created_window is not None` and expect `True`; execute `created_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#g_pytest_plugin_fixtures_md_docs_api_testing_pytest_plugin_fixtures_md_0_3f58feb3b4 | 3.2a through 3.7b | planned parity | +| docs/api/libtmux.neo.md::docs/api/libtmux.neo.md[5] | execute `session.session_windows` and expect `'...'`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_api_libtmux_neo_md_docs_api_libtmux_neo_md_5_7b844bc7cc | 3.3a through 3.7c | planned parity | +| docs/api/libtmux.neo.md::docs/api/libtmux.neo.md[6] | execute `window = session.active_window` without exception; execute `window` and expect `Window(@1 ...:..., Session($1 ...))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_api_libtmux_neo_md_docs_api_libtmux_neo_md_6_ec88d14a85 | 3.3a through 3.7c | planned parity | +| docs/api/libtmux.neo.md::docs/api/libtmux.neo.md[7] | execute `window.window_name` and expect `'...'`; execute `window.window_id` and expect `'@1'`; execute `window.window_height` and expect `'...'`; execute `window.window_width` and expect `'...'`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_api_libtmux_neo_md_docs_api_libtmux_neo_md_7_a017914423 | 3.3a through 3.7c | planned parity | +| docs/api/libtmux.neo.md::docs/api/libtmux.neo.md[8] | execute `window.window_panes` and expect `'1'`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_api_libtmux_neo_md_docs_api_libtmux_neo_md_8_e39239d3c5 | 3.3a through 3.7c | planned parity | +| docs/api/libtmux.neo.md::docs/api/libtmux.neo.md[9] | execute `pane = window.active_pane` without exception; execute `pane` and expect `Pane(%1 Window(@1 ...:..., Session($1 libtmux_...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_api_libtmux_neo_md_docs_api_libtmux_neo_md_9_e4665aaa3b | 3.3a through 3.7c | planned parity | +| docs/api/testing/pytest-plugin/fixtures.md::docs/api/testing/pytest-plugin/fixtures.md[0] | execute `created_session = server.new_session(session_name="my-session")` without exception; execute `created_session is not None` and expect `True`; execute `created_session.kill()` without exception; execute `created_window = session.new_window(window_name="test")` without exception; execute `created_window is not None` and expect `True`; execute `created_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#g_pytest_plugin_fixtures_md_docs_api_testing_pytest_plugin_fixtures_md_0_3f58feb3b4 | 3.3a through 3.7c | planned parity | | docs/api/testing/pytest-plugin/usage.md::docs/api/testing/pytest-plugin/usage.md[0] | execute `import pytest` without exception; execute `@pytest.fixture def session_params() -> dict[str, int]: return {"x": 800, "y": 600}` without exception. | documentation contract test | PythonBehaviorParityTest#testing_pytest_plugin_usage_md_docs_api_testing_pytest_plugin_usage_md_0_5015659fc1 | not tmux-bound | planned parity | | docs/api/testing/pytest-plugin/usage.md::docs/api/testing/pytest-plugin/usage.md[1] | execute `temp_server = Server()` without exception; execute `temp_session = temp_server.new_session()` without exception; execute `temp_server.is_alive()` and expect `True`; execute `temp_server.kill()` without exception. | documentation contract test | PythonBehaviorParityTest#testing_pytest_plugin_usage_md_docs_api_testing_pytest_plugin_usage_md_1_170fae20e8 | not tmux-bound | planned parity | | docs/api/testing/pytest-plugin/usage.md::docs/api/testing/pytest-plugin/usage.md[2] | execute `config_path = request.getfixturevalue("tmp_path") / "tmux.conf"` without exception; execute `_ = config_path.write_text("set -g status off")` without exception; execute `configured_server = Server(config_file=str(config_path))` without exception; execute `_ = configured_server.new_session()` without exception; execute `configured_server.is_alive()` and expect `True`; execute `configured_server.kill()` without exception. | documentation contract test | PythonBehaviorParityTest#testing_pytest_plugin_usage_md_docs_api_testing_pytest_plugin_usage_md_2_2c18b42b8b | not tmux-bound | planned parity | | docs/api/testing/pytest-plugin/usage.md::docs/api/testing/pytest-plugin/usage.md[3] | execute `import pathlib` without exception; execute `import pytest` without exception; execute `@pytest.fixture(autouse=True, scope="function") def set_home( monkeypatch: pytest.MonkeyPatch, user_path: pathlib.Path, ) -> None: monkeypatch.setenv("HOME", str(user_path))` without exception. | documentation contract test | PythonBehaviorParityTest#testing_pytest_plugin_usage_md_docs_api_testing_pytest_plugin_usage_md_3_040e32b95f | not tmux-bound | planned parity | -| docs/index.md::docs/index.md[0] | execute `demo_window = session.new_window(window_name="my-project")` without exception; execute `demo_pane = demo_window.active_pane` without exception; execute `demo_pane.send_keys("echo hello")` without exception; execute `demo_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_index_md_docs_index_md_0_5002a6845e | 3.2a through 3.7b | planned parity | -| docs/index.md::docs/index.md[1] | execute `socket_path = server.cmd( "display-message", "-p", "-t", session.session_id, "#{socket_path}" ).stdout[0]` without exception; execute `monkeypatch.setenv("TMUX", f"{socket_path},1,{session.session_id}")` without exception; execute `monkeypatch.setenv("TMUX_PANE", pane.pane_id)` without exception; execute `Pane.from_env().pane_id == pane.pane_id` and expect `True`; execute `Session.from_env().session_name == session.session_name` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_index_md_docs_index_md_1_b6f73301e4 | 3.2a through 3.7b | planned parity | -| docs/index.md::docs/index.md[2] | execute `test_window = session.new_window(window_name="test")` without exception; execute `test_pane = test_window.active_pane` without exception; execute `test_pane.send_keys("echo hello")` without exception; execute `test_window.window_name` and expect `'test'`; execute `test_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_index_md_docs_index_md_2_9cef31ef79 | 3.2a through 3.7b | planned parity | -| docs/quickstart.md::docs/quickstart.md[0] | execute `import libtmux` without exception; execute `server = libtmux.Server()` without exception; execute `server` and expect `Server(socket_path=/tmp/tmux-.../default)`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_0_5c4e9027f7 | 3.2a through 3.7b | planned parity | -| docs/quickstart.md::docs/quickstart.md[10] | execute `server.sessions[0].rename_session('foo')` and expect `Session($1 foo)`; execute `session = server.sessions.get(session_name="foo")` without exception; execute `session` and expect `Session($1 foo)`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_10_1ad96b2cc0 | 3.2a through 3.7b | planned parity | -| docs/quickstart.md::docs/quickstart.md[11] | execute `session.new_window(attach=False, window_name="ha in the bg")` and expect `Window(@2 ...:ha in the bg, Session($1 ...))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_11_4a56cd3fb5 | 3.2a through 3.7b | planned parity | -| docs/quickstart.md::docs/quickstart.md[12] | execute `session.kill_window(window.window_id)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_12_b0b3d61124 | 3.2a through 3.7b | planned parity | -| docs/quickstart.md::docs/quickstart.md[13] | execute `session.new_window(attach=False, window_name="ha in the bg")` and expect `Window(@2 ...:ha in the bg, Session($1 ...))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_13_94d0ffffb2 | 3.2a through 3.7b | planned parity | -| docs/quickstart.md::docs/quickstart.md[14] | execute `session.new_window(attach=False, window_name="ha in the bg")` and expect `Window(@2 ...:ha in the bg, Session($1 ...))`; execute `session.kill_window('ha in the bg')` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_14_4c829f0a75 | 3.2a through 3.7b | planned parity | -| docs/quickstart.md::docs/quickstart.md[15] | execute `window = session.new_window(attach=False, window_name="check this out")` without exception; execute `window` and expect `Window(@2 2:check this out, Session($1 ...))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_15_d3b0a6f018 | 3.2a through 3.7b | planned parity | -| docs/quickstart.md::docs/quickstart.md[16] | execute `window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_16_e5f13e6688 | 3.2a through 3.7b | planned parity | -| docs/quickstart.md::docs/quickstart.md[17] | execute `window = session.active_window` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_17_b87610e4e3 | 3.2a through 3.7b | planned parity | -| docs/quickstart.md::docs/quickstart.md[18] | execute `window.split(attach=False)` and expect `Pane(%2 Window(@1 ...:..., Session($1 ...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_18_9d4e813a76 | 3.2a through 3.7b | planned parity | -| docs/quickstart.md::docs/quickstart.md[19] | execute `window.rename_window('libtmuxower')` and expect `Window(@1 ...:..., Session($1 ...))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_19_1ed0784542 | 3.2a through 3.7b | planned parity | -| docs/quickstart.md::docs/quickstart.md[1] | execute `server.cmd('new-session', '-d', '-P', '-F#{session_id}').stdout[0]` and expect `'$2'`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_1_c7d3e620a3 | 3.2a through 3.7b | planned parity | -| docs/quickstart.md::docs/quickstart.md[20] | execute `pane = window.split()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_20_3a6eae8369 | 3.2a through 3.7b | planned parity | -| docs/quickstart.md::docs/quickstart.md[21] | execute `pane = window.split(attach=False)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_21_7445c300b3 | 3.2a through 3.7b | planned parity | -| docs/quickstart.md::docs/quickstart.md[22] | execute `pane.select()` and expect `Pane(%1 Window(@1 ...:..., Session($1 ...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_22_d8b57efd93 | 3.2a through 3.7b | planned parity | -| docs/quickstart.md::docs/quickstart.md[23] | execute `window = session.new_window(attach=False, window_name="test")` without exception; execute `pane = window.split(attach=False)` without exception; execute `pane.send_keys('echo hey', enter=False)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_23_1f33e5b331 | 3.2a through 3.7b | planned parity | -| docs/quickstart.md::docs/quickstart.md[24] | execute `pane.enter()` and expect `Pane(%1 ...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_24_1200606fdf | 3.2a through 3.7b | planned parity | -| docs/quickstart.md::docs/quickstart.md[25] | execute `pane.send_keys('echo Howdy', enter=True, suppress_history=True)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_25_08ec8ee4a0 | 3.2a through 3.7b | planned parity | -| docs/quickstart.md::docs/quickstart.md[26] | execute `server.show_option('buffer-limit')` and expect `50`; execute `window.show_options() # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `{...}`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_26_7798619390 | 3.2a through 3.7b | planned parity | -| docs/quickstart.md::docs/quickstart.md[27] | execute `window.set_option('automatic-rename', False) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...)`; execute `window.show_option('automatic-rename')` and expect `False`; execute `window.unset_option('automatic-rename') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_27_c16e06c8e8 | 3.2a through 3.7b | planned parity | -| docs/quickstart.md::docs/quickstart.md[2] | execute `session.cmd('new-window', '-P').stdout[0]` and expect `'libtmux...:2.0'`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_2_868a54960d | 3.2a through 3.7b | planned parity | -| docs/quickstart.md::docs/quickstart.md[3] | execute `Window.from_window_id(window_id=session.cmd('new-window', '-P', '-F#{window_id}').stdout[0], server=session.server)` and expect `Window(@2 2:..., Session($1 libtmux_...))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_3_469dba23a7 | 3.2a through 3.7b | planned parity | -| docs/quickstart.md::docs/quickstart.md[4] | execute `window.cmd('split-window', '-P', '-F#{pane_id}').stdout[0]` and expect `'%2'`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_4_f2abec70e2 | 3.2a through 3.7b | planned parity | -| docs/quickstart.md::docs/quickstart.md[5] | execute `Pane.from_pane_id(pane_id=window.cmd('split-window', '-P', '-F#{pane_id}').stdout[0], server=window.server)` and expect `Pane(%... Window(@1 1:..., Session($1 libtmux_...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_5_7bc3e3ea98 | 3.2a through 3.7b | planned parity | -| docs/quickstart.md::docs/quickstart.md[6] | execute `server.sessions` and expect `[Session($1 ...), Session($0 ...)]`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_6_e1cefc99b0 | 3.2a through 3.7b | planned parity | -| docs/quickstart.md::docs/quickstart.md[7] | execute `server.sessions[0]` and expect `Session($1 ...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_7_994dd7180c | 3.2a through 3.7b | planned parity | -| docs/quickstart.md::docs/quickstart.md[8] | execute `server.sessions.filter(session_id='$1')[0]` and expect `Session($1 ...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_8_ed04d31709 | 3.2a through 3.7b | planned parity | -| docs/quickstart.md::docs/quickstart.md[9] | execute `server.sessions[0].rename_session('foo')` and expect `Session($1 foo)`; execute `server.sessions.filter(session_name="foo")[0]` and expect `Session($1 foo)`; execute `server.sessions.get(session_name="foo")` and expect `Session($1 foo)`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_9_7aae9f19ec | 3.2a through 3.7b | planned parity | -| docs/topics/automation_patterns.md::docs/topics/automation_patterns.md[0] | execute `import time` without exception; execute `proc_window = session.new_window(window_name='process', attach=False)` without exception; execute `proc_pane = proc_window.active_pane` without exception; execute `proc_pane.send_keys('sleep 2 && echo "Process complete"')` without exception; execute `time.sleep(0.1)` without exception; execute `proc_window.window_name` and expect `'process'`; execute `proc_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_automation_patterns_md_docs_topics_automation_patterns_md_0_887870afdc | 3.2a through 3.7b | planned parity | -| docs/topics/automation_patterns.md::docs/topics/automation_patterns.md[10] | execute `import time` without exception; execute `retry_window = session.new_window(window_name='retry-demo', attach=False)` without exception; execute `retry_pane = retry_window.active_pane` without exception; execute `def retry_until_success(pane, command, success_marker, max_retries=3, delay=0.5): """Retry command until success marker appears.""" for attempt in range(max_retries): pane.send_keys(command) time.sleep(delay) output = '\\n'.join(pane.capture_pane()) if success_marker in output: return True, attempt + 1 return False, max_retries` without exception; execute `success, attempts = retry_until_success( retry_pane, 'echo "OK"', 'OK', max_retries=3, delay=0.2 )` without exception; execute `success` and expect `True`; execute `attempts` and expect `1`; execute `retry_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_automation_patterns_md_docs_topics_automation_patterns_md_10_4b90629469 | 3.2a through 3.7b | planned parity | -| docs/topics/automation_patterns.md::docs/topics/automation_patterns.md[11] | execute `import time` without exception; execute `queue_window = session.new_window(window_name='queue', attach=False)` without exception; execute `queue_pane = queue_window.active_pane` without exception; execute `def process_task_queue(pane, tasks, completion_marker='TASK_DONE'): """Process a queue of tasks sequentially.""" results = [] for i, task in enumerate(tasks): pane.send_keys(f'{task}; echo "{completion_marker}_{i}"') # Wait for this task to complete start = time.time() while time.time() - start < 5.0: output = '\\n'.join(pane.capture_pane()) if f'{completion_marker}_{i}' in output: results.append((i, True)) break time.sleep(0.1) else: results.append((i, False)) return results` without exception; execute `tasks = ['echo "Step 1"', 'echo "Step 2"', 'echo "Step 3"']` without exception; execute `results = process_task_queue(queue_pane, tasks)` without exception; execute `all(success for _, success in results)` and expect `True`; execute `queue_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_automation_patterns_md_docs_topics_automation_patterns_md_11_9b0ee172d9 | 3.2a through 3.7b | planned parity | -| docs/topics/automation_patterns.md::docs/topics/automation_patterns.md[12] | execute `import time` without exception; execute `state_window = session.new_window(window_name='state-machine', attach=False)` without exception; execute `state_pane = state_window.active_pane` without exception; execute `def run_state_machine(pane, states, timeout_per_state=2.0): """Run through a series of states with transitions.""" current_state = 0 history = [] while current_state < len(states): state_name, command, next_marker = states[current_state] pane.send_keys(command) start = time.time() while time.time() - start < timeout_per_state: output = '\\n'.join(pane.capture_pane()) if next_marker in output: history.append(state_name) current_state += 1 break time.sleep(0.1) else: return history, False # Timeout return history, True` without exception; execute `states = [ ('init', 'echo "INIT_DONE"', 'INIT_DONE'), ('process', 'echo "PROCESS_DONE"', 'PROCESS_DONE'), ('cleanup', 'echo "CLEANUP_DONE"', 'CLEANUP_DONE'), ]` without exception; execute `history, success = run_state_machine(state_pane, states)` without exception; execute `success` and expect `True`; execute `len(history)` and expect `3`; execute `state_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_automation_patterns_md_docs_topics_automation_patterns_md_12_0019f56ba3 | 3.2a through 3.7b | planned parity | -| docs/topics/automation_patterns.md::docs/topics/automation_patterns.md[13] | execute `bp_window = session.new_window(window_name='best-practice', attach=False)` without exception; execute `bp_pane = bp_window.active_pane` without exception; execute `bp_pane.send_keys('long_command; echo "__DONE__"')` without exception; execute `import time` without exception; execute `time.sleep(0.2)` without exception; execute `'__DONE__' in '\\n'.join(bp_pane.capture_pane())` and expect `True`; execute `bp_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_automation_patterns_md_docs_topics_automation_patterns_md_13_263a0a7422 | 3.2a through 3.7b | planned parity | -| docs/topics/automation_patterns.md::docs/topics/automation_patterns.md[14] | execute `cleanup_window = session.new_window(window_name='cleanup-demo', attach=False)` without exception; execute `cleanup_window # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...)`; execute `cleanup_window.kill()` without exception; execute `cleanup_window not in session.windows` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_automation_patterns_md_docs_topics_automation_patterns_md_14_51c82f6dd5 | 3.2a through 3.7b | planned parity | -| docs/topics/automation_patterns.md::docs/topics/automation_patterns.md[15] | execute `with session.new_window(window_name='safe-work') as safe_window: pane = safe_window.active_pane # Work happens here pass # Even if exception occurs, window is cleaned up` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_automation_patterns_md_docs_topics_automation_patterns_md_15_d080f809da | 3.2a through 3.7b | planned parity | -| docs/topics/automation_patterns.md::docs/topics/automation_patterns.md[1] | execute `import time` without exception; execute `status_window = session.new_window(window_name='status-check', attach=False)` without exception; execute `status_pane = status_window.active_pane` without exception; execute `def is_process_running(pane, marker='RUNNING'): """Check if a marker indicates process is still running.""" output = pane.capture_pane() return marker in '\\n'.join(output)` without exception; execute `status_pane.send_keys('echo "RUNNING"; sleep 0.3; echo "DONE"')` without exception; execute `time.sleep(0.1)` without exception; execute `'RUNNING' in '\\n'.join(status_pane.capture_pane())` and expect `True`; execute `time.sleep(0.5)` without exception; execute `'DONE' in '\\n'.join(status_pane.capture_pane())` and expect `True`; execute `status_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_automation_patterns_md_docs_topics_automation_patterns_md_1_091b7723b5 | 3.2a through 3.7b | planned parity | -| docs/topics/automation_patterns.md::docs/topics/automation_patterns.md[2] | execute `import time` without exception; execute `monitor_window = session.new_window(window_name='monitor', attach=False)` without exception; execute `monitor_pane = monitor_window.active_pane` without exception; execute `def wait_for_output(pane, text, timeout=5.0, poll_interval=0.1): """Wait for specific text to appear in pane output.""" start = time.time() while time.time() - start < timeout: output = '\\n'.join(pane.capture_pane()) if text in output: return True time.sleep(poll_interval) return False` without exception; execute `monitor_pane.send_keys('sleep 0.2; echo "READY"')` without exception; execute `wait_for_output(monitor_pane, 'READY', timeout=2.0)` and expect `True`; execute `monitor_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_automation_patterns_md_docs_topics_automation_patterns_md_2_efd039245a | 3.2a through 3.7b | planned parity | -| docs/topics/automation_patterns.md::docs/topics/automation_patterns.md[3] | execute `import time` without exception; execute `error_window = session.new_window(window_name='error-check', attach=False)` without exception; execute `error_pane = error_window.active_pane` without exception; execute `def check_for_errors(pane, patterns=None): """Check pane output for error patterns.""" if patterns is None: patterns = ['Error:', 'error:', 'ERROR', 'FAILED', 'Exception'] output = '\\n'.join(pane.capture_pane()) for pattern in patterns: if pattern in output: return pattern return None` without exception; execute `error_pane.send_keys('echo "Success!"')` without exception; execute `time.sleep(0.1)` without exception; execute `check_for_errors(error_pane) is None` and expect `True`; execute `error_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_automation_patterns_md_docs_topics_automation_patterns_md_3_962186aff6 | 3.2a through 3.7b | planned parity | -| docs/topics/automation_patterns.md::docs/topics/automation_patterns.md[4] | execute `import time` without exception; execute `capture_window = session.new_window(window_name='capture', attach=False)` without exception; execute `capture_pane = capture_window.active_pane` without exception; execute `def capture_after_marker(pane, marker, timeout=5.0): """Capture output after a marker appears.""" start_time = time.time() while time.time() - start_time < timeout: lines = pane.capture_pane() output = '\\n'.join(lines) if marker in output: # Return all lines after the marker found = False result = [] for line in lines: if marker in line: found = True continue if found: result.append(line) return result time.sleep(0.1) return None` without exception; execute `capture_pane.send_keys('echo "MARKER"; echo "captured data"')` without exception; execute `time.sleep(0.3)` without exception; execute `result = capture_after_marker(capture_pane, 'MARKER', timeout=2.0)` without exception; execute `any('captured' in line for line in (result or []))` and expect `True`; execute `capture_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_automation_patterns_md_docs_topics_automation_patterns_md_4_5d507b840e | 3.2a through 3.7b | planned parity | -| docs/topics/automation_patterns.md::docs/topics/automation_patterns.md[5] | execute `import time` without exception; execute `from libtmux.constants import PaneDirection` without exception; execute `parallel_window = session.new_window(window_name='parallel', attach=False)` without exception; execute `parallel_window.resize(height=40, width=120) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...)`; execute `pane1 = parallel_window.active_pane` without exception; execute `pane2 = pane1.split(direction=PaneDirection.Right)` without exception; execute `pane3 = pane1.split(direction=PaneDirection.Below)` without exception; execute `tasks = [ (pane1, 'echo "Task 1"; sleep 0.2; echo "DONE1"'), (pane2, 'echo "Task 2"; sleep 0.1; echo "DONE2"'), (pane3, 'echo "Task 3"; sleep 0.3; echo "DONE3"'), ]` without exception; execute `for pane, cmd in tasks: pane.send_keys(cmd)` without exception; execute `time.sleep(0.5)` without exception; execute `all('DONE' in '\\n'.join(p.capture_pane()) for p, _ in tasks)` and expect `True`; execute `parallel_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_automation_patterns_md_docs_topics_automation_patterns_md_5_26c1bc29c6 | 3.2a through 3.7b | planned parity | -| docs/topics/automation_patterns.md::docs/topics/automation_patterns.md[6] | execute `import time` without exception; execute `from libtmux.constants import PaneDirection` without exception; execute `multi_window = session.new_window(window_name='multi-monitor', attach=False)` without exception; execute `multi_window.resize(height=40, width=120) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...)`; execute `panes = [multi_window.active_pane]` without exception; execute `panes.append(panes[0].split(direction=PaneDirection.Right))` without exception; execute `panes.append(panes[0].split(direction=PaneDirection.Below))` without exception; execute `def wait_all_complete(panes, marker='COMPLETE', timeout=10.0): """Wait for all panes to show completion marker.""" start = time.time() remaining = set(range(len(panes))) while remaining and time.time() - start < timeout: for i in list(remaining): if marker in '\\n'.join(panes[i].capture_pane()): remaining.remove(i) time.sleep(0.1) return len(remaining) == 0` without exception; execute `for i, pane in enumerate(panes): pane.send_keys(f'sleep 0.{i+1}; echo "COMPLETE"')` without exception; execute `wait_all_complete(panes, 'COMPLETE', timeout=2.0)` and expect `True`; execute `multi_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_automation_patterns_md_docs_topics_automation_patterns_md_6_431ba17d12 | 3.2a through 3.7b | planned parity | -| docs/topics/automation_patterns.md::docs/topics/automation_patterns.md[7] | execute `with server.new_session(session_name='temp-work') as temp_session: window = temp_session.new_window(window_name='task') pane = window.active_pane pane.send_keys('echo "Isolated work"') # Session exists during work temp_session in server.sessions` and expect `True`; execute `temp_session not in server.sessions` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_automation_patterns_md_docs_topics_automation_patterns_md_7_b244ac24ec | 3.2a through 3.7b | planned parity | -| docs/topics/automation_patterns.md::docs/topics/automation_patterns.md[8] | execute `import time` without exception; execute `with session.new_window(window_name='subtask') as sub_window: pane = sub_window.active_pane pane.send_keys('echo "Subtask running"') time.sleep(0.1) 'Subtask' in '\\n'.join(pane.capture_pane())` and expect `True`; execute `sub_window not in session.windows` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_automation_patterns_md_docs_topics_automation_patterns_md_8_f7764b8715 | 3.2a through 3.7b | planned parity | -| docs/topics/automation_patterns.md::docs/topics/automation_patterns.md[9] | execute `import time` without exception; execute `timeout_window = session.new_window(window_name='timeout-demo', attach=False)` without exception; execute `timeout_pane = timeout_window.active_pane` without exception; execute `class CommandTimeout(Exception): """Raised when a command times out.""" pass` without exception; execute `def run_with_timeout(pane, command, marker='__DONE__', timeout=5.0): """Run command and wait for completion with timeout.""" pane.send_keys(f'{command}; echo {marker}') start = time.time() while time.time() - start < timeout: output = '\\n'.join(pane.capture_pane()) if marker in output: return output time.sleep(0.1) raise CommandTimeout(f'Command timed out after {timeout}s')` without exception; execute `result = run_with_timeout(timeout_pane, 'echo "fast"', timeout=2.0)` without exception; execute `'fast' in result` and expect `True`; execute `timeout_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_automation_patterns_md_docs_topics_automation_patterns_md_9_d60e07d979 | 3.2a through 3.7b | planned parity | -| docs/topics/clients.md::docs/topics/clients.md[0] | execute `with control_mode() as ctl: client = server.clients.get(client_name=ctl.client_name) attached = client.attached_session` without exception; execute `attached is not None` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_clients_md_docs_topics_clients_md_0_87f0d41a5d | 3.2a through 3.7b | planned parity | -| docs/topics/clients.md::docs/topics/clients.md[1] | execute `with control_mode() as ctl: client = server.clients.get(client_name=ctl.client_name) pane = client.attached_pane` without exception; execute `pane is None or pane.pane_id.startswith('%')` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_clients_md_docs_topics_clients_md_1_0ff98b00d9 | 3.2a through 3.7b | planned parity | -| docs/topics/clients.md::docs/topics/clients.md[2] | execute `with control_mode() as ctl: attached = [ c for c in server.clients if c.client_name == ctl.client_name ]` without exception; execute `bool(attached)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_clients_md_docs_topics_clients_md_2_99d2e27e61 | 3.2a through 3.7b | planned parity | +| docs/index.md::docs/index.md[0] | execute `demo_window = session.new_window(window_name="my-project")` without exception; execute `demo_pane = demo_window.active_pane` without exception; execute `demo_pane.send_keys("echo hello")` without exception; execute `demo_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_index_md_docs_index_md_0_5002a6845e | 3.3a through 3.7c | planned parity | +| docs/index.md::docs/index.md[1] | execute `socket_path = server.cmd( "display-message", "-p", "-t", session.session_id, "#{socket_path}" ).stdout[0]` without exception; execute `monkeypatch.setenv("TMUX", f"{socket_path},1,{session.session_id}")` without exception; execute `monkeypatch.setenv("TMUX_PANE", pane.pane_id)` without exception; execute `Pane.from_env().pane_id == pane.pane_id` and expect `True`; execute `Session.from_env().session_name == session.session_name` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_index_md_docs_index_md_1_b6f73301e4 | 3.3a through 3.7c | planned parity | +| docs/index.md::docs/index.md[2] | execute `test_window = session.new_window(window_name="test")` without exception; execute `test_pane = test_window.active_pane` without exception; execute `test_pane.send_keys("echo hello")` without exception; execute `test_window.window_name` and expect `'test'`; execute `test_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_index_md_docs_index_md_2_9cef31ef79 | 3.3a through 3.7c | planned parity | +| docs/quickstart.md::docs/quickstart.md[0] | execute `import libtmux` without exception; execute `server = libtmux.Server()` without exception; execute `server` and expect `Server(socket_path=/tmp/tmux-.../default)`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_0_5c4e9027f7 | 3.3a through 3.7c | planned parity | +| docs/quickstart.md::docs/quickstart.md[10] | execute `server.sessions[0].rename_session('foo')` and expect `Session($1 foo)`; execute `session = server.sessions.get(session_name="foo")` without exception; execute `session` and expect `Session($1 foo)`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_10_1ad96b2cc0 | 3.3a through 3.7c | planned parity | +| docs/quickstart.md::docs/quickstart.md[11] | execute `session.new_window(attach=False, window_name="ha in the bg")` and expect `Window(@2 ...:ha in the bg, Session($1 ...))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_11_4a56cd3fb5 | 3.3a through 3.7c | planned parity | +| docs/quickstart.md::docs/quickstart.md[12] | execute `session.kill_window(window.window_id)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_12_b0b3d61124 | 3.3a through 3.7c | planned parity | +| docs/quickstart.md::docs/quickstart.md[13] | execute `session.new_window(attach=False, window_name="ha in the bg")` and expect `Window(@2 ...:ha in the bg, Session($1 ...))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_13_94d0ffffb2 | 3.3a through 3.7c | planned parity | +| docs/quickstart.md::docs/quickstart.md[14] | execute `session.new_window(attach=False, window_name="ha in the bg")` and expect `Window(@2 ...:ha in the bg, Session($1 ...))`; execute `session.kill_window('ha in the bg')` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_14_4c829f0a75 | 3.3a through 3.7c | planned parity | +| docs/quickstart.md::docs/quickstart.md[15] | execute `window = session.new_window(attach=False, window_name="check this out")` without exception; execute `window` and expect `Window(@2 2:check this out, Session($1 ...))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_15_d3b0a6f018 | 3.3a through 3.7c | planned parity | +| docs/quickstart.md::docs/quickstart.md[16] | execute `window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_16_e5f13e6688 | 3.3a through 3.7c | planned parity | +| docs/quickstart.md::docs/quickstart.md[17] | execute `window = session.active_window` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_17_b87610e4e3 | 3.3a through 3.7c | planned parity | +| docs/quickstart.md::docs/quickstart.md[18] | execute `window.split(attach=False)` and expect `Pane(%2 Window(@1 ...:..., Session($1 ...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_18_9d4e813a76 | 3.3a through 3.7c | planned parity | +| docs/quickstart.md::docs/quickstart.md[19] | execute `window.rename_window('libtmuxower')` and expect `Window(@1 ...:..., Session($1 ...))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_19_1ed0784542 | 3.3a through 3.7c | planned parity | +| docs/quickstart.md::docs/quickstart.md[1] | execute `server.cmd('new-session', '-d', '-P', '-F#{session_id}').stdout[0]` and expect `'$2'`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_1_c7d3e620a3 | 3.3a through 3.7c | planned parity | +| docs/quickstart.md::docs/quickstart.md[20] | execute `pane = window.split()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_20_3a6eae8369 | 3.3a through 3.7c | planned parity | +| docs/quickstart.md::docs/quickstart.md[21] | execute `pane = window.split(attach=False)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_21_7445c300b3 | 3.3a through 3.7c | planned parity | +| docs/quickstart.md::docs/quickstart.md[22] | execute `pane.select()` and expect `Pane(%1 Window(@1 ...:..., Session($1 ...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_22_d8b57efd93 | 3.3a through 3.7c | planned parity | +| docs/quickstart.md::docs/quickstart.md[23] | execute `window = session.new_window(attach=False, window_name="test")` without exception; execute `pane = window.split(attach=False)` without exception; execute `pane.send_keys('echo hey', enter=False)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_23_1f33e5b331 | 3.3a through 3.7c | planned parity | +| docs/quickstart.md::docs/quickstart.md[24] | execute `pane.enter()` and expect `Pane(%1 ...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_24_1200606fdf | 3.3a through 3.7c | planned parity | +| docs/quickstart.md::docs/quickstart.md[25] | execute `pane.send_keys('echo Howdy', enter=True, suppress_history=True)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_25_08ec8ee4a0 | 3.3a through 3.7c | planned parity | +| docs/quickstart.md::docs/quickstart.md[26] | execute `server.show_option('buffer-limit')` and expect `50`; execute `window.show_options() # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `{...}`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_26_7798619390 | 3.3a through 3.7c | planned parity | +| docs/quickstart.md::docs/quickstart.md[27] | execute `window.set_option('automatic-rename', False) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...)`; execute `window.show_option('automatic-rename')` and expect `False`; execute `window.unset_option('automatic-rename') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_27_c16e06c8e8 | 3.3a through 3.7c | planned parity | +| docs/quickstart.md::docs/quickstart.md[2] | execute `session.cmd('new-window', '-P').stdout[0]` and expect `'libtmux...:2.0'`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_2_868a54960d | 3.3a through 3.7c | planned parity | +| docs/quickstart.md::docs/quickstart.md[3] | execute `Window.from_window_id(window_id=session.cmd('new-window', '-P', '-F#{window_id}').stdout[0], server=session.server)` and expect `Window(@2 2:..., Session($1 libtmux_...))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_3_469dba23a7 | 3.3a through 3.7c | planned parity | +| docs/quickstart.md::docs/quickstart.md[4] | execute `window.cmd('split-window', '-P', '-F#{pane_id}').stdout[0]` and expect `'%2'`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_4_f2abec70e2 | 3.3a through 3.7c | planned parity | +| docs/quickstart.md::docs/quickstart.md[5] | execute `Pane.from_pane_id(pane_id=window.cmd('split-window', '-P', '-F#{pane_id}').stdout[0], server=window.server)` and expect `Pane(%... Window(@1 1:..., Session($1 libtmux_...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_5_7bc3e3ea98 | 3.3a through 3.7c | planned parity | +| docs/quickstart.md::docs/quickstart.md[6] | execute `server.sessions` and expect `[Session($1 ...), Session($0 ...)]`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_6_e1cefc99b0 | 3.3a through 3.7c | planned parity | +| docs/quickstart.md::docs/quickstart.md[7] | execute `server.sessions[0]` and expect `Session($1 ...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_7_994dd7180c | 3.3a through 3.7c | planned parity | +| docs/quickstart.md::docs/quickstart.md[8] | execute `server.sessions.filter(session_id='$1')[0]` and expect `Session($1 ...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_8_ed04d31709 | 3.3a through 3.7c | planned parity | +| docs/quickstart.md::docs/quickstart.md[9] | execute `server.sessions[0].rename_session('foo')` and expect `Session($1 foo)`; execute `server.sessions.filter(session_name="foo")[0]` and expect `Session($1 foo)`; execute `server.sessions.get(session_name="foo")` and expect `Session($1 foo)`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_quickstart_md_docs_quickstart_md_9_7aae9f19ec | 3.3a through 3.7c | planned parity | +| docs/topics/automation_patterns.md::docs/topics/automation_patterns.md[0] | execute `import time` without exception; execute `proc_window = session.new_window(window_name='process', attach=False)` without exception; execute `proc_pane = proc_window.active_pane` without exception; execute `proc_pane.send_keys('sleep 2 && echo "Process complete"')` without exception; execute `time.sleep(0.1)` without exception; execute `proc_window.window_name` and expect `'process'`; execute `proc_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_automation_patterns_md_docs_topics_automation_patterns_md_0_887870afdc | 3.3a through 3.7c | planned parity | +| docs/topics/automation_patterns.md::docs/topics/automation_patterns.md[10] | execute `import time` without exception; execute `retry_window = session.new_window(window_name='retry-demo', attach=False)` without exception; execute `retry_pane = retry_window.active_pane` without exception; execute `def retry_until_success(pane, command, success_marker, max_retries=3, delay=0.5): """Retry command until success marker appears.""" for attempt in range(max_retries): pane.send_keys(command) time.sleep(delay) output = '\\n'.join(pane.capture_pane()) if success_marker in output: return True, attempt + 1 return False, max_retries` without exception; execute `success, attempts = retry_until_success( retry_pane, 'echo "OK"', 'OK', max_retries=3, delay=0.2 )` without exception; execute `success` and expect `True`; execute `attempts` and expect `1`; execute `retry_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_automation_patterns_md_docs_topics_automation_patterns_md_10_4b90629469 | 3.3a through 3.7c | planned parity | +| docs/topics/automation_patterns.md::docs/topics/automation_patterns.md[11] | execute `import time` without exception; execute `queue_window = session.new_window(window_name='queue', attach=False)` without exception; execute `queue_pane = queue_window.active_pane` without exception; execute `def process_task_queue(pane, tasks, completion_marker='TASK_DONE'): """Process a queue of tasks sequentially.""" results = [] for i, task in enumerate(tasks): pane.send_keys(f'{task}; echo "{completion_marker}_{i}"') # Wait for this task to complete start = time.time() while time.time() - start < 5.0: output = '\\n'.join(pane.capture_pane()) if f'{completion_marker}_{i}' in output: results.append((i, True)) break time.sleep(0.1) else: results.append((i, False)) return results` without exception; execute `tasks = ['echo "Step 1"', 'echo "Step 2"', 'echo "Step 3"']` without exception; execute `results = process_task_queue(queue_pane, tasks)` without exception; execute `all(success for _, success in results)` and expect `True`; execute `queue_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_automation_patterns_md_docs_topics_automation_patterns_md_11_9b0ee172d9 | 3.3a through 3.7c | planned parity | +| docs/topics/automation_patterns.md::docs/topics/automation_patterns.md[12] | execute `import time` without exception; execute `state_window = session.new_window(window_name='state-machine', attach=False)` without exception; execute `state_pane = state_window.active_pane` without exception; execute `def run_state_machine(pane, states, timeout_per_state=2.0): """Run through a series of states with transitions.""" current_state = 0 history = [] while current_state < len(states): state_name, command, next_marker = states[current_state] pane.send_keys(command) start = time.time() while time.time() - start < timeout_per_state: output = '\\n'.join(pane.capture_pane()) if next_marker in output: history.append(state_name) current_state += 1 break time.sleep(0.1) else: return history, False # Timeout return history, True` without exception; execute `states = [ ('init', 'echo "INIT_DONE"', 'INIT_DONE'), ('process', 'echo "PROCESS_DONE"', 'PROCESS_DONE'), ('cleanup', 'echo "CLEANUP_DONE"', 'CLEANUP_DONE'), ]` without exception; execute `history, success = run_state_machine(state_pane, states)` without exception; execute `success` and expect `True`; execute `len(history)` and expect `3`; execute `state_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_automation_patterns_md_docs_topics_automation_patterns_md_12_0019f56ba3 | 3.3a through 3.7c | planned parity | +| docs/topics/automation_patterns.md::docs/topics/automation_patterns.md[13] | execute `bp_window = session.new_window(window_name='best-practice', attach=False)` without exception; execute `bp_pane = bp_window.active_pane` without exception; execute `bp_pane.send_keys('long_command; echo "__DONE__"')` without exception; execute `import time` without exception; execute `time.sleep(0.2)` without exception; execute `'__DONE__' in '\\n'.join(bp_pane.capture_pane())` and expect `True`; execute `bp_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_automation_patterns_md_docs_topics_automation_patterns_md_13_263a0a7422 | 3.3a through 3.7c | planned parity | +| docs/topics/automation_patterns.md::docs/topics/automation_patterns.md[14] | execute `cleanup_window = session.new_window(window_name='cleanup-demo', attach=False)` without exception; execute `cleanup_window # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...)`; execute `cleanup_window.kill()` without exception; execute `cleanup_window not in session.windows` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_automation_patterns_md_docs_topics_automation_patterns_md_14_51c82f6dd5 | 3.3a through 3.7c | planned parity | +| docs/topics/automation_patterns.md::docs/topics/automation_patterns.md[15] | execute `with session.new_window(window_name='safe-work') as safe_window: pane = safe_window.active_pane # Work happens here pass # Even if exception occurs, window is cleaned up` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_automation_patterns_md_docs_topics_automation_patterns_md_15_d080f809da | 3.3a through 3.7c | planned parity | +| docs/topics/automation_patterns.md::docs/topics/automation_patterns.md[1] | execute `import time` without exception; execute `status_window = session.new_window(window_name='status-check', attach=False)` without exception; execute `status_pane = status_window.active_pane` without exception; execute `def is_process_running(pane, marker='RUNNING'): """Check if a marker indicates process is still running.""" output = pane.capture_pane() return marker in '\\n'.join(output)` without exception; execute `status_pane.send_keys('echo "RUNNING"; sleep 0.3; echo "DONE"')` without exception; execute `time.sleep(0.1)` without exception; execute `'RUNNING' in '\\n'.join(status_pane.capture_pane())` and expect `True`; execute `time.sleep(0.5)` without exception; execute `'DONE' in '\\n'.join(status_pane.capture_pane())` and expect `True`; execute `status_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_automation_patterns_md_docs_topics_automation_patterns_md_1_091b7723b5 | 3.3a through 3.7c | planned parity | +| docs/topics/automation_patterns.md::docs/topics/automation_patterns.md[2] | execute `import time` without exception; execute `monitor_window = session.new_window(window_name='monitor', attach=False)` without exception; execute `monitor_pane = monitor_window.active_pane` without exception; execute `def wait_for_output(pane, text, timeout=5.0, poll_interval=0.1): """Wait for specific text to appear in pane output.""" start = time.time() while time.time() - start < timeout: output = '\\n'.join(pane.capture_pane()) if text in output: return True time.sleep(poll_interval) return False` without exception; execute `monitor_pane.send_keys('sleep 0.2; echo "READY"')` without exception; execute `wait_for_output(monitor_pane, 'READY', timeout=2.0)` and expect `True`; execute `monitor_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_automation_patterns_md_docs_topics_automation_patterns_md_2_efd039245a | 3.3a through 3.7c | planned parity | +| docs/topics/automation_patterns.md::docs/topics/automation_patterns.md[3] | execute `import time` without exception; execute `error_window = session.new_window(window_name='error-check', attach=False)` without exception; execute `error_pane = error_window.active_pane` without exception; execute `def check_for_errors(pane, patterns=None): """Check pane output for error patterns.""" if patterns is None: patterns = ['Error:', 'error:', 'ERROR', 'FAILED', 'Exception'] output = '\\n'.join(pane.capture_pane()) for pattern in patterns: if pattern in output: return pattern return None` without exception; execute `error_pane.send_keys('echo "Success!"')` without exception; execute `time.sleep(0.1)` without exception; execute `check_for_errors(error_pane) is None` and expect `True`; execute `error_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_automation_patterns_md_docs_topics_automation_patterns_md_3_962186aff6 | 3.3a through 3.7c | planned parity | +| docs/topics/automation_patterns.md::docs/topics/automation_patterns.md[4] | execute `import time` without exception; execute `capture_window = session.new_window(window_name='capture', attach=False)` without exception; execute `capture_pane = capture_window.active_pane` without exception; execute `def capture_after_marker(pane, marker, timeout=5.0): """Capture output after a marker appears.""" start_time = time.time() while time.time() - start_time < timeout: lines = pane.capture_pane() output = '\\n'.join(lines) if marker in output: # Return all lines after the marker found = False result = [] for line in lines: if marker in line: found = True continue if found: result.append(line) return result time.sleep(0.1) return None` without exception; execute `capture_pane.send_keys('echo "MARKER"; echo "captured data"')` without exception; execute `time.sleep(0.3)` without exception; execute `result = capture_after_marker(capture_pane, 'MARKER', timeout=2.0)` without exception; execute `any('captured' in line for line in (result or []))` and expect `True`; execute `capture_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_automation_patterns_md_docs_topics_automation_patterns_md_4_5d507b840e | 3.3a through 3.7c | planned parity | +| docs/topics/automation_patterns.md::docs/topics/automation_patterns.md[5] | execute `import time` without exception; execute `from libtmux.constants import PaneDirection` without exception; execute `parallel_window = session.new_window(window_name='parallel', attach=False)` without exception; execute `parallel_window.resize(height=40, width=120) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...)`; execute `pane1 = parallel_window.active_pane` without exception; execute `pane2 = pane1.split(direction=PaneDirection.Right)` without exception; execute `pane3 = pane1.split(direction=PaneDirection.Below)` without exception; execute `tasks = [ (pane1, 'echo "Task 1"; sleep 0.2; echo "DONE1"'), (pane2, 'echo "Task 2"; sleep 0.1; echo "DONE2"'), (pane3, 'echo "Task 3"; sleep 0.3; echo "DONE3"'), ]` without exception; execute `for pane, cmd in tasks: pane.send_keys(cmd)` without exception; execute `time.sleep(0.5)` without exception; execute `all('DONE' in '\\n'.join(p.capture_pane()) for p, _ in tasks)` and expect `True`; execute `parallel_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_automation_patterns_md_docs_topics_automation_patterns_md_5_26c1bc29c6 | 3.3a through 3.7c | planned parity | +| docs/topics/automation_patterns.md::docs/topics/automation_patterns.md[6] | execute `import time` without exception; execute `from libtmux.constants import PaneDirection` without exception; execute `multi_window = session.new_window(window_name='multi-monitor', attach=False)` without exception; execute `multi_window.resize(height=40, width=120) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...)`; execute `panes = [multi_window.active_pane]` without exception; execute `panes.append(panes[0].split(direction=PaneDirection.Right))` without exception; execute `panes.append(panes[0].split(direction=PaneDirection.Below))` without exception; execute `def wait_all_complete(panes, marker='COMPLETE', timeout=10.0): """Wait for all panes to show completion marker.""" start = time.time() remaining = set(range(len(panes))) while remaining and time.time() - start < timeout: for i in list(remaining): if marker in '\\n'.join(panes[i].capture_pane()): remaining.remove(i) time.sleep(0.1) return len(remaining) == 0` without exception; execute `for i, pane in enumerate(panes): pane.send_keys(f'sleep 0.{i+1}; echo "COMPLETE"')` without exception; execute `wait_all_complete(panes, 'COMPLETE', timeout=2.0)` and expect `True`; execute `multi_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_automation_patterns_md_docs_topics_automation_patterns_md_6_431ba17d12 | 3.3a through 3.7c | planned parity | +| docs/topics/automation_patterns.md::docs/topics/automation_patterns.md[7] | execute `with server.new_session(session_name='temp-work') as temp_session: window = temp_session.new_window(window_name='task') pane = window.active_pane pane.send_keys('echo "Isolated work"') # Session exists during work temp_session in server.sessions` and expect `True`; execute `temp_session not in server.sessions` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_automation_patterns_md_docs_topics_automation_patterns_md_7_b244ac24ec | 3.3a through 3.7c | planned parity | +| docs/topics/automation_patterns.md::docs/topics/automation_patterns.md[8] | execute `import time` without exception; execute `with session.new_window(window_name='subtask') as sub_window: pane = sub_window.active_pane pane.send_keys('echo "Subtask running"') time.sleep(0.1) 'Subtask' in '\\n'.join(pane.capture_pane())` and expect `True`; execute `sub_window not in session.windows` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_automation_patterns_md_docs_topics_automation_patterns_md_8_f7764b8715 | 3.3a through 3.7c | planned parity | +| docs/topics/automation_patterns.md::docs/topics/automation_patterns.md[9] | execute `import time` without exception; execute `timeout_window = session.new_window(window_name='timeout-demo', attach=False)` without exception; execute `timeout_pane = timeout_window.active_pane` without exception; execute `class CommandTimeout(Exception): """Raised when a command times out.""" pass` without exception; execute `def run_with_timeout(pane, command, marker='__DONE__', timeout=5.0): """Run command and wait for completion with timeout.""" pane.send_keys(f'{command}; echo {marker}') start = time.time() while time.time() - start < timeout: output = '\\n'.join(pane.capture_pane()) if marker in output: return output time.sleep(0.1) raise CommandTimeout(f'Command timed out after {timeout}s')` without exception; execute `result = run_with_timeout(timeout_pane, 'echo "fast"', timeout=2.0)` without exception; execute `'fast' in result` and expect `True`; execute `timeout_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_automation_patterns_md_docs_topics_automation_patterns_md_9_d60e07d979 | 3.3a through 3.7c | planned parity | +| docs/topics/clients.md::docs/topics/clients.md[0] | execute `with control_mode() as ctl: client = server.clients.get(client_name=ctl.client_name) attached = client.attached_session` without exception; execute `attached is not None` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_clients_md_docs_topics_clients_md_0_87f0d41a5d | 3.3a through 3.7c | planned parity | +| docs/topics/clients.md::docs/topics/clients.md[1] | execute `with control_mode() as ctl: client = server.clients.get(client_name=ctl.client_name) pane = client.attached_pane` without exception; execute `pane is None or pane.pane_id.startswith('%')` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_clients_md_docs_topics_clients_md_1_0ff98b00d9 | 3.3a through 3.7c | planned parity | +| docs/topics/clients.md::docs/topics/clients.md[2] | execute `with control_mode() as ctl: attached = [ c for c in server.clients if c.client_name == ctl.client_name ]` without exception; execute `bool(attached)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_clients_md_docs_topics_clients_md_2_99d2e27e61 | 3.3a through 3.7c | planned parity | | docs/topics/context_managers.md::docs/topics/context_managers.md[0] | execute `import libtmux` without exception. | documentation contract test | PythonBehaviorParityTest#docs_topics_context_managers_md_docs_topics_context_managers_md_0_ed8462c4a7 | not tmux-bound | planned parity | -| docs/topics/context_managers.md::docs/topics/context_managers.md[1] | execute `with Server() as server: session = server.new_session() print(server.is_alive())` and expect `True`; execute `print(server.is_alive()) # Server is killed after exiting context` and expect `False`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_context_managers_md_docs_topics_context_managers_md_1_a71200aff7 | 3.2a through 3.7b | planned parity | -| docs/topics/context_managers.md::docs/topics/context_managers.md[2] | execute `server = Server()` without exception; execute `with server.new_session() as session: print(session in server.sessions) window = session.new_window()` and expect `True`; execute `print(session in server.sessions) # Session is killed after exiting context` and expect `False`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_context_managers_md_docs_topics_context_managers_md_2_020ff19587 | 3.2a through 3.7b | planned parity | -| docs/topics/context_managers.md::docs/topics/context_managers.md[3] | execute `server = Server()` without exception; execute `session = server.new_session()` without exception; execute `with session.new_window() as window: print(window in session.windows) pane = window.split()` and expect `True`; execute `print(window in session.windows) # Window is killed after exiting context` and expect `False`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_context_managers_md_docs_topics_context_managers_md_3_3859334d99 | 3.2a through 3.7b | planned parity | -| docs/topics/context_managers.md::docs/topics/context_managers.md[4] | execute `server = Server()` without exception; execute `session = server.new_session()` without exception; execute `window = session.new_window()` without exception; execute `with window.split() as pane: print(pane in window.panes) pane.send_keys('echo "Hello"')` and expect `True`; execute `print(pane in window.panes) # Pane is killed after exiting context` and expect `False`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_context_managers_md_docs_topics_context_managers_md_4_f08e853cd5 | 3.2a through 3.7b | planned parity | -| docs/topics/context_managers.md::docs/topics/context_managers.md[5] | execute `with Server() as server: with server.new_session() as session: with session.new_window() as window: with window.split() as pane: pane.send_keys('echo "Hello"') # Do work with the pane # Everything is cleaned up automatically when exiting contexts` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_context_managers_md_docs_topics_context_managers_md_5_ea6e5b2860 | 3.2a through 3.7b | planned parity | -| docs/topics/filtering.md::docs/topics/filtering.md[0] | execute `server.sessions # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Session($... ...)]`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_0_190d6fc1c9 | 3.2a through 3.7b | planned parity | -| docs/topics/filtering.md::docs/topics/filtering.md[10] | execute `w1 = session.new_window(window_name="feature-login")` without exception; execute `w2 = session.new_window(window_name="feature-signup")` without exception; execute `w3 = session.new_window(window_name="bugfix-typo")` without exception; execute `session.windows.filter( window_name__startswith='feature', window_name__endswith='signup' ) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Window(@... ...:feature-signup, Session($... ...))]`; execute `session.windows.filter( window_name__contains='feature' ).filter( window_name__contains='login' ) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Window(@... ...:feature-login, Session($... ...))]`; execute `w1.kill()` without exception; execute `w2.kill()` without exception; execute `w3.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_10_bc8123f45b | 3.2a through 3.7b | planned parity | -| docs/topics/filtering.md::docs/topics/filtering.md[11] | execute `w1 = session.new_window(window_name="MyApp-Server")` without exception; execute `w2 = session.new_window(window_name="myapp-worker")` without exception; execute `myapp_windows = session.windows.filter(window_name__icontains='MYAPP')` without exception; execute `len(myapp_windows) >= 2` and expect `True`; execute `session.windows.filter(window_name__istartswith='myapp') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Window(@... ...:MyApp-Server, Session($... ...)), Window(@... ...:myapp-worker, Session($... ...))]`; execute `w1.kill()` without exception; execute `w2.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_11_5e38730037 | 3.2a through 3.7b | planned parity | -| docs/topics/filtering.md::docs/topics/filtering.md[12] | execute `w1 = session.new_window(window_name="app-v1-0")` without exception; execute `w2 = session.new_window(window_name="app-v2-0")` without exception; execute `w3 = session.new_window(window_name="app-beta")` without exception; execute `versioned = session.windows.filter(window_name__regex=r'v\d+-\d+$')` without exception; execute `len(versioned) >= 2` and expect `True`; execute `session.windows.filter(window_name__iregex=r'BETA') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Window(@... ...:app-beta, Session($... ...))]`; execute `w1.kill()` without exception; execute `w2.kill()` without exception; execute `w3.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_12_111e348049 | 3.2a through 3.7b | planned parity | -| docs/topics/filtering.md::docs/topics/filtering.md[13] | execute `w1 = session.new_window(window_name="dev")` without exception; execute `w2 = session.new_window(window_name="staging")` without exception; execute `w3 = session.new_window(window_name="prod")` without exception; execute `target_envs = ["dev", "prod"]` without exception; execute `session.windows.filter(window_name__in=target_envs) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Window(@... ...:dev, Session($... ...)), Window(@... ...:prod, Session($... ...))]`; execute `non_prod = session.windows.filter(window_name__nin=["prod"])` without exception; execute `any(w.window_name == "prod" for w in non_prod)` and expect `False`; execute `w1.kill()` without exception; execute `w2.kill()` without exception; execute `w3.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_13_54dbf98c56 | 3.2a through 3.7b | planned parity | -| docs/topics/filtering.md::docs/topics/filtering.md[14] | execute `server.panes # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Pane(%... Window(@... ..., Session($... ...)))]`; execute `pane = session.active_pane` without exception; execute `pane # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Pane(%... Window(@... ..., Session($... ...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_14_594195a62b | 3.2a through 3.7b | planned parity | -| docs/topics/filtering.md::docs/topics/filtering.md[15] | execute `w1 = session.new_window(window_name="vim-main")` without exception; execute `w2 = session.new_window(window_name="nvim-config")` without exception; execute `w3 = session.new_window(window_name="shell")` without exception; execute `editors = session.windows.filter(window_name__iregex=r'n?vim')` without exception; execute `len(editors) >= 2` and expect `True`; execute `w1.kill()` without exception; execute `w2.kill()` without exception; execute `w3.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_15_4558f80de6 | 3.2a through 3.7b | planned parity | -| docs/topics/filtering.md::docs/topics/filtering.md[16] | execute `w1 = session.new_window(window_name="project-frontend")` without exception; execute `w2 = session.new_window(window_name="project-backend")` without exception; execute `w3 = session.new_window(window_name="logs")` without exception; execute `project_windows = session.windows.filter(window_name__startswith='project-')` without exception; execute `len(project_windows) >= 2` and expect `True`; execute `backend = session.windows.get(window_name='project-backend')` without exception; execute `backend.window_name` and expect `'project-backend'`; execute `w1.kill()` without exception; execute `w2.kill()` without exception; execute `w3.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_16_6b541e4fe7 | 3.2a through 3.7b | planned parity | -| docs/topics/filtering.md::docs/topics/filtering.md[17] | execute `s_alpha = server.new_session(session_name='alpha-1')` without exception; execute `s_beta = server.new_session(session_name='beta-1')` without exception; execute `alphas = server.search_sessions(filter='#{m:alpha-*,#{session_name}}')` without exception; execute `[s.session_name for s in alphas]` and expect `['alpha-1']`; execute `exact = server.search_sessions( filter='#{==:#{session_name},alpha-1}' )` without exception; execute `[s.session_name for s in exact]` and expect `['alpha-1']`; execute `s_alpha.kill()` without exception; execute `s_beta.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_17_45f423bb54 | 3.2a through 3.7b | planned parity | -| docs/topics/filtering.md::docs/topics/filtering.md[18] | execute `result = server.display_message( '#{m:alpha-*,alpha-1}', get_text=True )` without exception; execute `result[0]` and expect `'1'`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_18_2084fce1f1 | 3.2a through 3.7b | planned parity | -| docs/topics/filtering.md::docs/topics/filtering.md[1] | execute `server.sessions.filter(session_name=session.session_name) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Session($... ...)]`; execute `server.sessions.filter(session_name__exact=session.session_name) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Session($... ...)]`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_1_f85fcf994b | 3.2a through 3.7b | planned parity | -| docs/topics/filtering.md::docs/topics/filtering.md[2] | execute `w1 = session.new_window(window_name="api-server")` without exception; execute `w2 = session.new_window(window_name="api-worker")` without exception; execute `w3 = session.new_window(window_name="web-frontend")` without exception; execute `api_windows = session.windows.filter(window_name__contains='api')` without exception; execute `len(api_windows) >= 2` and expect `True`; execute `web_windows = session.windows.filter(window_name__startswith='web')` without exception; execute `len(web_windows) >= 1` and expect `True`; execute `w1.kill()` without exception; execute `w2.kill()` without exception; execute `w3.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_2_bb23f841ff | 3.2a through 3.7b | planned parity | -| docs/topics/filtering.md::docs/topics/filtering.md[3] | execute `window = session.windows.get(window_id=session.active_window.window_id)` without exception; execute `window # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ..., Session($... ...))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_3_954d4fbe3a | 3.2a through 3.7b | planned parity | -| docs/topics/filtering.md::docs/topics/filtering.md[4] | execute `from libtmux import exc` without exception; execute `try: session.windows.get(window_name="nonexistent") except exc.LibTmuxException as e: print(e)` and expect `No objects found: window_name='nonexistent'`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_4_b7d4b8d7a5 | 3.2a through 3.7b | planned parity | -| docs/topics/filtering.md::docs/topics/filtering.md[5] | execute `session.windows.get(window_name="nonexistent", default=None) is None` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_5_1975da3146 | 3.2a through 3.7b | planned parity | -| docs/topics/filtering.md::docs/topics/filtering.md[6] | execute `home = server.new_session(session_name="home")` without exception; execute `shared = home.new_window(window_name="shared", attach=False)` without exception; execute `guest = server.new_session(session_name="guest")` without exception; execute `_ = server.cmd( "link-window", "-d", "-s", shared.window_id, "-t", f"{guest.session_id}:" )` without exception; execute `len(server.windows.filter(window_id=shared.window_id))` and expect `2`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_6_f8ff4a6d97 | 3.2a through 3.7b | planned parity | -| docs/topics/filtering.md::docs/topics/filtering.md[7] | execute `from libtmux import exc` without exception; execute `home = server.new_session(session_name="home")` without exception; execute `shared = home.new_window(window_name="shared", attach=False)` without exception; execute `guest = server.new_session(session_name="guest")` without exception; execute `_ = server.cmd( "link-window", "-d", "-s", shared.window_id, "-t", f"{guest.session_id}:" )` without exception; execute `try: server.windows.get(window_id=shared.window_id) except exc.MultipleObjectsReturned as e: print(e) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Multiple objects returned (2): window_id='@...'`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_7_c12a9f514f | 3.2a through 3.7b | planned parity | -| docs/topics/filtering.md::docs/topics/filtering.md[8] | execute `home = server.new_session(session_name="home")` without exception; execute `shared = home.new_window(window_name="shared", attach=False)` without exception; execute `guest = server.new_session(session_name="guest")` without exception; execute `_ = server.cmd( "link-window", "-d", "-s", shared.window_id, "-t", f"{guest.session_id}:" )` without exception; execute `sorted(s.session_name for s in shared.linked_sessions)` and expect `['guest', 'home']`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_8_1224a7171f | 3.2a through 3.7b | planned parity | -| docs/topics/filtering.md::docs/topics/filtering.md[9] | execute `from libtmux.window import Window` without exception; execute `home = server.new_session(session_name="home")` without exception; execute `shared = home.new_window(window_name="shared", attach=False)` without exception; execute `guest = server.new_session(session_name="guest")` without exception; execute `_ = server.cmd( "link-window", "-d", "-s", shared.window_id, "-t", f"{guest.session_id}:" )` without exception; execute `Window.from_window_id(server, shared.window_id).window_id == shared.window_id` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_9_911ae33a2e | 3.2a through 3.7b | planned parity | -| docs/topics/floating_panes.md::docs/topics/floating_panes.md[0] | execute `from libtmux.common import has_gte_version` without exception; execute `if has_gte_version("3.7"): floating = window.new_pane(width=20, height=5, shell="sleep 30") is_floating = floating.pane_floating_flag else: is_floating = "1"` without exception; execute `is_floating` and expect `'1'`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_floating_panes_md_docs_topics_floating_panes_md_0_1fde3715a8 | 3.2a through 3.7b | planned parity | -| docs/topics/floating_panes.md::docs/topics/floating_panes.md[1] | execute `from libtmux.common import has_gte_version` without exception; execute `if has_gte_version("3.7"): placed = window.new_pane(width=20, height=5, x=2, y=1, shell="sleep 30") position = (placed.pane_x, placed.pane_y) else: position = ("2", "1")` without exception; execute `position` and expect `('2', '1')`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_floating_panes_md_docs_topics_floating_panes_md_1_e785fa5c58 | 3.2a through 3.7b | planned parity | -| docs/topics/floating_panes.md::docs/topics/floating_panes.md[2] | execute `from libtmux.common import has_gte_version` without exception; execute `if has_gte_version("3.7"): held = window.new_pane(width=20, height=5, shell="sleep 30", keep=True) remain = held.cmd("show-options", "-p", "-v", "remain-on-exit").stdout else: remain = ["key"]` without exception; execute `remain` and expect `['key']`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_floating_panes_md_docs_topics_floating_panes_md_2_6c875da77f | 3.2a through 3.7b | planned parity | -| docs/topics/floating_panes.md::docs/topics/floating_panes.md[3] | execute `from libtmux.common import has_gte_version` without exception; execute `if has_gte_version("3.7"): _ = window.new_pane(width=20, height=5, shell="sleep 30") floating = [p for p in window.panes if p.pane_floating_flag == "1"] found = len(floating) >= 1 else: found = True` without exception; execute `found` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_floating_panes_md_docs_topics_floating_panes_md_3_30f804e434 | 3.2a through 3.7b | planned parity | -| docs/topics/format-tokens.md::docs/topics/format-tokens.md[0] | execute `session = server.new_session()` without exception; execute `session.pane_id == session.active_window.active_pane.pane_id` and expect `True`; execute `session.window_id == session.active_window.window_id` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_format_tokens_md_docs_topics_format_tokens_md_0_fcbd5bfda4 | 3.2a through 3.7b | planned parity | -| docs/topics/format-tokens.md::docs/topics/format-tokens.md[1] | execute `from libtmux.neo import get_output_format` without exception; execute `fields, _ = get_output_format("list-sessions", "3.6a")` without exception; execute `'session_id' in fields` and expect `True`; execute `'pane_id' in fields # active pane for the listed session` and expect `True`; execute `'client_name' in fields # client fields require list-clients` and expect `False`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_format_tokens_md_docs_topics_format_tokens_md_1_c73624c189 | 3.2a through 3.7b | planned parity | +| docs/topics/context_managers.md::docs/topics/context_managers.md[1] | execute `with Server() as server: session = server.new_session() print(server.is_alive())` and expect `True`; execute `print(server.is_alive()) # Server is killed after exiting context` and expect `False`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_context_managers_md_docs_topics_context_managers_md_1_a71200aff7 | 3.3a through 3.7c | planned parity | +| docs/topics/context_managers.md::docs/topics/context_managers.md[2] | execute `server = Server()` without exception; execute `with server.new_session() as session: print(session in server.sessions) window = session.new_window()` and expect `True`; execute `print(session in server.sessions) # Session is killed after exiting context` and expect `False`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_context_managers_md_docs_topics_context_managers_md_2_020ff19587 | 3.3a through 3.7c | planned parity | +| docs/topics/context_managers.md::docs/topics/context_managers.md[3] | execute `server = Server()` without exception; execute `session = server.new_session()` without exception; execute `with session.new_window() as window: print(window in session.windows) pane = window.split()` and expect `True`; execute `print(window in session.windows) # Window is killed after exiting context` and expect `False`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_context_managers_md_docs_topics_context_managers_md_3_3859334d99 | 3.3a through 3.7c | planned parity | +| docs/topics/context_managers.md::docs/topics/context_managers.md[4] | execute `server = Server()` without exception; execute `session = server.new_session()` without exception; execute `window = session.new_window()` without exception; execute `with window.split() as pane: print(pane in window.panes) pane.send_keys('echo "Hello"')` and expect `True`; execute `print(pane in window.panes) # Pane is killed after exiting context` and expect `False`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_context_managers_md_docs_topics_context_managers_md_4_f08e853cd5 | 3.3a through 3.7c | planned parity | +| docs/topics/context_managers.md::docs/topics/context_managers.md[5] | execute `with Server() as server: with server.new_session() as session: with session.new_window() as window: with window.split() as pane: pane.send_keys('echo "Hello"') # Do work with the pane # Everything is cleaned up automatically when exiting contexts` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_context_managers_md_docs_topics_context_managers_md_5_ea6e5b2860 | 3.3a through 3.7c | planned parity | +| docs/topics/filtering.md::docs/topics/filtering.md[0] | execute `server.sessions # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Session($... ...)]`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_0_190d6fc1c9 | 3.3a through 3.7c | planned parity | +| docs/topics/filtering.md::docs/topics/filtering.md[10] | execute `w1 = session.new_window(window_name="feature-login")` without exception; execute `w2 = session.new_window(window_name="feature-signup")` without exception; execute `w3 = session.new_window(window_name="bugfix-typo")` without exception; execute `session.windows.filter( window_name__startswith='feature', window_name__endswith='signup' ) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Window(@... ...:feature-signup, Session($... ...))]`; execute `session.windows.filter( window_name__contains='feature' ).filter( window_name__contains='login' ) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Window(@... ...:feature-login, Session($... ...))]`; execute `w1.kill()` without exception; execute `w2.kill()` without exception; execute `w3.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_10_bc8123f45b | 3.3a through 3.7c | planned parity | +| docs/topics/filtering.md::docs/topics/filtering.md[11] | execute `w1 = session.new_window(window_name="MyApp-Server")` without exception; execute `w2 = session.new_window(window_name="myapp-worker")` without exception; execute `myapp_windows = session.windows.filter(window_name__icontains='MYAPP')` without exception; execute `len(myapp_windows) >= 2` and expect `True`; execute `session.windows.filter(window_name__istartswith='myapp') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Window(@... ...:MyApp-Server, Session($... ...)), Window(@... ...:myapp-worker, Session($... ...))]`; execute `w1.kill()` without exception; execute `w2.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_11_5e38730037 | 3.3a through 3.7c | planned parity | +| docs/topics/filtering.md::docs/topics/filtering.md[12] | execute `w1 = session.new_window(window_name="app-v1-0")` without exception; execute `w2 = session.new_window(window_name="app-v2-0")` without exception; execute `w3 = session.new_window(window_name="app-beta")` without exception; execute `versioned = session.windows.filter(window_name__regex=r'v\d+-\d+$')` without exception; execute `len(versioned) >= 2` and expect `True`; execute `session.windows.filter(window_name__iregex=r'BETA') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Window(@... ...:app-beta, Session($... ...))]`; execute `w1.kill()` without exception; execute `w2.kill()` without exception; execute `w3.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_12_111e348049 | 3.3a through 3.7c | planned parity | +| docs/topics/filtering.md::docs/topics/filtering.md[13] | execute `w1 = session.new_window(window_name="dev")` without exception; execute `w2 = session.new_window(window_name="staging")` without exception; execute `w3 = session.new_window(window_name="prod")` without exception; execute `target_envs = ["dev", "prod"]` without exception; execute `session.windows.filter(window_name__in=target_envs) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Window(@... ...:dev, Session($... ...)), Window(@... ...:prod, Session($... ...))]`; execute `non_prod = session.windows.filter(window_name__nin=["prod"])` without exception; execute `any(w.window_name == "prod" for w in non_prod)` and expect `False`; execute `w1.kill()` without exception; execute `w2.kill()` without exception; execute `w3.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_13_54dbf98c56 | 3.3a through 3.7c | planned parity | +| docs/topics/filtering.md::docs/topics/filtering.md[14] | execute `server.panes # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Pane(%... Window(@... ..., Session($... ...)))]`; execute `pane = session.active_pane` without exception; execute `pane # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Pane(%... Window(@... ..., Session($... ...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_14_594195a62b | 3.3a through 3.7c | planned parity | +| docs/topics/filtering.md::docs/topics/filtering.md[15] | execute `w1 = session.new_window(window_name="vim-main")` without exception; execute `w2 = session.new_window(window_name="nvim-config")` without exception; execute `w3 = session.new_window(window_name="shell")` without exception; execute `editors = session.windows.filter(window_name__iregex=r'n?vim')` without exception; execute `len(editors) >= 2` and expect `True`; execute `w1.kill()` without exception; execute `w2.kill()` without exception; execute `w3.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_15_4558f80de6 | 3.3a through 3.7c | planned parity | +| docs/topics/filtering.md::docs/topics/filtering.md[16] | execute `w1 = session.new_window(window_name="project-frontend")` without exception; execute `w2 = session.new_window(window_name="project-backend")` without exception; execute `w3 = session.new_window(window_name="logs")` without exception; execute `project_windows = session.windows.filter(window_name__startswith='project-')` without exception; execute `len(project_windows) >= 2` and expect `True`; execute `backend = session.windows.get(window_name='project-backend')` without exception; execute `backend.window_name` and expect `'project-backend'`; execute `w1.kill()` without exception; execute `w2.kill()` without exception; execute `w3.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_16_6b541e4fe7 | 3.3a through 3.7c | planned parity | +| docs/topics/filtering.md::docs/topics/filtering.md[17] | execute `s_alpha = server.new_session(session_name='alpha-1')` without exception; execute `s_beta = server.new_session(session_name='beta-1')` without exception; execute `alphas = server.search_sessions(filter='#{m:alpha-*,#{session_name}}')` without exception; execute `[s.session_name for s in alphas]` and expect `['alpha-1']`; execute `exact = server.search_sessions( filter='#{==:#{session_name},alpha-1}' )` without exception; execute `[s.session_name for s in exact]` and expect `['alpha-1']`; execute `s_alpha.kill()` without exception; execute `s_beta.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_17_45f423bb54 | 3.3a through 3.7c | planned parity | +| docs/topics/filtering.md::docs/topics/filtering.md[18] | execute `result = server.display_message( '#{m:alpha-*,alpha-1}', get_text=True )` without exception; execute `result[0]` and expect `'1'`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_18_2084fce1f1 | 3.3a through 3.7c | planned parity | +| docs/topics/filtering.md::docs/topics/filtering.md[1] | execute `server.sessions.filter(session_name=session.session_name) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Session($... ...)]`; execute `server.sessions.filter(session_name__exact=session.session_name) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Session($... ...)]`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_1_f85fcf994b | 3.3a through 3.7c | planned parity | +| docs/topics/filtering.md::docs/topics/filtering.md[2] | execute `w1 = session.new_window(window_name="api-server")` without exception; execute `w2 = session.new_window(window_name="api-worker")` without exception; execute `w3 = session.new_window(window_name="web-frontend")` without exception; execute `api_windows = session.windows.filter(window_name__contains='api')` without exception; execute `len(api_windows) >= 2` and expect `True`; execute `web_windows = session.windows.filter(window_name__startswith='web')` without exception; execute `len(web_windows) >= 1` and expect `True`; execute `w1.kill()` without exception; execute `w2.kill()` without exception; execute `w3.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_2_bb23f841ff | 3.3a through 3.7c | planned parity | +| docs/topics/filtering.md::docs/topics/filtering.md[3] | execute `window = session.windows.get(window_id=session.active_window.window_id)` without exception; execute `window # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ..., Session($... ...))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_3_954d4fbe3a | 3.3a through 3.7c | planned parity | +| docs/topics/filtering.md::docs/topics/filtering.md[4] | execute `from libtmux import exc` without exception; execute `try: session.windows.get(window_name="nonexistent") except exc.LibTmuxException as e: print(e)` and expect `No objects found: window_name='nonexistent'`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_4_b7d4b8d7a5 | 3.3a through 3.7c | planned parity | +| docs/topics/filtering.md::docs/topics/filtering.md[5] | execute `session.windows.get(window_name="nonexistent", default=None) is None` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_5_1975da3146 | 3.3a through 3.7c | planned parity | +| docs/topics/filtering.md::docs/topics/filtering.md[6] | execute `home = server.new_session(session_name="home")` without exception; execute `shared = home.new_window(window_name="shared", attach=False)` without exception; execute `guest = server.new_session(session_name="guest")` without exception; execute `_ = server.cmd( "link-window", "-d", "-s", shared.window_id, "-t", f"{guest.session_id}:" )` without exception; execute `len(server.windows.filter(window_id=shared.window_id))` and expect `2`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_6_f8ff4a6d97 | 3.3a through 3.7c | planned parity | +| docs/topics/filtering.md::docs/topics/filtering.md[7] | execute `from libtmux import exc` without exception; execute `home = server.new_session(session_name="home")` without exception; execute `shared = home.new_window(window_name="shared", attach=False)` without exception; execute `guest = server.new_session(session_name="guest")` without exception; execute `_ = server.cmd( "link-window", "-d", "-s", shared.window_id, "-t", f"{guest.session_id}:" )` without exception; execute `try: server.windows.get(window_id=shared.window_id) except exc.MultipleObjectsReturned as e: print(e) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Multiple objects returned (2): window_id='@...'`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_7_c12a9f514f | 3.3a through 3.7c | planned parity | +| docs/topics/filtering.md::docs/topics/filtering.md[8] | execute `home = server.new_session(session_name="home")` without exception; execute `shared = home.new_window(window_name="shared", attach=False)` without exception; execute `guest = server.new_session(session_name="guest")` without exception; execute `_ = server.cmd( "link-window", "-d", "-s", shared.window_id, "-t", f"{guest.session_id}:" )` without exception; execute `sorted(s.session_name for s in shared.linked_sessions)` and expect `['guest', 'home']`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_8_1224a7171f | 3.3a through 3.7c | planned parity | +| docs/topics/filtering.md::docs/topics/filtering.md[9] | execute `from libtmux.window import Window` without exception; execute `home = server.new_session(session_name="home")` without exception; execute `shared = home.new_window(window_name="shared", attach=False)` without exception; execute `guest = server.new_session(session_name="guest")` without exception; execute `_ = server.cmd( "link-window", "-d", "-s", shared.window_id, "-t", f"{guest.session_id}:" )` without exception; execute `Window.from_window_id(server, shared.window_id).window_id == shared.window_id` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_filtering_md_docs_topics_filtering_md_9_911ae33a2e | 3.3a through 3.7c | planned parity | +| docs/topics/floating_panes.md::docs/topics/floating_panes.md[0] | execute `from libtmux.common import has_gte_version` without exception; execute `if has_gte_version("3.7"): floating = window.new_pane(width=20, height=5, shell="sleep 30") is_floating = floating.pane_floating_flag else: is_floating = "1"` without exception; execute `is_floating` and expect `'1'`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_floating_panes_md_docs_topics_floating_panes_md_0_1fde3715a8 | 3.3a through 3.7c | planned parity | +| docs/topics/floating_panes.md::docs/topics/floating_panes.md[1] | execute `from libtmux.common import has_gte_version` without exception; execute `if has_gte_version("3.7"): placed = window.new_pane(width=20, height=5, x=2, y=1, shell="sleep 30") position = (placed.pane_x, placed.pane_y) else: position = ("2", "1")` without exception; execute `position` and expect `('2', '1')`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_floating_panes_md_docs_topics_floating_panes_md_1_e785fa5c58 | 3.3a through 3.7c | planned parity | +| docs/topics/floating_panes.md::docs/topics/floating_panes.md[2] | execute `from libtmux.common import has_gte_version` without exception; execute `if has_gte_version("3.7"): held = window.new_pane(width=20, height=5, shell="sleep 30", keep=True) remain = held.cmd("show-options", "-p", "-v", "remain-on-exit").stdout else: remain = ["key"]` without exception; execute `remain` and expect `['key']`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_floating_panes_md_docs_topics_floating_panes_md_2_6c875da77f | 3.3a through 3.7c | planned parity | +| docs/topics/floating_panes.md::docs/topics/floating_panes.md[3] | execute `from libtmux.common import has_gte_version` without exception; execute `if has_gte_version("3.7"): _ = window.new_pane(width=20, height=5, shell="sleep 30") floating = [p for p in window.panes if p.pane_floating_flag == "1"] found = len(floating) >= 1 else: found = True` without exception; execute `found` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_floating_panes_md_docs_topics_floating_panes_md_3_30f804e434 | 3.3a through 3.7c | planned parity | +| docs/topics/format-tokens.md::docs/topics/format-tokens.md[0] | execute `session = server.new_session()` without exception; execute `session.pane_id == session.active_window.active_pane.pane_id` and expect `True`; execute `session.window_id == session.active_window.window_id` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_format_tokens_md_docs_topics_format_tokens_md_0_fcbd5bfda4 | 3.3a through 3.7c | planned parity | +| docs/topics/format-tokens.md::docs/topics/format-tokens.md[1] | execute `from libtmux.neo import get_output_format` without exception; execute `fields, _ = get_output_format("list-sessions", "3.6a")` without exception; execute `'session_id' in fields` and expect `True`; execute `'pane_id' in fields # active pane for the listed session` and expect `True`; execute `'client_name' in fields # client fields require list-clients` and expect `False`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_format_tokens_md_docs_topics_format_tokens_md_1_c73624c189 | 3.3a through 3.7c | planned parity | | docs/topics/format-tokens.md::docs/topics/format-tokens.md[2] | execute `from libtmux.neo import get_output_format` without exception; execute `fields, _ = get_output_format("list-clients", "3.6a")` without exception; execute `all(t in fields for t in ("client_name", "session_id", "pane_id"))` and expect `True`. | documentation contract test | PythonBehaviorParityTest#docs_topics_format_tokens_md_docs_topics_format_tokens_md_2_9e413d8b28 | not tmux-bound | planned parity | -| docs/topics/options_and_hooks.md::docs/topics/options_and_hooks.md[0] | execute `session.show_options() # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `{...}`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_options_and_hooks_md_docs_topics_options_and_hooks_md_0_3437dd3c06 | 3.2a through 3.7b | planned parity | -| docs/topics/options_and_hooks.md::docs/topics/options_and_hooks.md[1] | execute `server.show_option('buffer-limit')` and expect `50`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_options_and_hooks_md_docs_topics_options_and_hooks_md_1_34244a97a6 | 3.2a through 3.7b | planned parity | -| docs/topics/options_and_hooks.md::docs/topics/options_and_hooks.md[2] | execute `window.set_option('automatic-rename', False) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...)`; execute `window.show_option('automatic-rename')` and expect `False`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_options_and_hooks_md_docs_topics_options_and_hooks_md_2_a27878544f | 3.2a through 3.7b | planned parity | -| docs/topics/options_and_hooks.md::docs/topics/options_and_hooks.md[3] | execute `window.unset_option('automatic-rename') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_options_and_hooks_md_docs_topics_options_and_hooks_md_3_b5bbcbef53 | 3.2a through 3.7b | planned parity | -| docs/topics/options_and_hooks.md::docs/topics/options_and_hooks.md[4] | execute `from libtmux.constants import OptionScope` without exception; execute `session.show_options(scope=OptionScope.Window) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `{...}`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_options_and_hooks_md_docs_topics_options_and_hooks_md_4_8ff1aac550 | 3.2a through 3.7b | planned parity | -| docs/topics/options_and_hooks.md::docs/topics/options_and_hooks.md[5] | execute `server.show_option('buffer-limit', global_=True)` and expect `50`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_options_and_hooks_md_docs_topics_options_and_hooks_md_5_d4bcae3a85 | 3.2a through 3.7b | planned parity | -| docs/topics/options_and_hooks.md::docs/topics/options_and_hooks.md[6] | execute `session.set_hook('session-renamed', 'display-message "Session renamed"') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Session(...)`; execute `session.show_hook('session-renamed') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `{0: 'display-message "Session renamed"'}`; execute `session.show_hooks() # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `{...}`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_options_and_hooks_md_docs_topics_options_and_hooks_md_6_890f6b0ea5 | 3.2a through 3.7b | planned parity | -| docs/topics/options_and_hooks.md::docs/topics/options_and_hooks.md[7] | execute `session.unset_hook('session-renamed') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Session(...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_options_and_hooks_md_docs_topics_options_and_hooks_md_7_63ed9539ef | 3.2a through 3.7b | planned parity | -| docs/topics/options_and_hooks.md::docs/topics/options_and_hooks.md[8] | execute `session.set_hook('after-split-window[0]', 'display-message "Split 0"') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Session(...)`; execute `session.set_hook('after-split-window[1]', 'display-message "Split 1"') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Session(...)`; execute `hooks = session.show_hook('after-split-window')` without exception; execute `sorted(hooks.keys())` and expect `[0, 1]`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_options_and_hooks_md_docs_topics_options_and_hooks_md_8_5eb13b767c | 3.2a through 3.7b | planned parity | -| docs/topics/options_and_hooks.md::docs/topics/options_and_hooks.md[9] | execute `session.set_hooks('window-linked', { 0: 'display-message "Window linked 0"', 1: 'display-message "Window linked 1"', }) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Session(...)`; execute `session.unset_hook('after-split-window[0]') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Session(...)`; execute `session.unset_hook('after-split-window[1]') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Session(...)`; execute `session.unset_hook('window-linked[0]') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Session(...)`; execute `session.unset_hook('window-linked[1]') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Session(...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_options_and_hooks_md_docs_topics_options_and_hooks_md_9_97552747a8 | 3.2a through 3.7b | planned parity | -| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[0] | execute `pane = window.split(shell='sh')` without exception; execute `pane.send_keys('echo "Hello from libtmux"')` without exception; execute `import time; time.sleep(0.1) # Allow command to execute` without exception; execute `output = pane.capture_pane()` without exception; execute `'Hello from libtmux' in '\\n'.join(output)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_0_645b933013 | 3.2a through 3.7b | planned parity | -| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[10] | execute `import time` without exception; execute `pane.send_keys('printf "text \\n"') # 3 trailing spaces` without exception; execute `time.sleep(0.1)` without exception; execute `output = pane.capture_pane(preserve_trailing=True)` without exception; execute `isinstance(output, list)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_10_1ebe21a3c5 | 3.2a through 3.7b | planned parity | -| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[11] | execute `pending = pane.capture_pane(pending=True)` without exception; execute `isinstance(pending, list)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_11_82b8149fda | 3.2a through 3.7b | planned parity | -| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[12] | execute `import time` without exception; execute `pane.send_keys('sleep 0.2; echo "TASK_COMPLETE"')` without exception; execute `for _ in range(30): output = pane.capture_pane() if 'TASK_COMPLETE' in '\\n'.join(output): break time.sleep(0.1)` without exception; execute `'TASK_COMPLETE' in '\\n'.join(output)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_12_77f2508eca | 3.2a through 3.7b | planned parity | -| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[13] | execute `import time` without exception; execute `def wait_for_text(pane, text, timeout=5.0): """Wait for text to appear in pane output.""" start = time.time() while time.time() - start < timeout: output = pane.capture_pane() if text in '\\n'.join(output): return True time.sleep(0.1) return False` without exception; execute `pane.send_keys('echo "READY"')` without exception; execute `wait_for_text(pane, 'READY', timeout=2.0)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_13_2289744fea | 3.2a through 3.7b | planned parity | -| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[14] | execute `width = pane.display_message('#{pane_width}', get_text=True)` without exception; execute `isinstance(width, list) and len(width) > 0` and expect `True`; execute `height = pane.display_message('#{pane_height}', get_text=True)` without exception; execute `isinstance(height, list) and len(height) > 0` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_14_8f6850cdbd | 3.2a through 3.7b | planned parity | -| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[15] | execute `cwd = pane.display_message('#{pane_current_path}', get_text=True)` without exception; execute `isinstance(cwd, list)` and expect `True`; execute `pane_id = pane.display_message('#{pane_id}', get_text=True)` without exception; execute `pane_id[0].startswith('%')` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_15_37a93f3fac | 3.2a through 3.7b | planned parity | -| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[16] | execute `pane.resize(height=20, width=80) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Pane(%... Window(@... ..., Session($... ...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_16_9f44e98c9c | 3.2a through 3.7b | planned parity | -| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[17] | execute `from libtmux.constants import ResizeAdjustmentDirection` without exception; execute `pane.resize(adjustment_direction=ResizeAdjustmentDirection.Up, adjustment=5) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Pane(%... Window(@... ..., Session($... ...)))`; execute `pane.resize(adjustment_direction=ResizeAdjustmentDirection.Left, adjustment=10) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Pane(%... Window(@... ..., Session($... ...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_17_1edb425a8f | 3.2a through 3.7b | planned parity | -| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[18] | execute `pane.resize(zoom=True) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Pane(%... Window(@... ..., Session($... ...)))`; execute `pane.resize(zoom=True) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Pane(%... Window(@... ..., Session($... ...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_18_3cdae56da5 | 3.2a through 3.7b | planned parity | -| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[19] | execute `pane.clear() # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Pane(%... Window(@... ..., Session($... ...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_19_0f33de1765 | 3.2a through 3.7b | planned parity | -| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[1] | execute `pane.send_keys('echo "waiting"', enter=False)` without exception; execute `output = pane.capture_pane()` without exception; execute `'waiting' in '\\n'.join(output)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_1_b6a6b530c5 | 3.2a through 3.7b | planned parity | -| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[20] | execute `temp_pane = pane.split()` without exception; execute `temp_pane in window.panes` and expect `True`; execute `temp_pane.kill()` without exception; execute `temp_pane not in window.panes` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_20_3cadeac64e | 3.2a through 3.7b | planned parity | -| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[21] | execute `pane.window.resize(height=60, width=120) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...)`; execute `keep_pane = pane.split()` without exception; execute `extra1 = pane.split()` without exception; execute `extra2 = pane.split()` without exception; execute `keep_pane.kill(all_except=True)` without exception; execute `keep_pane in window.panes` and expect `True`; execute `extra1 not in window.panes` and expect `True`; execute `extra2 not in window.panes` and expect `True`; execute `keep_pane.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_21_8fbc703e15 | 3.2a through 3.7b | planned parity | -| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[22] | execute `import time` without exception; execute `def run_and_capture(pane, command, marker='__DONE__', timeout=5.0): """Run a command and return its output.""" pane.send_keys(f'{command}; echo {marker}') start = time.time() while time.time() - start < timeout: output = pane.capture_pane() output_str = '\\n'.join(output) if marker in output_str: return output # Return all captured output time.sleep(0.1) raise TimeoutError(f'Command did not complete within {timeout}s')` without exception; execute `result = run_and_capture(pane, 'echo "captured text"', timeout=2.0)` without exception; execute `'captured text' in '\\n'.join(result)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_22_bb780f31fc | 3.2a through 3.7b | planned parity | -| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[23] | execute `import time` without exception; execute `def check_for_errors(pane, error_patterns=None): """Check pane output for error patterns.""" if error_patterns is None: error_patterns = ['error:', 'Error:', 'ERROR', 'failed', 'FAILED'] output = '\\n'.join(pane.capture_pane()) for pattern in error_patterns: if pattern in output: return True return False` without exception; execute `pane.send_keys('echo "All good"')` without exception; execute `time.sleep(0.1)` without exception; execute `check_for_errors(pane)` and expect `False`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_23_edda539e05 | 3.2a through 3.7b | planned parity | -| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[2] | execute `import time` without exception; execute `pane.send_keys('echo "execute me"', enter=False)` without exception; execute `pane.enter() # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Pane(%... Window(@... ..., Session($... ...)))`; execute `time.sleep(0.2)` without exception; execute `output = pane.capture_pane()` without exception; execute `'execute me' in '\\n'.join(output)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_2_6bca8e9329 | 3.2a through 3.7b | planned parity | -| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[3] | execute `import time` without exception; execute `pane.send_keys('echo "Tab:\\tNewline:\\n"', literal=True)` without exception; execute `time.sleep(0.1)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_3_36d5ab640c | 3.2a through 3.7b | planned parity | -| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[4] | execute `import time` without exception; execute `pane.send_keys('echo "secret command"', suppress_history=True)` without exception; execute `time.sleep(0.1)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_4_a8a73dc1c7 | 3.2a through 3.7b | planned parity | -| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[5] | execute `pane.send_keys(cmd=None, repeat=5)` without exception; execute `pane.send_keys(cmd=None, reset=True)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_5_70e41a9e8b | 3.2a through 3.7b | planned parity | -| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[6] | execute `import time` without exception; execute `pane.send_keys('echo "Line 1"; echo "Line 2"; echo "Line 3"')` without exception; execute `time.sleep(0.1)` without exception; execute `output = pane.capture_pane()` without exception; execute `isinstance(output, list)` and expect `True`; execute `any('Line 2' in line for line in output)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_6_188ee9f8fd | 3.2a through 3.7b | planned parity | -| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[7] | execute `recent = pane.capture_pane(start=-5, end='-')` without exception; execute `isinstance(recent, list)` and expect `True`; execute `full_history = pane.capture_pane(start='-', end='-')` without exception; execute `len(full_history) >= 0` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_7_6cbf3a638c | 3.2a through 3.7b | planned parity | -| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[8] | execute `import time` without exception; execute `pane.send_keys('printf "\\033[31mRED\\033[0m \\033[32mGREEN\\033[0m"')` without exception; execute `time.sleep(0.1)` without exception; execute `output = pane.capture_pane()` without exception; execute `'RED' in '\\n'.join(output)` and expect `True`; execute `colored_output = pane.capture_pane(escape_sequences=True)` without exception; execute `isinstance(colored_output, list)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_8_7f49ead560 | 3.2a through 3.7b | planned parity | -| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[9] | execute `import time` without exception; execute `pane.send_keys('echo "' + 'x' * 200 + '"')` without exception; execute `time.sleep(0.1)` without exception; execute `output = pane.capture_pane(join_wrapped=True)` without exception; execute `isinstance(output, list)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_9_0a43f7ce1c | 3.2a through 3.7b | planned parity | -| docs/topics/self_location.md::docs/topics/self_location.md[0] | execute `socket_path = server.cmd( "display-message", "-p", "-t", session.session_id, "#{socket_path}" ).stdout[0]` without exception; execute `monkeypatch.setenv("TMUX", f"{socket_path},1,{session.session_id}")` without exception; execute `monkeypatch.setenv("TMUX_PANE", pane.pane_id)` without exception; execute `Pane.from_env().pane_id == pane.pane_id` and expect `True`; execute `Window.from_env().window_id == window.window_id` and expect `True`; execute `Session.from_env().session_id == session.session_id` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_self_location_md_docs_topics_self_location_md_0_ff37fbb90b | 3.2a through 3.7b | planned parity | -| docs/topics/self_location.md::docs/topics/self_location.md[1] | execute `socket_path = server.cmd( "display-message", "-p", "-t", session.session_id, "#{socket_path}" ).stdout[0]` without exception; execute `env = { "TMUX": f"{socket_path},1,{session.session_id}", "TMUX_PANE": pane.pane_id, }` without exception; execute `Session.from_env(env).session_id == session.session_id` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_self_location_md_docs_topics_self_location_md_1_ac5fbdbdb2 | 3.2a through 3.7b | planned parity | +| docs/topics/options_and_hooks.md::docs/topics/options_and_hooks.md[0] | execute `session.show_options() # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `{...}`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_options_and_hooks_md_docs_topics_options_and_hooks_md_0_3437dd3c06 | 3.3a through 3.7c | planned parity | +| docs/topics/options_and_hooks.md::docs/topics/options_and_hooks.md[1] | execute `server.show_option('buffer-limit')` and expect `50`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_options_and_hooks_md_docs_topics_options_and_hooks_md_1_34244a97a6 | 3.3a through 3.7c | planned parity | +| docs/topics/options_and_hooks.md::docs/topics/options_and_hooks.md[2] | execute `window.set_option('automatic-rename', False) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...)`; execute `window.show_option('automatic-rename')` and expect `False`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_options_and_hooks_md_docs_topics_options_and_hooks_md_2_a27878544f | 3.3a through 3.7c | planned parity | +| docs/topics/options_and_hooks.md::docs/topics/options_and_hooks.md[3] | execute `window.unset_option('automatic-rename') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_options_and_hooks_md_docs_topics_options_and_hooks_md_3_b5bbcbef53 | 3.3a through 3.7c | planned parity | +| docs/topics/options_and_hooks.md::docs/topics/options_and_hooks.md[4] | execute `from libtmux.constants import OptionScope` without exception; execute `session.show_options(scope=OptionScope.Window) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `{...}`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_options_and_hooks_md_docs_topics_options_and_hooks_md_4_8ff1aac550 | 3.3a through 3.7c | planned parity | +| docs/topics/options_and_hooks.md::docs/topics/options_and_hooks.md[5] | execute `server.show_option('buffer-limit', global_=True)` and expect `50`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_options_and_hooks_md_docs_topics_options_and_hooks_md_5_d4bcae3a85 | 3.3a through 3.7c | planned parity | +| docs/topics/options_and_hooks.md::docs/topics/options_and_hooks.md[6] | execute `session.set_hook('session-renamed', 'display-message "Session renamed"') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Session(...)`; execute `session.show_hook('session-renamed') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `{0: 'display-message "Session renamed"'}`; execute `session.show_hooks() # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `{...}`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_options_and_hooks_md_docs_topics_options_and_hooks_md_6_890f6b0ea5 | 3.3a through 3.7c | planned parity | +| docs/topics/options_and_hooks.md::docs/topics/options_and_hooks.md[7] | execute `session.unset_hook('session-renamed') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Session(...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_options_and_hooks_md_docs_topics_options_and_hooks_md_7_63ed9539ef | 3.3a through 3.7c | planned parity | +| docs/topics/options_and_hooks.md::docs/topics/options_and_hooks.md[8] | execute `session.set_hook('after-split-window[0]', 'display-message "Split 0"') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Session(...)`; execute `session.set_hook('after-split-window[1]', 'display-message "Split 1"') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Session(...)`; execute `hooks = session.show_hook('after-split-window')` without exception; execute `sorted(hooks.keys())` and expect `[0, 1]`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_options_and_hooks_md_docs_topics_options_and_hooks_md_8_5eb13b767c | 3.3a through 3.7c | planned parity | +| docs/topics/options_and_hooks.md::docs/topics/options_and_hooks.md[9] | execute `session.set_hooks('window-linked', { 0: 'display-message "Window linked 0"', 1: 'display-message "Window linked 1"', }) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Session(...)`; execute `session.unset_hook('after-split-window[0]') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Session(...)`; execute `session.unset_hook('after-split-window[1]') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Session(...)`; execute `session.unset_hook('window-linked[0]') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Session(...)`; execute `session.unset_hook('window-linked[1]') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Session(...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_options_and_hooks_md_docs_topics_options_and_hooks_md_9_97552747a8 | 3.3a through 3.7c | planned parity | +| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[0] | execute `pane = window.split(shell='sh')` without exception; execute `pane.send_keys('echo "Hello from libtmux"')` without exception; execute `import time; time.sleep(0.1) # Allow command to execute` without exception; execute `output = pane.capture_pane()` without exception; execute `'Hello from libtmux' in '\\n'.join(output)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_0_645b933013 | 3.3a through 3.7c | planned parity | +| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[10] | execute `import time` without exception; execute `pane.send_keys('printf "text \\n"') # 3 trailing spaces` without exception; execute `time.sleep(0.1)` without exception; execute `output = pane.capture_pane(preserve_trailing=True)` without exception; execute `isinstance(output, list)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_10_1ebe21a3c5 | 3.3a through 3.7c | planned parity | +| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[11] | execute `pending = pane.capture_pane(pending=True)` without exception; execute `isinstance(pending, list)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_11_82b8149fda | 3.3a through 3.7c | planned parity | +| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[12] | execute `import time` without exception; execute `pane.send_keys('sleep 0.2; echo "TASK_COMPLETE"')` without exception; execute `for _ in range(30): output = pane.capture_pane() if 'TASK_COMPLETE' in '\\n'.join(output): break time.sleep(0.1)` without exception; execute `'TASK_COMPLETE' in '\\n'.join(output)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_12_77f2508eca | 3.3a through 3.7c | planned parity | +| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[13] | execute `import time` without exception; execute `def wait_for_text(pane, text, timeout=5.0): """Wait for text to appear in pane output.""" start = time.time() while time.time() - start < timeout: output = pane.capture_pane() if text in '\\n'.join(output): return True time.sleep(0.1) return False` without exception; execute `pane.send_keys('echo "READY"')` without exception; execute `wait_for_text(pane, 'READY', timeout=2.0)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_13_2289744fea | 3.3a through 3.7c | planned parity | +| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[14] | execute `width = pane.display_message('#{pane_width}', get_text=True)` without exception; execute `isinstance(width, list) and len(width) > 0` and expect `True`; execute `height = pane.display_message('#{pane_height}', get_text=True)` without exception; execute `isinstance(height, list) and len(height) > 0` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_14_8f6850cdbd | 3.3a through 3.7c | planned parity | +| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[15] | execute `cwd = pane.display_message('#{pane_current_path}', get_text=True)` without exception; execute `isinstance(cwd, list)` and expect `True`; execute `pane_id = pane.display_message('#{pane_id}', get_text=True)` without exception; execute `pane_id[0].startswith('%')` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_15_37a93f3fac | 3.3a through 3.7c | planned parity | +| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[16] | execute `pane.resize(height=20, width=80) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Pane(%... Window(@... ..., Session($... ...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_16_9f44e98c9c | 3.3a through 3.7c | planned parity | +| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[17] | execute `from libtmux.constants import ResizeAdjustmentDirection` without exception; execute `pane.resize(adjustment_direction=ResizeAdjustmentDirection.Up, adjustment=5) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Pane(%... Window(@... ..., Session($... ...)))`; execute `pane.resize(adjustment_direction=ResizeAdjustmentDirection.Left, adjustment=10) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Pane(%... Window(@... ..., Session($... ...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_17_1edb425a8f | 3.3a through 3.7c | planned parity | +| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[18] | execute `pane.resize(zoom=True) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Pane(%... Window(@... ..., Session($... ...)))`; execute `pane.resize(zoom=True) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Pane(%... Window(@... ..., Session($... ...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_18_3cdae56da5 | 3.3a through 3.7c | planned parity | +| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[19] | execute `pane.clear() # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Pane(%... Window(@... ..., Session($... ...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_19_0f33de1765 | 3.3a through 3.7c | planned parity | +| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[1] | execute `pane.send_keys('echo "waiting"', enter=False)` without exception; execute `output = pane.capture_pane()` without exception; execute `'waiting' in '\\n'.join(output)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_1_b6a6b530c5 | 3.3a through 3.7c | planned parity | +| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[20] | execute `temp_pane = pane.split()` without exception; execute `temp_pane in window.panes` and expect `True`; execute `temp_pane.kill()` without exception; execute `temp_pane not in window.panes` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_20_3cadeac64e | 3.3a through 3.7c | planned parity | +| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[21] | execute `pane.window.resize(height=60, width=120) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...)`; execute `keep_pane = pane.split()` without exception; execute `extra1 = pane.split()` without exception; execute `extra2 = pane.split()` without exception; execute `keep_pane.kill(all_except=True)` without exception; execute `keep_pane in window.panes` and expect `True`; execute `extra1 not in window.panes` and expect `True`; execute `extra2 not in window.panes` and expect `True`; execute `keep_pane.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_21_8fbc703e15 | 3.3a through 3.7c | planned parity | +| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[22] | execute `import time` without exception; execute `def run_and_capture(pane, command, marker='__DONE__', timeout=5.0): """Run a command and return its output.""" pane.send_keys(f'{command}; echo {marker}') start = time.time() while time.time() - start < timeout: output = pane.capture_pane() output_str = '\\n'.join(output) if marker in output_str: return output # Return all captured output time.sleep(0.1) raise TimeoutError(f'Command did not complete within {timeout}s')` without exception; execute `result = run_and_capture(pane, 'echo "captured text"', timeout=2.0)` without exception; execute `'captured text' in '\\n'.join(result)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_22_bb780f31fc | 3.3a through 3.7c | planned parity | +| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[23] | execute `import time` without exception; execute `def check_for_errors(pane, error_patterns=None): """Check pane output for error patterns.""" if error_patterns is None: error_patterns = ['error:', 'Error:', 'ERROR', 'failed', 'FAILED'] output = '\\n'.join(pane.capture_pane()) for pattern in error_patterns: if pattern in output: return True return False` without exception; execute `pane.send_keys('echo "All good"')` without exception; execute `time.sleep(0.1)` without exception; execute `check_for_errors(pane)` and expect `False`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_23_edda539e05 | 3.3a through 3.7c | planned parity | +| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[2] | execute `import time` without exception; execute `pane.send_keys('echo "execute me"', enter=False)` without exception; execute `pane.enter() # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Pane(%... Window(@... ..., Session($... ...)))`; execute `time.sleep(0.2)` without exception; execute `output = pane.capture_pane()` without exception; execute `'execute me' in '\\n'.join(output)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_2_6bca8e9329 | 3.3a through 3.7c | planned parity | +| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[3] | execute `import time` without exception; execute `pane.send_keys('echo "Tab:\\tNewline:\\n"', literal=True)` without exception; execute `time.sleep(0.1)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_3_36d5ab640c | 3.3a through 3.7c | planned parity | +| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[4] | execute `import time` without exception; execute `pane.send_keys('echo "secret command"', suppress_history=True)` without exception; execute `time.sleep(0.1)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_4_a8a73dc1c7 | 3.3a through 3.7c | planned parity | +| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[5] | execute `pane.send_keys(cmd=None, repeat=5)` without exception; execute `pane.send_keys(cmd=None, reset=True)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_5_70e41a9e8b | 3.3a through 3.7c | planned parity | +| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[6] | execute `import time` without exception; execute `pane.send_keys('echo "Line 1"; echo "Line 2"; echo "Line 3"')` without exception; execute `time.sleep(0.1)` without exception; execute `output = pane.capture_pane()` without exception; execute `isinstance(output, list)` and expect `True`; execute `any('Line 2' in line for line in output)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_6_188ee9f8fd | 3.3a through 3.7c | planned parity | +| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[7] | execute `recent = pane.capture_pane(start=-5, end='-')` without exception; execute `isinstance(recent, list)` and expect `True`; execute `full_history = pane.capture_pane(start='-', end='-')` without exception; execute `len(full_history) >= 0` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_7_6cbf3a638c | 3.3a through 3.7c | planned parity | +| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[8] | execute `import time` without exception; execute `pane.send_keys('printf "\\033[31mRED\\033[0m \\033[32mGREEN\\033[0m"')` without exception; execute `time.sleep(0.1)` without exception; execute `output = pane.capture_pane()` without exception; execute `'RED' in '\\n'.join(output)` and expect `True`; execute `colored_output = pane.capture_pane(escape_sequences=True)` without exception; execute `isinstance(colored_output, list)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_8_7f49ead560 | 3.3a through 3.7c | planned parity | +| docs/topics/pane_interaction.md::docs/topics/pane_interaction.md[9] | execute `import time` without exception; execute `pane.send_keys('echo "' + 'x' * 200 + '"')` without exception; execute `time.sleep(0.1)` without exception; execute `output = pane.capture_pane(join_wrapped=True)` without exception; execute `isinstance(output, list)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_pane_interaction_md_docs_topics_pane_interaction_md_9_0a43f7ce1c | 3.3a through 3.7c | planned parity | +| docs/topics/self_location.md::docs/topics/self_location.md[0] | execute `socket_path = server.cmd( "display-message", "-p", "-t", session.session_id, "#{socket_path}" ).stdout[0]` without exception; execute `monkeypatch.setenv("TMUX", f"{socket_path},1,{session.session_id}")` without exception; execute `monkeypatch.setenv("TMUX_PANE", pane.pane_id)` without exception; execute `Pane.from_env().pane_id == pane.pane_id` and expect `True`; execute `Window.from_env().window_id == window.window_id` and expect `True`; execute `Session.from_env().session_id == session.session_id` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_self_location_md_docs_topics_self_location_md_0_ff37fbb90b | 3.3a through 3.7c | planned parity | +| docs/topics/self_location.md::docs/topics/self_location.md[1] | execute `socket_path = server.cmd( "display-message", "-p", "-t", session.session_id, "#{socket_path}" ).stdout[0]` without exception; execute `env = { "TMUX": f"{socket_path},1,{session.session_id}", "TMUX_PANE": pane.pane_id, }` without exception; execute `Session.from_env(env).session_id == session.session_id` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_self_location_md_docs_topics_self_location_md_1_ac5fbdbdb2 | 3.3a through 3.7c | planned parity | | docs/topics/self_location.md::docs/topics/self_location.md[2] | execute `from libtmux import exc` without exception; execute `try: here = Pane.from_env({}) except exc.NotInsideTmux: here = None` without exception; execute `here is None` and expect `True`. | documentation contract test | PythonBehaviorParityTest#docs_topics_self_location_md_docs_topics_self_location_md_2_1461e160ac | not tmux-bound | planned parity | -| docs/topics/self_location.md::docs/topics/self_location.md[3] | execute `socket_path = server.cmd( "display-message", "-p", "-t", session.session_id, "#{socket_path}" ).stdout[0]` without exception; execute `worker_window = session.new_window(window_name="worker", attach=False)` without exception; execute `worker = worker_window.active_pane` without exception; execute `env = { "TMUX": f"{socket_path},1,{session.session_id}", "TMUX_PANE": worker.pane_id, }` without exception; execute `session.active_window.window_id == worker_window.window_id` and expect `False`; execute `Window.from_env(env).window_id == worker_window.window_id` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_self_location_md_docs_topics_self_location_md_3_555e724540 | 3.2a through 3.7b | planned parity | -| docs/topics/self_location.md::docs/topics/self_location.md[4] | execute `socket_path = server.cmd( "display-message", "-p", "-t", session.session_id, "#{socket_path}" ).stdout[0]` without exception; execute `worker_window = session.new_window(window_name="worker", attach=False)` without exception; execute `worker = worker_window.active_pane` without exception; execute `env = { "TMUX": f"{socket_path},1,{session.session_id}", # names *this* session "TMUX_PANE": worker.pane_id, }` without exception; execute `elsewhere = server.new_session(session_name="elsewhere")` without exception; execute `_ = worker_window.move_window(session=elsewhere.session_id, no_select=True)` without exception; execute `Session.from_env(env).session_name # where the pane is, not where TMUX says` and expect `'elsewhere'`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_self_location_md_docs_topics_self_location_md_4_91a24b14b7 | 3.2a through 3.7b | planned parity | -| docs/topics/self_location.md::docs/topics/self_location.md[5] | execute `socket_path = server.cmd( "display-message", "-p", "-t", session.session_id, "#{socket_path}" ).stdout[0]` without exception; execute `home = server.new_session(session_name="aaa-home")` without exception; execute `shared = home.new_window(window_name="shared", attach=False)` without exception; execute `worker = shared.active_pane` without exception; execute `env = { "TMUX": f"{socket_path},1,{home.session_id}", "TMUX_PANE": worker.pane_id, }` without exception; execute `guest = server.new_session(session_name="zzz-guest")` without exception; execute `_ = server.cmd( "link-window", "-s", shared.window_id, "-t", f"{guest.session_id}:" )` without exception; execute `holders = {p.session_name for p in server.panes.filter(pane_id=worker.pane_id)}` without exception; execute `holders == {"aaa-home", "zzz-guest"}` and expect `True`; execute `tmux_says = server.cmd( "display-message", "-p", "-t", worker.pane_id, "#{session_name}" ).stdout[0]` without exception; execute `Session.from_env(env).session_name == tmux_says` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_self_location_md_docs_topics_self_location_md_5_4b9b55ee65 | 3.2a through 3.7b | planned parity | -| docs/topics/self_location.md::docs/topics/self_location.md[6] | execute `def announce(env=None): """Report the session this code is running in.""" return Session.from_env(env).session_name` without exception; execute `socket_path = server.cmd( "display-message", "-p", "-t", session.session_id, "#{socket_path}" ).stdout[0]` without exception; execute `env = { "TMUX": f"{socket_path},1,{session.session_id}", "TMUX_PANE": pane.pane_id, }` without exception; execute `announce(env) == session.session_name` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_self_location_md_docs_topics_self_location_md_6_5e1cc1853a | 3.2a through 3.7b | planned parity | -| docs/topics/traversal.md::docs/topics/traversal.md[0] | execute `session = server.new_session() # Create a test session using existing server` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_0_f88b231524 | 3.2a through 3.7b | planned parity | -| docs/topics/traversal.md::docs/topics/traversal.md[10] | execute `window.panes # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Pane(%... Window(@... ..., Session($... ...)))]`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_10_4ae2d40efe | 3.2a through 3.7b | planned parity | -| docs/topics/traversal.md::docs/topics/traversal.md[11] | execute `window.active_pane # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Pane(%... Window(@... ..., Session($... ...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_11_8613cf476e | 3.2a through 3.7b | planned parity | -| docs/topics/traversal.md::docs/topics/traversal.md[12] | execute `pane = window.panes[0]` without exception; execute `pane.window.window_id == window.window_id` and expect `True`; execute `pane.session.session_id == session.session_id` and expect `True`; execute `pane.server is server` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_12_11d6f8ab2f | 3.2a through 3.7b | planned parity | -| docs/topics/traversal.md::docs/topics/traversal.md[13] | execute `session.windows.filter(window_index=window.window_index) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Window(@... ..., Session($... ...))]`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_13_19094dff60 | 3.2a through 3.7b | planned parity | -| docs/topics/traversal.md::docs/topics/traversal.md[14] | execute `window.panes.get(pane_id=pane.pane_id) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Pane(%... Window(@... ..., Session($... ...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_14_9454947fbb | 3.2a through 3.7b | planned parity | -| docs/topics/traversal.md::docs/topics/traversal.md[15] | execute `w1 = session.new_window(window_name="app-frontend")` without exception; execute `w2 = session.new_window(window_name="app-backend")` without exception; execute `w3 = session.new_window(window_name="logs")` without exception; execute `session.windows.filter(window_name__startswith='app-') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Window(@... ...:app-frontend, Session($... ...)), Window(@... ...:app-backend, Session($... ...))]`; execute `session.windows.filter(window_name__contains='end') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Window(@... ...:app-frontend, Session($... ...)), Window(@... ...:app-backend, Session($... ...))]`; execute `w1.kill()` without exception; execute `w2.kill()` without exception; execute `w3.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_15_ffb8f60600 | 3.2a through 3.7b | planned parity | -| docs/topics/traversal.md::docs/topics/traversal.md[16] | execute `w1 = session.new_window(window_name="MyApp")` without exception; execute `w2 = session.new_window(window_name="myapp-worker")` without exception; execute `session.windows.filter(window_name__istartswith='myapp') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Window(@... ...:MyApp, Session($... ...)), Window(@... ...:myapp-worker, Session($... ...))]`; execute `w1.kill()` without exception; execute `w2.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_16_ce5ff58725 | 3.2a through 3.7b | planned parity | -| docs/topics/traversal.md::docs/topics/traversal.md[17] | execute `w1 = session.new_window(window_name="release-v1-0")` without exception; execute `w2 = session.new_window(window_name="release-v2-0")` without exception; execute `w3 = session.new_window(window_name="dev")` without exception; execute `session.windows.filter(window_name__regex=r'v\d+-\d+') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Window(@... ...:release-v1-0, Session($... ...)), Window(@... ...:release-v2-0, Session($... ...))]`; execute `w1.kill()` without exception; execute `w2.kill()` without exception; execute `w3.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_17_7ea3dd13ef | 3.2a through 3.7b | planned parity | -| docs/topics/traversal.md::docs/topics/traversal.md[18] | execute `w1 = session.new_window(window_name="api-prod")` without exception; execute `w2 = session.new_window(window_name="api-staging")` without exception; execute `w3 = session.new_window(window_name="web-prod")` without exception; execute `session.windows.filter( window_name__startswith='api', window_name__endswith='prod' ) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Window(@... ...:api-prod, Session($... ...))]`; execute `session.windows.filter( window_name__contains='api' ).filter( window_name__contains='staging' ) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Window(@... ...:api-staging, Session($... ...))]`; execute `w1.kill()` without exception; execute `w2.kill()` without exception; execute `w3.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_18_70edda95b8 | 3.2a through 3.7b | planned parity | -| docs/topics/traversal.md::docs/topics/traversal.md[19] | execute `session.windows.get(window_name="nonexistent", default=None) is None` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_19_a3c307694b | 3.2a through 3.7b | planned parity | -| docs/topics/traversal.md::docs/topics/traversal.md[1] | execute `server # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Server(socket_name=...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_1_c1d67d1446 | 3.2a through 3.7b | planned parity | -| docs/topics/traversal.md::docs/topics/traversal.md[20] | execute `window in session.windows` and expect `True`; execute `pane in window.panes` and expect `True`; execute `session in server.sessions` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_20_953a737631 | 3.2a through 3.7b | planned parity | -| docs/topics/traversal.md::docs/topics/traversal.md[21] | execute `window.window_id == session.active_window.window_id` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_21_7043421454 | 3.2a through 3.7b | planned parity | -| docs/topics/traversal.md::docs/topics/traversal.md[22] | execute `pane.pane_id == window.active_pane.pane_id` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_22_1a26987032 | 3.2a through 3.7b | planned parity | -| docs/topics/traversal.md::docs/topics/traversal.md[2] | execute `server.sessions # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Session($... ...)]`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_2_15eb87751f | 3.2a through 3.7b | planned parity | -| docs/topics/traversal.md::docs/topics/traversal.md[3] | execute `server.windows # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Window(@... ..., Session($... ...))]`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_3_e82a9da6b0 | 3.2a through 3.7b | planned parity | -| docs/topics/traversal.md::docs/topics/traversal.md[4] | execute `server.panes # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Pane(%... Window(@... ..., Session($... ...)))]`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_4_30f754f9a2 | 3.2a through 3.7b | planned parity | -| docs/topics/traversal.md::docs/topics/traversal.md[5] | execute `session = server.sessions[0]` without exception; execute `session # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Session($... ...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_5_7fb785fe9d | 3.2a through 3.7b | planned parity | -| docs/topics/traversal.md::docs/topics/traversal.md[6] | execute `session.windows # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Window(@... ..., Session($... ...))]`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_6_a18e5f213c | 3.2a through 3.7b | planned parity | -| docs/topics/traversal.md::docs/topics/traversal.md[7] | execute `session.active_window # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ..., Session($... ...))`; execute `session.active_pane # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Pane(%... Window(@... ..., Session($... ...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_7_42d2312d36 | 3.2a through 3.7b | planned parity | -| docs/topics/traversal.md::docs/topics/traversal.md[8] | execute `window = session.windows[0]` without exception; execute `window.window_index # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `'...'`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_8_529cc07680 | 3.2a through 3.7b | planned parity | -| docs/topics/traversal.md::docs/topics/traversal.md[9] | execute `window.session # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Session($... ...)`; execute `window.session.session_id == session.session_id` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_9_dfba55476c | 3.2a through 3.7b | planned parity | -| docs/topics/workspace_setup.md::docs/topics/workspace_setup.md[0] | execute `new_window = session.new_window(window_name='workspace')` without exception; execute `new_window # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...:workspace, Session($... ...))`; execute `new_window in session.windows` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_workspace_setup_md_docs_topics_workspace_setup_md_0_f498347395 | 3.2a through 3.7b | planned parity | -| docs/topics/workspace_setup.md::docs/topics/workspace_setup.md[10] | execute `from libtmux.constants import PaneDirection` without exception; execute `def create_pane_grid(session, rows=2, cols=2, name='grid'): """Create an NxM grid of panes.""" window = session.new_window(window_name=name, attach=False) window.resize(height=50, width=160) panes = [] base_pane = window.active_pane panes.append(base_pane) # Create first row of panes current = base_pane for _ in range(cols - 1): new_pane = current.split(direction=PaneDirection.Right) panes.append(new_pane) current = new_pane # Create additional rows for _ in range(rows - 1): row_start = panes[-cols] current = row_start for col in range(cols): new_pane = panes[-cols + col].split(direction=PaneDirection.Below) panes.append(new_pane) # Apply tiled layout for even distribution window.select_layout('tiled') return window, panes` without exception; execute `grid_window, grid_panes = create_pane_grid(session, rows=2, cols=2, name='test-grid')` without exception; execute `len(grid_panes) >= 4` and expect `True`; execute `grid_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_workspace_setup_md_docs_topics_workspace_setup_md_10_ccd6daaff8 | 3.2a through 3.7b | planned parity | -| docs/topics/workspace_setup.md::docs/topics/workspace_setup.md[11] | execute `import time` without exception; execute `def run_in_panes(panes, commands): """Run different commands in each pane.""" for pane, cmd in zip(panes, commands): pane.send_keys(cmd)` without exception; execute `multi_window = session.new_window(window_name='multi-cmd', attach=False)` without exception; execute `multi_window.resize(height=40, width=120) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...)`; execute `pane_a = multi_window.active_pane` without exception; execute `pane_b = multi_window.split()` without exception; execute `pane_c = multi_window.split()` without exception; execute `run_in_panes( [pane_a, pane_b, pane_c], ['echo "Task A"', 'echo "Task B"', 'echo "Task C"'], )` without exception; execute `time.sleep(0.2)` without exception; execute `'Task A' in '\\n'.join(pane_a.capture_pane())` and expect `True`; execute `multi_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_workspace_setup_md_docs_topics_workspace_setup_md_11_55bf63f4b7 | 3.2a through 3.7b | planned parity | -| docs/topics/workspace_setup.md::docs/topics/workspace_setup.md[12] | execute `with session.new_window(window_name='temp-window') as temp_win: pane = temp_win.active_pane pane.send_keys('echo "temporary workspace"') temp_win in session.windows` and expect `True`; execute `temp_win not in session.windows` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_workspace_setup_md_docs_topics_workspace_setup_md_12_4ce17f6320 | 3.2a through 3.7b | planned parity | -| docs/topics/workspace_setup.md::docs/topics/workspace_setup.md[1] | execute `background_window = session.new_window( window_name='background-task', attach=False, )` without exception; execute `background_window # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...:background-task, Session($... ...))`; execute `background_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_workspace_setup_md_docs_topics_workspace_setup_md_1_39bd95e35f | 3.2a through 3.7b | planned parity | -| docs/topics/workspace_setup.md::docs/topics/workspace_setup.md[2] | execute `shell_window = session.new_window( window_name='shell-test', attach=False, window_shell='sh -c "echo Hello; exec sh"', )` without exception; execute `shell_window # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...:shell-test, Session($... ...))`; execute `shell_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_workspace_setup_md_docs_topics_workspace_setup_md_2_2571ab047c | 3.2a through 3.7b | planned parity | -| docs/topics/workspace_setup.md::docs/topics/workspace_setup.md[3] | execute `import time` without exception; execute `from libtmux.constants import PaneDirection` without exception; execute `v_split_window = session.new_window(window_name='v-split-demo', attach=False)` without exception; execute `v_split_window.resize(height=40, width=120) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...)`; execute `top_pane = v_split_window.active_pane` without exception; execute `bottom_pane = v_split_window.split()` without exception; execute `bottom_pane # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Pane(%... Window(@... ..., Session($... ...)))`; execute `len(v_split_window.panes)` and expect `2`; execute `v_split_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_workspace_setup_md_docs_topics_workspace_setup_md_3_36d833b5c9 | 3.2a through 3.7b | planned parity | -| docs/topics/workspace_setup.md::docs/topics/workspace_setup.md[4] | execute `from libtmux.constants import PaneDirection` without exception; execute `h_split_window = session.new_window(window_name='h-split', attach=False)` without exception; execute `h_split_window.resize(height=40, width=120) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...)`; execute `left_pane = h_split_window.active_pane` without exception; execute `right_pane = left_pane.split(direction=PaneDirection.Right)` without exception; execute `right_pane # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Pane(%... Window(@... ..., Session($... ...)))`; execute `len(h_split_window.panes)` and expect `2`; execute `h_split_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_workspace_setup_md_docs_topics_workspace_setup_md_4_30a3c7caed | 3.2a through 3.7b | planned parity | -| docs/topics/workspace_setup.md::docs/topics/workspace_setup.md[5] | execute `size_window = session.new_window(window_name='size-demo', attach=False)` without exception; execute `size_window.resize(height=40, width=120) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...)`; execute `main_pane = size_window.active_pane` without exception; execute `small_pane = main_pane.split(size='20%')` without exception; execute `small_pane # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Pane(%... Window(@... ..., Session($... ...)))`; execute `size_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_workspace_setup_md_docs_topics_workspace_setup_md_5_ce8c840d88 | 3.2a through 3.7b | planned parity | -| docs/topics/workspace_setup.md::docs/topics/workspace_setup.md[6] | execute `layout_window = session.new_window(window_name='layout-demo', attach=False)` without exception; execute `layout_window.resize(height=60, width=120) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...)`; execute `pane1 = layout_window.active_pane` without exception; execute `pane2 = layout_window.split()` without exception; execute `pane3 = layout_window.split()` without exception; execute `pane4 = layout_window.split()` without exception; execute `layout_window.select_layout('tiled') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...)`; execute `layout_window.select_layout('even-horizontal') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...)`; execute `layout_window.select_layout('main-vertical') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...)`; execute `layout_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_workspace_setup_md_docs_topics_workspace_setup_md_6_23162871d8 | 3.2a through 3.7b | planned parity | -| docs/topics/workspace_setup.md::docs/topics/workspace_setup.md[7] | execute `rename_window = session.new_window(window_name='old-name', attach=False)` without exception; execute `rename_window.rename_window('new-name') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...:new-name, Session($... ...))`; execute `rename_window.window_name` and expect `'new-name'`; execute `rename_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_workspace_setup_md_docs_topics_workspace_setup_md_7_103a4558c1 | 3.2a through 3.7b | planned parity | -| docs/topics/workspace_setup.md::docs/topics/workspace_setup.md[8] | execute `demo_window = session.new_window(window_name='props-demo', attach=False)` without exception; execute `demo_window.window_index # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `'...'`; execute `demo_window.window_id # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `'@...'`; execute `demo_window.session # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Session($... ...)`; execute `demo_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_workspace_setup_md_docs_topics_workspace_setup_md_8_2358ddc219 | 3.2a through 3.7b | planned parity | -| docs/topics/workspace_setup.md::docs/topics/workspace_setup.md[9] | execute `import time` without exception; execute `from libtmux.constants import PaneDirection` without exception; execute `def create_dev_workspace(session, name='dev'): """Create a typical development workspace layout.""" window = session.new_window(window_name=name, attach=False) window.resize(height=50, width=160) # Main editing pane (large, left side) main_pane = window.active_pane # Terminal pane (bottom) terminal_pane = main_pane.split(size='30%') # Logs pane (right side of terminal) log_pane = terminal_pane.split(direction=PaneDirection.Right) return { 'window': window, 'main': main_pane, 'terminal': terminal_pane, 'logs': log_pane, }` without exception; execute `workspace = create_dev_workspace(session, 'my-project')` without exception; execute `len(workspace['window'].panes)` and expect `3`; execute `workspace['window'].kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_workspace_setup_md_docs_topics_workspace_setup_md_9_21f429a720 | 3.2a through 3.7b | planned parity | -| source-decision::attached-session-count | Given session rows whose session_attached values are '0', '1', and '2', Server.attachedSessions() returns only the '1' and '2' sessions by testing the attached count greater than zero; it never dispatches the Python noeq lookup. | deliberate correction contract | PythonBehaviorParityTest#source_decision_attached_session_count_d43f2e80e4 | 3.2a through 3.7b | deliberate Java correction | -| source-decision::list-accessor-leniency | Live listings, finders and snapshot capture throw when tmux is absent or capture fails. Successful empty captures retain server identity. Raw commands keep nonzero exit status as data. | deliberate correction contract | ServerTest#liveReadsRejectAnAbsentDaemon | 3.2a through 3.7b | deliberate Java correction | -| src/libtmux/_internal/constants.py::libtmux._internal.constants.Hooks | execute `from libtmux._internal.constants import Hooks` without exception; execute `raw = [ "session-renamed[0] set-option -g status-left-style bg=red", "session-renamed[1] display-message 'session renamed'", ]` without exception; execute `hooks = Hooks.from_stdout(raw)` without exception; execute `hooks.session_renamed[0]` and expect `'set-option -g status-left-style bg=red'`; execute `hooks.session_renamed[1]` and expect `"display-message 'session renamed'"`; execute `hooks.session_renamed.as_list()` and expect `['set-option -g status-left-style bg=red', "display-message 'session renamed'"]`; execute `raw_sparse = [ "pane-focus-in[0] refresh-client", "pane-focus-in[5] display-message 'focus'", ]` without exception; execute `hooks_sparse = Hooks.from_stdout(raw_sparse)` without exception; execute `0 in hooks_sparse.pane_focus_in` and expect `True`; execute `5 in hooks_sparse.pane_focus_in` and expect `True`; execute `3 in hooks_sparse.pane_focus_in` and expect `False`; execute `sorted(hooks_sparse.pane_focus_in.keys())` and expect `[0, 5]`; execute `for cmd in hooks_sparse.pane_focus_in.iter_values(): print(cmd)` and expect `refresh-client display-message 'focus'`; execute `raw_multi = [ "after-new-window[0] select-pane -t 0", "after-new-window[1] send-keys 'clear' Enter", "window-renamed[0] refresh-client -S", ]` without exception; execute `hooks_multi = Hooks.from_stdout(raw_multi)` without exception; execute `len(hooks_multi.after_new_window)` and expect `2`; execute `len(hooks_multi.window_renamed)` and expect `1`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_internal_constants_py_libtmux_internal_constants_Hooks_7039df8600 | 3.2a through 3.7b | planned parity | -| src/libtmux/_internal/constants.py::libtmux._internal.constants.Hooks.from_stdout | execute `from libtmux._internal.constants import Hooks` without exception; execute `raw = ["session-renamed[0] display-message 'renamed'"]` without exception; execute `hooks = Hooks.from_stdout(raw)` without exception; execute `hooks.session_renamed[0]` and expect `"display-message 'renamed'"`; execute `raw = [ "after-select-window[0] refresh-client", "after-select-window[10] display-message 'selected'", ]` without exception; execute `hooks = Hooks.from_stdout(raw)` without exception; execute `sorted(hooks.after_select_window.keys())` and expect `[0, 10]`; execute `hooks_empty = Hooks.from_stdout([])` without exception; execute `len(hooks_empty.session_renamed)` and expect `0`; execute `hooks_empty.session_renamed.as_list()` and expect `[]`. | real-tmux documentation contract test | PythonBehaviorParityTest#btmux_internal_constants_py_libtmux_internal_constants_Hooks_from_stdout_8dc0c3b379 | 3.2a through 3.7b | planned parity | -| src/libtmux/_internal/control_mode.py::libtmux._internal.control_mode.ControlMode | execute `with ControlMode(server=server, session=session) as ctl: clients = server.list_clients() assert len(clients) > 0 assert ctl.client_name != ''` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#btmux_internal_control_mode_py_libtmux_internal_control_mode_ControlMode_d7e85867d7 | 3.2a through 3.7b | planned parity | +| docs/topics/self_location.md::docs/topics/self_location.md[3] | execute `socket_path = server.cmd( "display-message", "-p", "-t", session.session_id, "#{socket_path}" ).stdout[0]` without exception; execute `worker_window = session.new_window(window_name="worker", attach=False)` without exception; execute `worker = worker_window.active_pane` without exception; execute `env = { "TMUX": f"{socket_path},1,{session.session_id}", "TMUX_PANE": worker.pane_id, }` without exception; execute `session.active_window.window_id == worker_window.window_id` and expect `False`; execute `Window.from_env(env).window_id == worker_window.window_id` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_self_location_md_docs_topics_self_location_md_3_555e724540 | 3.3a through 3.7c | planned parity | +| docs/topics/self_location.md::docs/topics/self_location.md[4] | execute `socket_path = server.cmd( "display-message", "-p", "-t", session.session_id, "#{socket_path}" ).stdout[0]` without exception; execute `worker_window = session.new_window(window_name="worker", attach=False)` without exception; execute `worker = worker_window.active_pane` without exception; execute `env = { "TMUX": f"{socket_path},1,{session.session_id}", # names *this* session "TMUX_PANE": worker.pane_id, }` without exception; execute `elsewhere = server.new_session(session_name="elsewhere")` without exception; execute `_ = worker_window.move_window(session=elsewhere.session_id, no_select=True)` without exception; execute `Session.from_env(env).session_name # where the pane is, not where TMUX says` and expect `'elsewhere'`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_self_location_md_docs_topics_self_location_md_4_91a24b14b7 | 3.3a through 3.7c | planned parity | +| docs/topics/self_location.md::docs/topics/self_location.md[5] | execute `socket_path = server.cmd( "display-message", "-p", "-t", session.session_id, "#{socket_path}" ).stdout[0]` without exception; execute `home = server.new_session(session_name="aaa-home")` without exception; execute `shared = home.new_window(window_name="shared", attach=False)` without exception; execute `worker = shared.active_pane` without exception; execute `env = { "TMUX": f"{socket_path},1,{home.session_id}", "TMUX_PANE": worker.pane_id, }` without exception; execute `guest = server.new_session(session_name="zzz-guest")` without exception; execute `_ = server.cmd( "link-window", "-s", shared.window_id, "-t", f"{guest.session_id}:" )` without exception; execute `holders = {p.session_name for p in server.panes.filter(pane_id=worker.pane_id)}` without exception; execute `holders == {"aaa-home", "zzz-guest"}` and expect `True`; execute `tmux_says = server.cmd( "display-message", "-p", "-t", worker.pane_id, "#{session_name}" ).stdout[0]` without exception; execute `Session.from_env(env).session_name == tmux_says` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_self_location_md_docs_topics_self_location_md_5_4b9b55ee65 | 3.3a through 3.7c | planned parity | +| docs/topics/self_location.md::docs/topics/self_location.md[6] | execute `def announce(env=None): """Report the session this code is running in.""" return Session.from_env(env).session_name` without exception; execute `socket_path = server.cmd( "display-message", "-p", "-t", session.session_id, "#{socket_path}" ).stdout[0]` without exception; execute `env = { "TMUX": f"{socket_path},1,{session.session_id}", "TMUX_PANE": pane.pane_id, }` without exception; execute `announce(env) == session.session_name` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_self_location_md_docs_topics_self_location_md_6_5e1cc1853a | 3.3a through 3.7c | planned parity | +| docs/topics/traversal.md::docs/topics/traversal.md[0] | execute `session = server.new_session() # Create a test session using existing server` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_0_f88b231524 | 3.3a through 3.7c | planned parity | +| docs/topics/traversal.md::docs/topics/traversal.md[10] | execute `window.panes # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Pane(%... Window(@... ..., Session($... ...)))]`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_10_4ae2d40efe | 3.3a through 3.7c | planned parity | +| docs/topics/traversal.md::docs/topics/traversal.md[11] | execute `window.active_pane # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Pane(%... Window(@... ..., Session($... ...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_11_8613cf476e | 3.3a through 3.7c | planned parity | +| docs/topics/traversal.md::docs/topics/traversal.md[12] | execute `pane = window.panes[0]` without exception; execute `pane.window.window_id == window.window_id` and expect `True`; execute `pane.session.session_id == session.session_id` and expect `True`; execute `pane.server is server` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_12_11d6f8ab2f | 3.3a through 3.7c | planned parity | +| docs/topics/traversal.md::docs/topics/traversal.md[13] | execute `session.windows.filter(window_index=window.window_index) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Window(@... ..., Session($... ...))]`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_13_19094dff60 | 3.3a through 3.7c | planned parity | +| docs/topics/traversal.md::docs/topics/traversal.md[14] | execute `window.panes.get(pane_id=pane.pane_id) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Pane(%... Window(@... ..., Session($... ...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_14_9454947fbb | 3.3a through 3.7c | planned parity | +| docs/topics/traversal.md::docs/topics/traversal.md[15] | execute `w1 = session.new_window(window_name="app-frontend")` without exception; execute `w2 = session.new_window(window_name="app-backend")` without exception; execute `w3 = session.new_window(window_name="logs")` without exception; execute `session.windows.filter(window_name__startswith='app-') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Window(@... ...:app-frontend, Session($... ...)), Window(@... ...:app-backend, Session($... ...))]`; execute `session.windows.filter(window_name__contains='end') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Window(@... ...:app-frontend, Session($... ...)), Window(@... ...:app-backend, Session($... ...))]`; execute `w1.kill()` without exception; execute `w2.kill()` without exception; execute `w3.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_15_ffb8f60600 | 3.3a through 3.7c | planned parity | +| docs/topics/traversal.md::docs/topics/traversal.md[16] | execute `w1 = session.new_window(window_name="MyApp")` without exception; execute `w2 = session.new_window(window_name="myapp-worker")` without exception; execute `session.windows.filter(window_name__istartswith='myapp') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Window(@... ...:MyApp, Session($... ...)), Window(@... ...:myapp-worker, Session($... ...))]`; execute `w1.kill()` without exception; execute `w2.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_16_ce5ff58725 | 3.3a through 3.7c | planned parity | +| docs/topics/traversal.md::docs/topics/traversal.md[17] | execute `w1 = session.new_window(window_name="release-v1-0")` without exception; execute `w2 = session.new_window(window_name="release-v2-0")` without exception; execute `w3 = session.new_window(window_name="dev")` without exception; execute `session.windows.filter(window_name__regex=r'v\d+-\d+') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Window(@... ...:release-v1-0, Session($... ...)), Window(@... ...:release-v2-0, Session($... ...))]`; execute `w1.kill()` without exception; execute `w2.kill()` without exception; execute `w3.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_17_7ea3dd13ef | 3.3a through 3.7c | planned parity | +| docs/topics/traversal.md::docs/topics/traversal.md[18] | execute `w1 = session.new_window(window_name="api-prod")` without exception; execute `w2 = session.new_window(window_name="api-staging")` without exception; execute `w3 = session.new_window(window_name="web-prod")` without exception; execute `session.windows.filter( window_name__startswith='api', window_name__endswith='prod' ) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Window(@... ...:api-prod, Session($... ...))]`; execute `session.windows.filter( window_name__contains='api' ).filter( window_name__contains='staging' ) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Window(@... ...:api-staging, Session($... ...))]`; execute `w1.kill()` without exception; execute `w2.kill()` without exception; execute `w3.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_18_70edda95b8 | 3.3a through 3.7c | planned parity | +| docs/topics/traversal.md::docs/topics/traversal.md[19] | execute `session.windows.get(window_name="nonexistent", default=None) is None` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_19_a3c307694b | 3.3a through 3.7c | planned parity | +| docs/topics/traversal.md::docs/topics/traversal.md[1] | execute `server # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Server(socket_name=...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_1_c1d67d1446 | 3.3a through 3.7c | planned parity | +| docs/topics/traversal.md::docs/topics/traversal.md[20] | execute `window in session.windows` and expect `True`; execute `pane in window.panes` and expect `True`; execute `session in server.sessions` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_20_953a737631 | 3.3a through 3.7c | planned parity | +| docs/topics/traversal.md::docs/topics/traversal.md[21] | execute `window.window_id == session.active_window.window_id` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_21_7043421454 | 3.3a through 3.7c | planned parity | +| docs/topics/traversal.md::docs/topics/traversal.md[22] | execute `pane.pane_id == window.active_pane.pane_id` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_22_1a26987032 | 3.3a through 3.7c | planned parity | +| docs/topics/traversal.md::docs/topics/traversal.md[2] | execute `server.sessions # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Session($... ...)]`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_2_15eb87751f | 3.3a through 3.7c | planned parity | +| docs/topics/traversal.md::docs/topics/traversal.md[3] | execute `server.windows # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Window(@... ..., Session($... ...))]`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_3_e82a9da6b0 | 3.3a through 3.7c | planned parity | +| docs/topics/traversal.md::docs/topics/traversal.md[4] | execute `server.panes # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Pane(%... Window(@... ..., Session($... ...)))]`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_4_30f754f9a2 | 3.3a through 3.7c | planned parity | +| docs/topics/traversal.md::docs/topics/traversal.md[5] | execute `session = server.sessions[0]` without exception; execute `session # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Session($... ...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_5_7fb785fe9d | 3.3a through 3.7c | planned parity | +| docs/topics/traversal.md::docs/topics/traversal.md[6] | execute `session.windows # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `[Window(@... ..., Session($... ...))]`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_6_a18e5f213c | 3.3a through 3.7c | planned parity | +| docs/topics/traversal.md::docs/topics/traversal.md[7] | execute `session.active_window # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ..., Session($... ...))`; execute `session.active_pane # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Pane(%... Window(@... ..., Session($... ...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_7_42d2312d36 | 3.3a through 3.7c | planned parity | +| docs/topics/traversal.md::docs/topics/traversal.md[8] | execute `window = session.windows[0]` without exception; execute `window.window_index # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `'...'`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_8_529cc07680 | 3.3a through 3.7c | planned parity | +| docs/topics/traversal.md::docs/topics/traversal.md[9] | execute `window.session # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Session($... ...)`; execute `window.session.session_id == session.session_id` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_traversal_md_docs_topics_traversal_md_9_dfba55476c | 3.3a through 3.7c | planned parity | +| docs/topics/workspace_setup.md::docs/topics/workspace_setup.md[0] | execute `new_window = session.new_window(window_name='workspace')` without exception; execute `new_window # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...:workspace, Session($... ...))`; execute `new_window in session.windows` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_workspace_setup_md_docs_topics_workspace_setup_md_0_f498347395 | 3.3a through 3.7c | planned parity | +| docs/topics/workspace_setup.md::docs/topics/workspace_setup.md[10] | execute `from libtmux.constants import PaneDirection` without exception; execute `def create_pane_grid(session, rows=2, cols=2, name='grid'): """Create an NxM grid of panes.""" window = session.new_window(window_name=name, attach=False) window.resize(height=50, width=160) panes = [] base_pane = window.active_pane panes.append(base_pane) # Create first row of panes current = base_pane for _ in range(cols - 1): new_pane = current.split(direction=PaneDirection.Right) panes.append(new_pane) current = new_pane # Create additional rows for _ in range(rows - 1): row_start = panes[-cols] current = row_start for col in range(cols): new_pane = panes[-cols + col].split(direction=PaneDirection.Below) panes.append(new_pane) # Apply tiled layout for even distribution window.select_layout('tiled') return window, panes` without exception; execute `grid_window, grid_panes = create_pane_grid(session, rows=2, cols=2, name='test-grid')` without exception; execute `len(grid_panes) >= 4` and expect `True`; execute `grid_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_workspace_setup_md_docs_topics_workspace_setup_md_10_ccd6daaff8 | 3.3a through 3.7c | planned parity | +| docs/topics/workspace_setup.md::docs/topics/workspace_setup.md[11] | execute `import time` without exception; execute `def run_in_panes(panes, commands): """Run different commands in each pane.""" for pane, cmd in zip(panes, commands): pane.send_keys(cmd)` without exception; execute `multi_window = session.new_window(window_name='multi-cmd', attach=False)` without exception; execute `multi_window.resize(height=40, width=120) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...)`; execute `pane_a = multi_window.active_pane` without exception; execute `pane_b = multi_window.split()` without exception; execute `pane_c = multi_window.split()` without exception; execute `run_in_panes( [pane_a, pane_b, pane_c], ['echo "Task A"', 'echo "Task B"', 'echo "Task C"'], )` without exception; execute `time.sleep(0.2)` without exception; execute `'Task A' in '\\n'.join(pane_a.capture_pane())` and expect `True`; execute `multi_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_workspace_setup_md_docs_topics_workspace_setup_md_11_55bf63f4b7 | 3.3a through 3.7c | planned parity | +| docs/topics/workspace_setup.md::docs/topics/workspace_setup.md[12] | execute `with session.new_window(window_name='temp-window') as temp_win: pane = temp_win.active_pane pane.send_keys('echo "temporary workspace"') temp_win in session.windows` and expect `True`; execute `temp_win not in session.windows` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_workspace_setup_md_docs_topics_workspace_setup_md_12_4ce17f6320 | 3.3a through 3.7c | planned parity | +| docs/topics/workspace_setup.md::docs/topics/workspace_setup.md[1] | execute `background_window = session.new_window( window_name='background-task', attach=False, )` without exception; execute `background_window # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...:background-task, Session($... ...))`; execute `background_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_workspace_setup_md_docs_topics_workspace_setup_md_1_39bd95e35f | 3.3a through 3.7c | planned parity | +| docs/topics/workspace_setup.md::docs/topics/workspace_setup.md[2] | execute `shell_window = session.new_window( window_name='shell-test', attach=False, window_shell='sh -c "echo Hello; exec sh"', )` without exception; execute `shell_window # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...:shell-test, Session($... ...))`; execute `shell_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_workspace_setup_md_docs_topics_workspace_setup_md_2_2571ab047c | 3.3a through 3.7c | planned parity | +| docs/topics/workspace_setup.md::docs/topics/workspace_setup.md[3] | execute `import time` without exception; execute `from libtmux.constants import PaneDirection` without exception; execute `v_split_window = session.new_window(window_name='v-split-demo', attach=False)` without exception; execute `v_split_window.resize(height=40, width=120) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...)`; execute `top_pane = v_split_window.active_pane` without exception; execute `bottom_pane = v_split_window.split()` without exception; execute `bottom_pane # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Pane(%... Window(@... ..., Session($... ...)))`; execute `len(v_split_window.panes)` and expect `2`; execute `v_split_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_workspace_setup_md_docs_topics_workspace_setup_md_3_36d833b5c9 | 3.3a through 3.7c | planned parity | +| docs/topics/workspace_setup.md::docs/topics/workspace_setup.md[4] | execute `from libtmux.constants import PaneDirection` without exception; execute `h_split_window = session.new_window(window_name='h-split', attach=False)` without exception; execute `h_split_window.resize(height=40, width=120) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...)`; execute `left_pane = h_split_window.active_pane` without exception; execute `right_pane = left_pane.split(direction=PaneDirection.Right)` without exception; execute `right_pane # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Pane(%... Window(@... ..., Session($... ...)))`; execute `len(h_split_window.panes)` and expect `2`; execute `h_split_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_workspace_setup_md_docs_topics_workspace_setup_md_4_30a3c7caed | 3.3a through 3.7c | planned parity | +| docs/topics/workspace_setup.md::docs/topics/workspace_setup.md[5] | execute `size_window = session.new_window(window_name='size-demo', attach=False)` without exception; execute `size_window.resize(height=40, width=120) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...)`; execute `main_pane = size_window.active_pane` without exception; execute `small_pane = main_pane.split(size='20%')` without exception; execute `small_pane # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Pane(%... Window(@... ..., Session($... ...)))`; execute `size_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_workspace_setup_md_docs_topics_workspace_setup_md_5_ce8c840d88 | 3.3a through 3.7c | planned parity | +| docs/topics/workspace_setup.md::docs/topics/workspace_setup.md[6] | execute `layout_window = session.new_window(window_name='layout-demo', attach=False)` without exception; execute `layout_window.resize(height=60, width=120) # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...)`; execute `pane1 = layout_window.active_pane` without exception; execute `pane2 = layout_window.split()` without exception; execute `pane3 = layout_window.split()` without exception; execute `pane4 = layout_window.split()` without exception; execute `layout_window.select_layout('tiled') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...)`; execute `layout_window.select_layout('even-horizontal') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...)`; execute `layout_window.select_layout('main-vertical') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...)`; execute `layout_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_workspace_setup_md_docs_topics_workspace_setup_md_6_23162871d8 | 3.3a through 3.7c | planned parity | +| docs/topics/workspace_setup.md::docs/topics/workspace_setup.md[7] | execute `rename_window = session.new_window(window_name='old-name', attach=False)` without exception; execute `rename_window.rename_window('new-name') # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Window(@... ...:new-name, Session($... ...))`; execute `rename_window.window_name` and expect `'new-name'`; execute `rename_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_workspace_setup_md_docs_topics_workspace_setup_md_7_103a4558c1 | 3.3a through 3.7c | planned parity | +| docs/topics/workspace_setup.md::docs/topics/workspace_setup.md[8] | execute `demo_window = session.new_window(window_name='props-demo', attach=False)` without exception; execute `demo_window.window_index # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `'...'`; execute `demo_window.window_id # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `'@...'`; execute `demo_window.session # doctest: +ELLIPSIS` with doctest options `+ELLIPSIS` and expect `Session($... ...)`; execute `demo_window.kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_workspace_setup_md_docs_topics_workspace_setup_md_8_2358ddc219 | 3.3a through 3.7c | planned parity | +| docs/topics/workspace_setup.md::docs/topics/workspace_setup.md[9] | execute `import time` without exception; execute `from libtmux.constants import PaneDirection` without exception; execute `def create_dev_workspace(session, name='dev'): """Create a typical development workspace layout.""" window = session.new_window(window_name=name, attach=False) window.resize(height=50, width=160) # Main editing pane (large, left side) main_pane = window.active_pane # Terminal pane (bottom) terminal_pane = main_pane.split(size='30%') # Logs pane (right side of terminal) log_pane = terminal_pane.split(direction=PaneDirection.Right) return { 'window': window, 'main': main_pane, 'terminal': terminal_pane, 'logs': log_pane, }` without exception; execute `workspace = create_dev_workspace(session, 'my-project')` without exception; execute `len(workspace['window'].panes)` and expect `3`; execute `workspace['window'].kill()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#docs_topics_workspace_setup_md_docs_topics_workspace_setup_md_9_21f429a720 | 3.3a through 3.7c | planned parity | +| source-decision::attached-session-count | Given session rows whose session_attached values are '0', '1', and '2', Server.attachedSessions() returns only the '1' and '2' sessions by testing the attached count greater than zero; it never dispatches the Python noeq lookup. | deliberate correction contract | PythonBehaviorParityTest#source_decision_attached_session_count_d43f2e80e4 | 3.3a through 3.7c | deliberate Java correction | +| source-decision::list-accessor-leniency | Live listings, finders and snapshot capture throw when tmux is absent or capture fails. Successful empty captures retain server identity. Raw commands keep nonzero exit status as data. | deliberate correction contract | ServerTest#liveReadsRejectAnAbsentDaemon | 3.3a through 3.7c | deliberate Java correction | +| src/libtmux/_internal/constants.py::libtmux._internal.constants.Hooks | execute `from libtmux._internal.constants import Hooks` without exception; execute `raw = [ "session-renamed[0] set-option -g status-left-style bg=red", "session-renamed[1] display-message 'session renamed'", ]` without exception; execute `hooks = Hooks.from_stdout(raw)` without exception; execute `hooks.session_renamed[0]` and expect `'set-option -g status-left-style bg=red'`; execute `hooks.session_renamed[1]` and expect `"display-message 'session renamed'"`; execute `hooks.session_renamed.as_list()` and expect `['set-option -g status-left-style bg=red', "display-message 'session renamed'"]`; execute `raw_sparse = [ "pane-focus-in[0] refresh-client", "pane-focus-in[5] display-message 'focus'", ]` without exception; execute `hooks_sparse = Hooks.from_stdout(raw_sparse)` without exception; execute `0 in hooks_sparse.pane_focus_in` and expect `True`; execute `5 in hooks_sparse.pane_focus_in` and expect `True`; execute `3 in hooks_sparse.pane_focus_in` and expect `False`; execute `sorted(hooks_sparse.pane_focus_in.keys())` and expect `[0, 5]`; execute `for cmd in hooks_sparse.pane_focus_in.iter_values(): print(cmd)` and expect `refresh-client display-message 'focus'`; execute `raw_multi = [ "after-new-window[0] select-pane -t 0", "after-new-window[1] send-keys 'clear' Enter", "window-renamed[0] refresh-client -S", ]` without exception; execute `hooks_multi = Hooks.from_stdout(raw_multi)` without exception; execute `len(hooks_multi.after_new_window)` and expect `2`; execute `len(hooks_multi.window_renamed)` and expect `1`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_internal_constants_py_libtmux_internal_constants_Hooks_7039df8600 | 3.3a through 3.7c | planned parity | +| src/libtmux/_internal/constants.py::libtmux._internal.constants.Hooks.from_stdout | execute `from libtmux._internal.constants import Hooks` without exception; execute `raw = ["session-renamed[0] display-message 'renamed'"]` without exception; execute `hooks = Hooks.from_stdout(raw)` without exception; execute `hooks.session_renamed[0]` and expect `"display-message 'renamed'"`; execute `raw = [ "after-select-window[0] refresh-client", "after-select-window[10] display-message 'selected'", ]` without exception; execute `hooks = Hooks.from_stdout(raw)` without exception; execute `sorted(hooks.after_select_window.keys())` and expect `[0, 10]`; execute `hooks_empty = Hooks.from_stdout([])` without exception; execute `len(hooks_empty.session_renamed)` and expect `0`; execute `hooks_empty.session_renamed.as_list()` and expect `[]`. | real-tmux documentation contract test | PythonBehaviorParityTest#btmux_internal_constants_py_libtmux_internal_constants_Hooks_from_stdout_8dc0c3b379 | 3.3a through 3.7c | planned parity | +| src/libtmux/_internal/control_mode.py::libtmux._internal.control_mode.ControlMode | execute `with ControlMode(server=server, session=session) as ctl: clients = server.list_clients() assert len(clients) > 0 assert ctl.client_name != ''` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#btmux_internal_control_mode_py_libtmux_internal_control_mode_ControlMode_d7e85867d7 | 3.3a through 3.7c | planned parity | | src/libtmux/_internal/dataclasses.py::libtmux._internal.dataclasses.SkipDefaultFieldsReprMixin | execute `@dataclasses.dataclass() class Item: name: str unit_price: float = 1.00 quantity_on_hand: int = 0` without exception; execute `@dataclasses.dataclass(repr=False) class ItemWithMixin(SkipDefaultFieldsReprMixin): name: str unit_price: float = 1.00 quantity_on_hand: int = 0` without exception; execute `Item('Test')` and expect `Item(name='Test', unit_price=1.0, quantity_on_hand=0)`; execute `ItemWithMixin('Test')` and expect `ItemWithMixin(name=Test)`; execute `Item('Test', quantity_on_hand=2)` and expect `Item(name='Test', unit_price=1.0, quantity_on_hand=2)`; execute `ItemWithMixin('Test', quantity_on_hand=2)` and expect `ItemWithMixin(name=Test, quantity_on_hand=2)`; execute `@dataclasses.dataclass class ItemWithMixin(SkipDefaultFieldsReprMixin): name: str unit_price: float = 1.00 quantity_on_hand: int = 0 __repr__ = SkipDefaultFieldsReprMixin.__repr__` without exception; execute `ItemWithMixin('Test')` and expect `ItemWithMixin(name=Test)`; execute `ItemWithMixin('Test', unit_price=2.00)` and expect `ItemWithMixin(name=Test, unit_price=2.0)`; execute `item = ItemWithMixin('Test')` without exception; execute `item.unit_price = 2.05` without exception; execute `item` and expect `ItemWithMixin(name=Test, unit_price=2.05)`. | documentation contract test | PythonBehaviorParityTest#l_dataclasses_py_libtmux_internal_dataclasses_SkipDefaultFieldsReprMixin_defe4814eb | not tmux-bound | planned parity | | src/libtmux/_internal/env.py::libtmux._internal.env.pane_id_from_env | execute `from libtmux._internal.env import pane_id_from_env` without exception; execute `pane_id_from_env({"TMUX_PANE": "%3"})` and expect `'%3'`; execute `pane_id_from_env({})` and expect `libtmux.exc.NotInsideTmux: Not inside a tmux pane: $TMUX_PANE is unset or empty`; execute `pane_id_from_env({"TMUX_PANE": "3"})` and expect `libtmux.exc.NotInsideTmux: Not inside a tmux pane: $TMUX_PANE is not a pane id...`. | documentation contract test | PythonBehaviorParityTest#src_libtmux_internal_env_py_libtmux_internal_env_pane_id_from_env_89d680cb4f | not tmux-bound | planned parity | | src/libtmux/_internal/env.py::libtmux._internal.env.resolve_env | execute `from libtmux._internal.env import resolve_env` without exception; execute `resolve_env({"TMUX_PANE": "%1"})` and expect `{'TMUX_PANE': '%1'}`; execute `resolve_env() is os.environ` and expect `True`. | documentation contract test | PythonBehaviorParityTest#src_libtmux_internal_env_py_libtmux_internal_env_resolve_env_c97bc4cb66 | not tmux-bound | planned parity | @@ -269,177 +269,177 @@ under [`tests/`](https://github.com/tmux-python/libtmux/blob/c4a980b/tests) and | src/libtmux/_vendor/version.py::libtmux._vendor.version.Version.public | execute `Version("1.2.3").public` and expect `'1.2.3'`; execute `Version("1.2.3+abc").public` and expect `'1.2.3'`; execute `Version("1.2.3+abc.dev1").public` and expect `'1.2.3'`. | documentation contract test | PythonBehaviorParityTest#src_libtmux_vendor_version_py_libtmux_vendor_version_Version_public_a994276b64 | not tmux-bound | planned parity | | src/libtmux/_vendor/version.py::libtmux._vendor.version.Version.release | execute `Version("1.2.3").release` and expect `(1, 2, 3)`; execute `Version("2.0.0").release` and expect `(2, 0, 0)`; execute `Version("1!2.0.0.post0").release` and expect `(2, 0, 0)`. | documentation contract test | PythonBehaviorParityTest#src_libtmux_vendor_version_py_libtmux_vendor_version_Version_release_1cffcad3ee | not tmux-bound | planned parity | | src/libtmux/_vendor/version.py::libtmux._vendor.version.parse | execute `parse('1.0.dev1')` and expect `<Version('1.0.dev1')>`. | documentation contract test | PythonBehaviorParityTest#src_libtmux_vendor_version_py_libtmux_vendor_version_parse_3ccdd22f60 | not tmux-bound | planned parity | -| src/libtmux/client.py::libtmux.client.Client | execute `with control_mode() as ctl: attached = [ c for c in server.clients if c.client_name == ctl.client_name ]` without exception; execute `bool(attached)` and expect `True`; execute `with control_mode() as ctl: client = server.clients.get(client_name=ctl.client_name) client.client_readonly in {"0", "1"}` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_client_py_libtmux_client_Client_817a1e37cc | 3.2a through 3.7b | planned parity | -| src/libtmux/client.py::libtmux.client.Client.attached_session | execute `with control_mode() as ctl: client = server.clients.get(client_name=ctl.client_name) attached = client.attached_session` without exception; execute `attached is not None` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_client_py_libtmux_client_Client_attached_session_8a3f340620 | 3.2a through 3.7b | planned parity | +| src/libtmux/client.py::libtmux.client.Client | execute `with control_mode() as ctl: attached = [ c for c in server.clients if c.client_name == ctl.client_name ]` without exception; execute `bool(attached)` and expect `True`; execute `with control_mode() as ctl: client = server.clients.get(client_name=ctl.client_name) client.client_readonly in {"0", "1"}` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_client_py_libtmux_client_Client_817a1e37cc | 3.3a through 3.7c | planned parity | +| src/libtmux/client.py::libtmux.client.Client.attached_session | execute `with control_mode() as ctl: client = server.clients.get(client_name=ctl.client_name) attached = client.attached_session` without exception; execute `attached is not None` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_client_py_libtmux_client_Client_attached_session_8a3f340620 | 3.3a through 3.7c | planned parity | | src/libtmux/common.py::libtmux.common.get_version_str | execute `isinstance(get_version_str(), str)` and expect `True`. | documentation contract test | PythonBehaviorParityTest#src_libtmux_common_py_libtmux_common_get_version_str_388fd49638 | not tmux-bound | planned parity | -| src/libtmux/common.py::libtmux.common.raise_if_stderr | execute `from libtmux.common import raise_if_stderr` without exception; execute `from libtmux import exc` without exception; execute `proc = session.cmd("display-message", "-p", "#{session_id}")` without exception; execute `raise_if_stderr(proc, "display-message") # no stderr → no raise` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_common_py_libtmux_common_raise_if_stderr_39dfbf9493 | 3.2a through 3.7b | planned parity | -| src/libtmux/common.py::libtmux.common.tmux_cmd | execute `proc = tmux_cmd(f'-L{server.socket_name}', 'new-session', '-d', '-P', '-F#S')` without exception; execute `if proc.stderr: raise exc.LibTmuxException( 'Command: %s returned error: %s' % (proc.cmd, proc.stderr) )` without exception; execute `print(f'tmux command returned {" ".join(proc.stdout)}')` and expect `tmux command returned 2`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_common_py_libtmux_common_tmux_cmd_fdb41cb7c5 | 3.2a through 3.7b | planned parity | +| src/libtmux/common.py::libtmux.common.raise_if_stderr | execute `from libtmux.common import raise_if_stderr` without exception; execute `from libtmux import exc` without exception; execute `proc = session.cmd("display-message", "-p", "#{session_id}")` without exception; execute `raise_if_stderr(proc, "display-message") # no stderr → no raise` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_common_py_libtmux_common_raise_if_stderr_39dfbf9493 | 3.3a through 3.7c | planned parity | +| src/libtmux/common.py::libtmux.common.tmux_cmd | execute `proc = tmux_cmd(f'-L{server.socket_name}', 'new-session', '-d', '-P', '-F#S')` without exception; execute `if proc.stderr: raise exc.LibTmuxException( 'Command: %s returned error: %s' % (proc.cmd, proc.stderr) )` without exception; execute `print(f'tmux command returned {" ".join(proc.stdout)}')` and expect `tmux command returned 2`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_common_py_libtmux_common_tmux_cmd_fdb41cb7c5 | 3.3a through 3.7c | planned parity | | src/libtmux/exc.py::libtmux.exc.MultipleObjectsReturned | execute `from libtmux import exc` without exception; execute `str(exc.MultipleObjectsReturned())` and expect `'Multiple objects returned'`; execute `str(exc.MultipleObjectsReturned(count=2, query={"pane_id": "%0"}))` and expect `"Multiple objects returned (2): pane_id='%0'"`; execute `issubclass(exc.MultipleObjectsReturned, exc.LibTmuxException)` and expect `True`. | documentation contract test | PythonBehaviorParityTest#src_libtmux_exc_py_libtmux_exc_MultipleObjectsReturned_cfd9b5a750 | not tmux-bound | planned parity | -| src/libtmux/exc.py::libtmux.exc.NotInsideTmux | execute `from libtmux import exc` without exception; execute `str(exc.NotInsideTmux("TMUX"))` and expect `'Not inside a tmux pane: $TMUX is unset or empty'`; execute `str(exc.NotInsideTmux("TMUX_PANE", reason="not a pane id"))` and expect `'Not inside a tmux pane: $TMUX_PANE is not a pane id'`; execute `str(exc.NotInsideTmux())` and expect `'Not inside a tmux pane'`; execute `issubclass(exc.NotInsideTmux, exc.LibTmuxException)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_exc_py_libtmux_exc_NotInsideTmux_97adb59e52 | 3.2a through 3.7b | planned parity | +| src/libtmux/exc.py::libtmux.exc.NotInsideTmux | execute `from libtmux import exc` without exception; execute `str(exc.NotInsideTmux("TMUX"))` and expect `'Not inside a tmux pane: $TMUX is unset or empty'`; execute `str(exc.NotInsideTmux("TMUX_PANE", reason="not a pane id"))` and expect `'Not inside a tmux pane: $TMUX_PANE is not a pane id'`; execute `str(exc.NotInsideTmux())` and expect `'Not inside a tmux pane'`; execute `issubclass(exc.NotInsideTmux, exc.LibTmuxException)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_exc_py_libtmux_exc_NotInsideTmux_97adb59e52 | 3.3a through 3.7c | planned parity | | src/libtmux/exc.py::libtmux.exc.ObjectDoesNotExist | execute `from libtmux import exc` without exception; execute `str(exc.ObjectDoesNotExist())` and expect `'No objects found'`; execute `str(exc.ObjectDoesNotExist(query={"pane_id": "%99"}))` and expect `"No objects found: pane_id='%99'"`; execute `issubclass(exc.ObjectDoesNotExist, exc.LibTmuxException)` and expect `True`. | documentation contract test | PythonBehaviorParityTest#src_libtmux_exc_py_libtmux_exc_ObjectDoesNotExist_e6e4f35699 | not tmux-bound | planned parity | | src/libtmux/exc.py::libtmux.exc.TmuxObjectDoesNotExist | execute `from libtmux import exc` without exception; execute `str(exc.TmuxObjectDoesNotExist())` and expect `'Could not find object'`; execute `str( exc.TmuxObjectDoesNotExist( obj_key="pane_id", obj_id="%99", list_cmd="list-panes", list_extra_args=("-t", "%99"), ) )` and expect `"Could not find pane_id=%99 for list-panes ('-t', '%99')"`. | documentation contract test | PythonBehaviorParityTest#src_libtmux_exc_py_libtmux_exc_TmuxObjectDoesNotExist_446e85853e | not tmux-bound | planned parity | | src/libtmux/exc.py::libtmux.exc._format_query | execute `from libtmux.exc import _format_query` without exception; execute `_format_query({"pane_id": "%0"})` and expect `"pane_id='%0'"`; execute `_format_query({"window_name": "shared", "window_index": "1"})` and expect `"window_name='shared', window_index='1'"`; execute `_format_query({})` and expect `''`. | documentation contract test | PythonBehaviorParityTest#src_libtmux_exc_py_libtmux_exc_format_query_ee8f0c47bc | not tmux-bound | planned parity | -| src/libtmux/hooks.py::libtmux.hooks.HooksMixin.set_hooks | execute `session.set_hooks('session-renamed', { 0: 'display-message "hook 0"', 1: 'display-message "hook 1"', })` and expect `Session($...)`; execute `hooks = session.show_hook('session-renamed')` without exception; execute `sorted(hooks.keys())` and expect `[0, 1]`; execute `session.unset_hook('session-renamed')` and expect `Session($...)`; execute `session.set_hooks('after-new-window', [ 'select-pane -t 0', 'send-keys "clear" Enter', ])` and expect `Session($...)`; execute `hooks = session.show_hook('after-new-window')` without exception; execute `sorted(hooks.keys())` and expect `[0, 1]`; execute `session.set_hooks( 'session-renamed', {0: 'display-message "new"'}, clear_existing=True, )` and expect `Session($...)`; execute `hooks = session.show_hook('session-renamed')` without exception; execute `sorted(hooks.keys())` and expect `[0]`; execute `session.unset_hook('session-renamed')` and expect `Session($...)`; execute `session.unset_hook('after-new-window')` and expect `Session($...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_hooks_py_libtmux_hooks_HooksMixin_set_hooks_795d87908d | 3.2a through 3.7b | planned parity | -| src/libtmux/hooks.py::libtmux.hooks.HooksMixin.show_hook | execute `session.set_hook('session-renamed[0]', 'display-message "test"')` and expect `Session($...)`; execute `hooks = session.show_hook('session-renamed')` without exception; execute `isinstance(hooks, SparseArray)` and expect `True`; execute `sorted(hooks.keys())` and expect `[0]`; execute `session.unset_hook('session-renamed')` and expect `Session($...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_hooks_py_libtmux_hooks_HooksMixin_show_hook_a532650d49 | 3.2a through 3.7b | planned parity | -| src/libtmux/hooks.py::libtmux.hooks.HooksMixin.show_hooks | execute `session.set_hook('session-renamed[0]', 'display-message "test"')` and expect `Session($...)`; execute `hooks = session.show_hooks()` without exception; execute `isinstance(hooks, dict)` and expect `True`; execute `'session-renamed[0]' in hooks` and expect `True`; execute `session.unset_hook('session-renamed')` and expect `Session($...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_hooks_py_libtmux_hooks_HooksMixin_show_hooks_25663db72d | 3.2a through 3.7b | planned parity | +| src/libtmux/hooks.py::libtmux.hooks.HooksMixin.set_hooks | execute `session.set_hooks('session-renamed', { 0: 'display-message "hook 0"', 1: 'display-message "hook 1"', })` and expect `Session($...)`; execute `hooks = session.show_hook('session-renamed')` without exception; execute `sorted(hooks.keys())` and expect `[0, 1]`; execute `session.unset_hook('session-renamed')` and expect `Session($...)`; execute `session.set_hooks('after-new-window', [ 'select-pane -t 0', 'send-keys "clear" Enter', ])` and expect `Session($...)`; execute `hooks = session.show_hook('after-new-window')` without exception; execute `sorted(hooks.keys())` and expect `[0, 1]`; execute `session.set_hooks( 'session-renamed', {0: 'display-message "new"'}, clear_existing=True, )` and expect `Session($...)`; execute `hooks = session.show_hook('session-renamed')` without exception; execute `sorted(hooks.keys())` and expect `[0]`; execute `session.unset_hook('session-renamed')` and expect `Session($...)`; execute `session.unset_hook('after-new-window')` and expect `Session($...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_hooks_py_libtmux_hooks_HooksMixin_set_hooks_795d87908d | 3.3a through 3.7c | planned parity | +| src/libtmux/hooks.py::libtmux.hooks.HooksMixin.show_hook | execute `session.set_hook('session-renamed[0]', 'display-message "test"')` and expect `Session($...)`; execute `hooks = session.show_hook('session-renamed')` without exception; execute `isinstance(hooks, SparseArray)` and expect `True`; execute `sorted(hooks.keys())` and expect `[0]`; execute `session.unset_hook('session-renamed')` and expect `Session($...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_hooks_py_libtmux_hooks_HooksMixin_show_hook_a532650d49 | 3.3a through 3.7c | planned parity | +| src/libtmux/hooks.py::libtmux.hooks.HooksMixin.show_hooks | execute `session.set_hook('session-renamed[0]', 'display-message "test"')` and expect `Session($...)`; execute `hooks = session.show_hooks()` without exception; execute `isinstance(hooks, dict)` and expect `True`; execute `'session-renamed[0]' in hooks` and expect `True`; execute `session.unset_hook('session-renamed')` and expect `Session($...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_hooks_py_libtmux_hooks_HooksMixin_show_hooks_25663db72d | 3.3a through 3.7c | planned parity | | src/libtmux/neo.py::libtmux.neo._best_winlink | execute `from libtmux.neo import _best_winlink` without exception; execute `_best_winlink([{"window_id": "@0", "window_index": "1"}])["window_index"]` and expect `'1'`; execute `_best_winlink([ {"window_id": "@0", "window_index": "1", "window_active": "0"}, {"window_id": "@0", "window_index": "5", "window_active": "1"}, ])["window_index"]` and expect `'5'`; execute `_best_winlink([ {"window_id": "@0", "window_index": "1", "window_active": "0"}, {"window_id": "@0", "window_index": "5", "window_active": "0"}, ])["window_index"]` and expect `'1'`; execute `_best_winlink([ {"window_id": "@0", "window_index": "5", "window_active": "0"}, {"window_id": "@0", "window_index": "1", "window_active": "0"}, ])["window_index"]` and expect `'1'`. | documentation contract test | PythonBehaviorParityTest#src_libtmux_neo_py_libtmux_neo_best_winlink_7afd65aff1 | not tmux-bound | planned parity | -| src/libtmux/neo.py::libtmux.neo._is_target_not_found_error | execute `from libtmux.neo import _is_target_not_found_error` without exception; execute `_is_target_not_found_error("can't find pane: %99")` and expect `True`; execute `_is_target_not_found_error("can't find window: @99")` and expect `True`; execute `_is_target_not_found_error("can't find session: $99")` and expect `True`; execute `_is_target_not_found_error("no server running on /tmp/tmux-1000/default")` and expect `False`; execute `_is_target_not_found_error( "error connecting to /tmp/tmux-1000/nope (No such file or directory)" )` and expect `False`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_neo_py_libtmux_neo_is_target_not_found_error_8297e6b672 | 3.2a through 3.7b | planned parity | +| src/libtmux/neo.py::libtmux.neo._is_target_not_found_error | execute `from libtmux.neo import _is_target_not_found_error` without exception; execute `_is_target_not_found_error("can't find pane: %99")` and expect `True`; execute `_is_target_not_found_error("can't find window: @99")` and expect `True`; execute `_is_target_not_found_error("can't find session: $99")` and expect `True`; execute `_is_target_not_found_error("no server running on /tmp/tmux-1000/default")` and expect `False`; execute `_is_target_not_found_error( "error connecting to /tmp/tmux-1000/nope (No such file or directory)" )` and expect `False`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_neo_py_libtmux_neo_is_target_not_found_error_8297e6b672 | 3.3a through 3.7c | planned parity | | src/libtmux/neo.py::libtmux.neo._normalize_tmux_version | execute `from libtmux.neo import _normalize_tmux_version` without exception; execute `_normalize_tmux_version("3.6a") < _normalize_tmux_version("master")` and expect `True`; execute `_normalize_tmux_version("3.2a") < _normalize_tmux_version("3.6a")` and expect `True`. | documentation contract test | PythonBehaviorParityTest#src_libtmux_neo_py_libtmux_neo_normalize_tmux_version_b739a9d4d2 | not tmux-bound | planned parity | | src/libtmux/neo.py::libtmux.neo._token_scope | execute `from libtmux.neo import _token_scope` without exception; execute `_token_scope("pane_id")` and expect `'pane'`; execute `_token_scope("window_zoomed_flag")` and expect `'window'`; execute `_token_scope("client_name")` and expect `'client'`; execute `_token_scope("version")` and expect `'universal'`; execute `_token_scope("mouse_x")` and expect `'event'`; execute `_token_scope("mouse_all_flag")` and expect `'pane'`; execute `_token_scope("active_window_index")` and expect `'session'`; execute `_token_scope("command_list_alias")` and expect `'context'`; execute `_token_scope("search_match")` and expect `'context'`; execute `_token_scope("libtmux_test_nonexistent_token")` and expect `'unknown'`. | documentation contract test | PythonBehaviorParityTest#src_libtmux_neo_py_libtmux_neo_token_scope_daba4038ac | not tmux-bound | planned parity | -| src/libtmux/neo.py::libtmux.neo.fetch_obj | execute `from libtmux.neo import fetch_obj` without exception; execute `fetch_obj( server=pane.server, obj_key="pane_id", obj_id=pane.pane_id, list_cmd="list-panes", list_extra_args=("-t", pane.pane_id), )["pane_id"] == pane.pane_id` and expect `True`; execute `from libtmux import exc` without exception; execute `try: fetch_obj( server=pane.server, obj_key="pane_id", obj_id="%99999", list_cmd="list-panes", list_extra_args=("-t", "%99999"), ) except exc.TmuxObjectDoesNotExist as e: print(e)` and expect `Could not find pane_id=%99999 for list-panes ('-t', '%99999')`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_neo_py_libtmux_neo_fetch_obj_81208b227d | 3.2a through 3.7b | planned parity | -| src/libtmux/neo.py::libtmux.neo.fetch_objs | execute `from libtmux.neo import fetch_objs` without exception; execute `objs = fetch_objs(server=server, list_cmd="list-sessions")` without exception; execute `isinstance(objs, list)` and expect `True`; execute `isinstance(objs[0], dict)` and expect `True`; execute `'session_id' in objs[0]` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_neo_py_libtmux_neo_fetch_objs_5008431037 | 3.2a through 3.7b | planned parity | -| src/libtmux/neo.py::libtmux.neo.get_output_format | execute `from libtmux.neo import get_output_format` without exception; execute `fields, fmt = get_output_format("list-sessions", "3.6a")` without exception; execute `'session_id' in fields` and expect `True`; execute `'pane_id' in fields # active pane for the listed session` and expect `True`; execute `'client_name' in fields # upward not allowed` and expect `False`; execute `'server' in fields` and expect `False`; execute `fields, _ = get_output_format("list-panes", "3.6a")` without exception; execute `all(t in fields for t in ('pane_id', 'window_id', 'session_id'))` and expect `True`; execute `fields, _ = get_output_format("list-clients", "3.6a")` without exception; execute `'client_name' in fields` and expect `True`; execute `'pane_id' in fields` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_neo_py_libtmux_neo_get_output_format_58df0c80be | 3.2a through 3.7b | planned parity | +| src/libtmux/neo.py::libtmux.neo.fetch_obj | execute `from libtmux.neo import fetch_obj` without exception; execute `fetch_obj( server=pane.server, obj_key="pane_id", obj_id=pane.pane_id, list_cmd="list-panes", list_extra_args=("-t", pane.pane_id), )["pane_id"] == pane.pane_id` and expect `True`; execute `from libtmux import exc` without exception; execute `try: fetch_obj( server=pane.server, obj_key="pane_id", obj_id="%99999", list_cmd="list-panes", list_extra_args=("-t", "%99999"), ) except exc.TmuxObjectDoesNotExist as e: print(e)` and expect `Could not find pane_id=%99999 for list-panes ('-t', '%99999')`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_neo_py_libtmux_neo_fetch_obj_81208b227d | 3.3a through 3.7c | planned parity | +| src/libtmux/neo.py::libtmux.neo.fetch_objs | execute `from libtmux.neo import fetch_objs` without exception; execute `objs = fetch_objs(server=server, list_cmd="list-sessions")` without exception; execute `isinstance(objs, list)` and expect `True`; execute `isinstance(objs[0], dict)` and expect `True`; execute `'session_id' in objs[0]` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_neo_py_libtmux_neo_fetch_objs_5008431037 | 3.3a through 3.7c | planned parity | +| src/libtmux/neo.py::libtmux.neo.get_output_format | execute `from libtmux.neo import get_output_format` without exception; execute `fields, fmt = get_output_format("list-sessions", "3.6a")` without exception; execute `'session_id' in fields` and expect `True`; execute `'pane_id' in fields # active pane for the listed session` and expect `True`; execute `'client_name' in fields # upward not allowed` and expect `False`; execute `'server' in fields` and expect `False`; execute `fields, _ = get_output_format("list-panes", "3.6a")` without exception; execute `all(t in fields for t in ('pane_id', 'window_id', 'session_id'))` and expect `True`; execute `fields, _ = get_output_format("list-clients", "3.6a")` without exception; execute `'client_name' in fields` and expect `True`; execute `'pane_id' in fields` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_neo_py_libtmux_neo_get_output_format_58df0c80be | 3.3a through 3.7c | planned parity | | src/libtmux/neo.py::libtmux.neo.parse_output | execute `from libtmux.neo import get_output_format, parse_output` without exception; execute `from libtmux.formats import FORMAT_SEPARATOR` without exception; execute `fields, fmt = get_output_format("list-sessions", "3.6a")` without exception; execute `values = [''] * len(fields)` without exception; execute `values[fields.index('session_id')] = '$1'` without exception; execute `result = parse_output( FORMAT_SEPARATOR.join(values) + FORMAT_SEPARATOR, list_cmd="list-sessions", tmux_version="3.6a", )` without exception; execute `result['session_id']` and expect `'$1'`; execute `'pane_id' in result` and expect `False`. | documentation contract test | PythonBehaviorParityTest#src_libtmux_neo_py_libtmux_neo_parse_output_4e6a4ce1c7 | not tmux-bound | planned parity | -| src/libtmux/options.py::libtmux.options.OptionsMixin._show_option | execute `import typing as t` without exception; execute `from libtmux.common import tmux_cmd` without exception; execute `from libtmux.constants import OptionScope` without exception; execute `class MyServer(OptionsMixin): socket_name = server.socket_name def cmd(self, cmd: str, *args: object): cmd_args: t.List[t.Union[str, int]] = [cmd] if self.socket_name: cmd_args.insert(0, f"-L{self.socket_name}") cmd_args.insert(0, "-f/dev/null") return tmux_cmd(*cmd_args, *args) default_option_scope = OptionScope.Server` without exception; execute `MyServer().cmd('new-session', '-d')` and expect `<libtmux.common.tmux_cmd object at ...>`; execute `MyServer()._show_option('exit-unattached', global_=True)` and expect `False`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_options_py_libtmux_options_OptionsMixin_show_option_e815e7192a | 3.2a through 3.7b | planned parity | -| src/libtmux/options.py::libtmux.options.OptionsMixin._show_option_raw | execute `import typing as t` without exception; execute `from libtmux.common import tmux_cmd` without exception; execute `from libtmux.constants import OptionScope` without exception; execute `class MyServer(OptionsMixin): socket_name = server.socket_name def cmd(self, cmd: str, *args: object): cmd_args: t.List[t.Union[str, int]] = [cmd] if self.socket_name: cmd_args.insert(0, f"-L{self.socket_name}") cmd_args.insert(0, "-f/dev/null") return tmux_cmd(*cmd_args, *args) default_option_scope = OptionScope.Server` without exception; execute `MyServer().cmd('new-session', '-d')` and expect `<libtmux.common.tmux_cmd object at ...>`; execute `MyServer()._show_option_raw('exit-unattached', global_=True)` and expect `<libtmux.common.tmux_cmd object at ...>`; execute `MyServer()._show_option_raw('exit-unattached', global_=True).stdout` and expect `['exit-unattached off']`; execute `isinstance(MyServer()._show_option_raw('exit-unattached', global_=True).stdout, list)` and expect `True`; execute `isinstance(MyServer()._show_option_raw('exit-unattached', global_=True).stdout[0], str)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_options_py_libtmux_options_OptionsMixin_show_option_raw_3a88b64fb9 | 3.2a through 3.7b | planned parity | -| src/libtmux/options.py::libtmux.options.OptionsMixin._show_options | execute `import typing as t` without exception; execute `from libtmux.common import tmux_cmd` without exception; execute `from libtmux.constants import OptionScope` without exception; execute `class MyServer(OptionsMixin): socket_name = server.socket_name def cmd(self, cmd: str, *args: object): cmd_args: t.List[t.Union[str, int]] = [cmd] if self.socket_name: cmd_args.insert(0, f"-L{self.socket_name}") cmd_args.insert(0, "-f/dev/null") return tmux_cmd(*cmd_args, *args) default_option_scope = OptionScope.Server` without exception; execute `MyServer()._show_options()` and expect `{...}`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_options_py_libtmux_options_OptionsMixin_show_options_5f74522ece | 3.2a through 3.7b | planned parity | -| src/libtmux/options.py::libtmux.options.OptionsMixin._show_options_dict | execute `import typing as t` without exception; execute `from libtmux.common import tmux_cmd` without exception; execute `from libtmux.constants import OptionScope` without exception; execute `class MyServer(OptionsMixin): socket_name = server.socket_name def cmd(self, cmd: str, *args: object): cmd_args: t.List[t.Union[str, int]] = [cmd] if self.socket_name: cmd_args.insert(0, f"-L{self.socket_name}") cmd_args.insert(0, "-f/dev/null") return tmux_cmd(*cmd_args, *args) default_option_scope = OptionScope.Server` without exception; execute `MyServer()._show_options_dict()` and expect `{...}`; execute `isinstance(MyServer()._show_options_dict(), dict)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_options_py_libtmux_options_OptionsMixin_show_options_dict_66d4563e30 | 3.2a through 3.7b | planned parity | -| src/libtmux/options.py::libtmux.options.OptionsMixin._show_options_raw | execute `import typing as t` without exception; execute `from libtmux.common import tmux_cmd` without exception; execute `from libtmux.constants import OptionScope` without exception; execute `class MyServer(OptionsMixin): socket_name = server.socket_name def cmd(self, cmd: str, *args: object): cmd_args: t.List[t.Union[str, int]] = [cmd] if self.socket_name: cmd_args.insert(0, f"-L{self.socket_name}") cmd_args.insert(0, "-f/dev/null") return tmux_cmd(*cmd_args, *args) default_option_scope = OptionScope.Server` without exception; execute `MyServer()._show_options_raw()` and expect `<libtmux.common.tmux_cmd object at ...>`; execute `MyServer()._show_options_raw().stdout` and expect `[...]`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_options_py_libtmux_options_OptionsMixin_show_options_raw_a787e92168 | 3.2a through 3.7b | planned parity | -| src/libtmux/options.py::libtmux.options.OptionsMixin.set_option | execute `import typing as t` without exception; execute `from libtmux.common import tmux_cmd` without exception; execute `from libtmux.constants import OptionScope` without exception; execute `from libtmux._internal.sparse_array import SparseArray` without exception; execute `class MyServer(OptionsMixin): socket_name = server.socket_name def cmd(self, cmd: str, *args: object): cmd_args: t.List[t.Union[str, int]] = [cmd] if self.socket_name: cmd_args.insert(0, f"-L{self.socket_name}") cmd_args.insert(0, "-f/dev/null") return tmux_cmd(*cmd_args, *args) default_option_scope = OptionScope.Server` without exception; execute `MyServer().set_option('escape-time', 1250)` and expect `<libtmux.options.MyServer object at ...>`; execute `MyServer()._show_option('escape-time')` and expect `1250`; execute `MyServer().set_option('escape-time', 495)` and expect `<libtmux.options.MyServer object at ...>`; execute `MyServer()._show_option('escape-time')` and expect `495`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_options_py_libtmux_options_OptionsMixin_set_option_51585c6225 | 3.2a through 3.7b | planned parity | -| src/libtmux/options.py::libtmux.options.OptionsMixin.show_option | execute `import typing as t` without exception; execute `from libtmux.common import tmux_cmd` without exception; execute `from libtmux.constants import OptionScope` without exception; execute `class MyServer(OptionsMixin): socket_name = server.socket_name def cmd(self, cmd: str, *args: object): cmd_args: t.List[t.Union[str, int]] = [cmd] if self.socket_name: cmd_args.insert(0, f"-L{self.socket_name}") cmd_args.insert(0, "-f/dev/null") return tmux_cmd(*cmd_args, *args) default_option_scope = OptionScope.Server` without exception; execute `MyServer().cmd('new-session', '-d')` and expect `<libtmux.common.tmux_cmd object at ...>`; execute `MyServer().show_option('exit-unattached', global_=True)` and expect `False`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_options_py_libtmux_options_OptionsMixin_show_option_a0663b7be1 | 3.2a through 3.7b | planned parity | -| src/libtmux/options.py::libtmux.options.OptionsMixin.show_options | execute `options = server.show_options()` without exception; execute `isinstance(options, dict)` and expect `True`; execute `'buffer-limit' in options` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_options_py_libtmux_options_OptionsMixin_show_options_6c26c4ef55 | 3.2a through 3.7b | planned parity | -| src/libtmux/options.py::libtmux.options.OptionsMixin.unset_option | execute `import typing as t` without exception; execute `from libtmux.common import tmux_cmd` without exception; execute `from libtmux.constants import OptionScope` without exception; execute `class MyServer(OptionsMixin): socket_name = server.socket_name def cmd(self, cmd: str, *args: object): cmd_args: t.List[t.Union[str, int]] = [cmd] if self.socket_name: cmd_args.insert(0, f"-L{self.socket_name}") cmd_args.insert(0, "-f/dev/null") return tmux_cmd(*cmd_args, *args) default_option_scope = OptionScope.Server` without exception; execute `MyServer().set_option('escape-time', 1250)` and expect `<libtmux.options.MyServer object at ...>`; execute `MyServer()._show_option('escape-time')` and expect `1250`; execute `MyServer().unset_option('escape-time')` and expect `<libtmux.options.MyServer object at ...>`; execute `isinstance(MyServer()._show_option('escape-time'), int)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_options_py_libtmux_options_OptionsMixin_unset_option_19d04a4d69 | 3.2a through 3.7b | planned parity | +| src/libtmux/options.py::libtmux.options.OptionsMixin._show_option | execute `import typing as t` without exception; execute `from libtmux.common import tmux_cmd` without exception; execute `from libtmux.constants import OptionScope` without exception; execute `class MyServer(OptionsMixin): socket_name = server.socket_name def cmd(self, cmd: str, *args: object): cmd_args: t.List[t.Union[str, int]] = [cmd] if self.socket_name: cmd_args.insert(0, f"-L{self.socket_name}") cmd_args.insert(0, "-f/dev/null") return tmux_cmd(*cmd_args, *args) default_option_scope = OptionScope.Server` without exception; execute `MyServer().cmd('new-session', '-d')` and expect `<libtmux.common.tmux_cmd object at ...>`; execute `MyServer()._show_option('exit-unattached', global_=True)` and expect `False`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_options_py_libtmux_options_OptionsMixin_show_option_e815e7192a | 3.3a through 3.7c | planned parity | +| src/libtmux/options.py::libtmux.options.OptionsMixin._show_option_raw | execute `import typing as t` without exception; execute `from libtmux.common import tmux_cmd` without exception; execute `from libtmux.constants import OptionScope` without exception; execute `class MyServer(OptionsMixin): socket_name = server.socket_name def cmd(self, cmd: str, *args: object): cmd_args: t.List[t.Union[str, int]] = [cmd] if self.socket_name: cmd_args.insert(0, f"-L{self.socket_name}") cmd_args.insert(0, "-f/dev/null") return tmux_cmd(*cmd_args, *args) default_option_scope = OptionScope.Server` without exception; execute `MyServer().cmd('new-session', '-d')` and expect `<libtmux.common.tmux_cmd object at ...>`; execute `MyServer()._show_option_raw('exit-unattached', global_=True)` and expect `<libtmux.common.tmux_cmd object at ...>`; execute `MyServer()._show_option_raw('exit-unattached', global_=True).stdout` and expect `['exit-unattached off']`; execute `isinstance(MyServer()._show_option_raw('exit-unattached', global_=True).stdout, list)` and expect `True`; execute `isinstance(MyServer()._show_option_raw('exit-unattached', global_=True).stdout[0], str)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_options_py_libtmux_options_OptionsMixin_show_option_raw_3a88b64fb9 | 3.3a through 3.7c | planned parity | +| src/libtmux/options.py::libtmux.options.OptionsMixin._show_options | execute `import typing as t` without exception; execute `from libtmux.common import tmux_cmd` without exception; execute `from libtmux.constants import OptionScope` without exception; execute `class MyServer(OptionsMixin): socket_name = server.socket_name def cmd(self, cmd: str, *args: object): cmd_args: t.List[t.Union[str, int]] = [cmd] if self.socket_name: cmd_args.insert(0, f"-L{self.socket_name}") cmd_args.insert(0, "-f/dev/null") return tmux_cmd(*cmd_args, *args) default_option_scope = OptionScope.Server` without exception; execute `MyServer()._show_options()` and expect `{...}`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_options_py_libtmux_options_OptionsMixin_show_options_5f74522ece | 3.3a through 3.7c | planned parity | +| src/libtmux/options.py::libtmux.options.OptionsMixin._show_options_dict | execute `import typing as t` without exception; execute `from libtmux.common import tmux_cmd` without exception; execute `from libtmux.constants import OptionScope` without exception; execute `class MyServer(OptionsMixin): socket_name = server.socket_name def cmd(self, cmd: str, *args: object): cmd_args: t.List[t.Union[str, int]] = [cmd] if self.socket_name: cmd_args.insert(0, f"-L{self.socket_name}") cmd_args.insert(0, "-f/dev/null") return tmux_cmd(*cmd_args, *args) default_option_scope = OptionScope.Server` without exception; execute `MyServer()._show_options_dict()` and expect `{...}`; execute `isinstance(MyServer()._show_options_dict(), dict)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_options_py_libtmux_options_OptionsMixin_show_options_dict_66d4563e30 | 3.3a through 3.7c | planned parity | +| src/libtmux/options.py::libtmux.options.OptionsMixin._show_options_raw | execute `import typing as t` without exception; execute `from libtmux.common import tmux_cmd` without exception; execute `from libtmux.constants import OptionScope` without exception; execute `class MyServer(OptionsMixin): socket_name = server.socket_name def cmd(self, cmd: str, *args: object): cmd_args: t.List[t.Union[str, int]] = [cmd] if self.socket_name: cmd_args.insert(0, f"-L{self.socket_name}") cmd_args.insert(0, "-f/dev/null") return tmux_cmd(*cmd_args, *args) default_option_scope = OptionScope.Server` without exception; execute `MyServer()._show_options_raw()` and expect `<libtmux.common.tmux_cmd object at ...>`; execute `MyServer()._show_options_raw().stdout` and expect `[...]`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_options_py_libtmux_options_OptionsMixin_show_options_raw_a787e92168 | 3.3a through 3.7c | planned parity | +| src/libtmux/options.py::libtmux.options.OptionsMixin.set_option | execute `import typing as t` without exception; execute `from libtmux.common import tmux_cmd` without exception; execute `from libtmux.constants import OptionScope` without exception; execute `from libtmux._internal.sparse_array import SparseArray` without exception; execute `class MyServer(OptionsMixin): socket_name = server.socket_name def cmd(self, cmd: str, *args: object): cmd_args: t.List[t.Union[str, int]] = [cmd] if self.socket_name: cmd_args.insert(0, f"-L{self.socket_name}") cmd_args.insert(0, "-f/dev/null") return tmux_cmd(*cmd_args, *args) default_option_scope = OptionScope.Server` without exception; execute `MyServer().set_option('escape-time', 1250)` and expect `<libtmux.options.MyServer object at ...>`; execute `MyServer()._show_option('escape-time')` and expect `1250`; execute `MyServer().set_option('escape-time', 495)` and expect `<libtmux.options.MyServer object at ...>`; execute `MyServer()._show_option('escape-time')` and expect `495`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_options_py_libtmux_options_OptionsMixin_set_option_51585c6225 | 3.3a through 3.7c | planned parity | +| src/libtmux/options.py::libtmux.options.OptionsMixin.show_option | execute `import typing as t` without exception; execute `from libtmux.common import tmux_cmd` without exception; execute `from libtmux.constants import OptionScope` without exception; execute `class MyServer(OptionsMixin): socket_name = server.socket_name def cmd(self, cmd: str, *args: object): cmd_args: t.List[t.Union[str, int]] = [cmd] if self.socket_name: cmd_args.insert(0, f"-L{self.socket_name}") cmd_args.insert(0, "-f/dev/null") return tmux_cmd(*cmd_args, *args) default_option_scope = OptionScope.Server` without exception; execute `MyServer().cmd('new-session', '-d')` and expect `<libtmux.common.tmux_cmd object at ...>`; execute `MyServer().show_option('exit-unattached', global_=True)` and expect `False`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_options_py_libtmux_options_OptionsMixin_show_option_a0663b7be1 | 3.3a through 3.7c | planned parity | +| src/libtmux/options.py::libtmux.options.OptionsMixin.show_options | execute `options = server.show_options()` without exception; execute `isinstance(options, dict)` and expect `True`; execute `'buffer-limit' in options` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_options_py_libtmux_options_OptionsMixin_show_options_6c26c4ef55 | 3.3a through 3.7c | planned parity | +| src/libtmux/options.py::libtmux.options.OptionsMixin.unset_option | execute `import typing as t` without exception; execute `from libtmux.common import tmux_cmd` without exception; execute `from libtmux.constants import OptionScope` without exception; execute `class MyServer(OptionsMixin): socket_name = server.socket_name def cmd(self, cmd: str, *args: object): cmd_args: t.List[t.Union[str, int]] = [cmd] if self.socket_name: cmd_args.insert(0, f"-L{self.socket_name}") cmd_args.insert(0, "-f/dev/null") return tmux_cmd(*cmd_args, *args) default_option_scope = OptionScope.Server` without exception; execute `MyServer().set_option('escape-time', 1250)` and expect `<libtmux.options.MyServer object at ...>`; execute `MyServer()._show_option('escape-time')` and expect `1250`; execute `MyServer().unset_option('escape-time')` and expect `<libtmux.options.MyServer object at ...>`; execute `isinstance(MyServer()._show_option('escape-time'), int)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_options_py_libtmux_options_OptionsMixin_unset_option_19d04a4d69 | 3.3a through 3.7c | planned parity | | src/libtmux/options.py::libtmux.options.convert_value | execute `convert_value("on")` and expect `True`; execute `convert_value("off")` and expect `False`; execute `convert_value("1")` and expect `1`; execute `convert_value("50")` and expect `50`; execute `convert_value("%50")` and expect `'%50'`. | documentation contract test | PythonBehaviorParityTest#src_libtmux_options_py_libtmux_options_convert_value_81c610b6db | not tmux-bound | planned parity | | src/libtmux/options.py::libtmux.options.convert_values | execute `convert_values(None)` without exception; execute `convert_values("on")` and expect `True`; execute `convert_values("off")` and expect `False`; execute `convert_values(["on"])` and expect `[True]`; execute `convert_values(["off"])` and expect `[False]`; execute `convert_values({"window_index": "1"})` and expect `{'window_index': 1}`; execute `convert_values({"visual-bell": "on"})` and expect `{'visual-bell': True}`. | documentation contract test | PythonBehaviorParityTest#src_libtmux_options_py_libtmux_options_convert_values_fb76ac2ac2 | not tmux-bound | planned parity | -| src/libtmux/options.py::libtmux.options.explode_arrays | execute `import io` without exception; execute `many_more_options = io.StringIO(r''' terminal-features[0] xterm*:clipboard:ccolour:cstyle:focus terminal-features[1] screen*:title ''')` without exception; execute `many_more_flat_dict = parse_options_to_dict(many_more_options)` without exception; execute `many_more_flat_dict == { "terminal-features[0]": "xterm*:clipboard:ccolour:cstyle:focus", "terminal-features[1]": "screen*:title",}` and expect `True`; execute `explode_arrays(many_more_flat_dict) == { "terminal-features": {0: "xterm*:clipboard:ccolour:cstyle:focus", 1: "screen*:title"}}` and expect `True`; execute `explode_arrays(parse_options_to_dict(io.StringIO(r''' terminal-features[0] xterm*:clipboard:ccolour:cstyle:focus terminal-features[5] screen*:title '''))) == { "terminal-features": {0: "xterm*:clipboard:ccolour:cstyle:focus", 5: "screen*:title"}}` and expect `True`; execute `from libtmux._internal.sparse_array import SparseArray` without exception; execute `hooks_output = io.StringIO(r''' session-renamed[0] display-message 'renamed' session-renamed[5] refresh-client pane-focus-in[0] run-shell 'echo focus' ''')` without exception; execute `hooks_exploded = explode_arrays( parse_options_to_dict(hooks_output), force_array=True, )` without exception; execute `isinstance(hooks_exploded["session-renamed"], SparseArray)` and expect `True`; execute `hooks_exploded["session-renamed"][0]` and expect `"display-message 'renamed'"`; execute `hooks_exploded["session-renamed"][5]` and expect `'refresh-client'`; execute `sorted(hooks_exploded["session-renamed"].keys())` and expect `[0, 5]`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_options_py_libtmux_options_explode_arrays_1e4dfcd9bd | 3.2a through 3.7b | planned parity | -| src/libtmux/options.py::libtmux.options.explode_complex | execute `import io` without exception; execute `explode_complex(explode_arrays(parse_options_to_dict(io.StringIO(r''' terminal-features[0] xterm*:clipboard:ccolour:cstyle:focus terminal-features[5] screen*:title '''))))` and expect `{'terminal-features': {'xterm*': ['clipboard', 'ccolour', 'cstyle', 'focus'], 'screen*': ['title']}}`; execute `explode_complex(explode_arrays(parse_options_to_dict(io.StringIO(r''' terminal-features[0] xterm*:clipboard:ccolour:cstyle:focus terminal-features[5] screen*:title ''')))) == { "terminal-features": {"xterm*": ["clipboard", "ccolour", "cstyle", "focus"], "screen*": ["title"]}}` and expect `True`; execute `explode_complex(explode_arrays(parse_options_to_dict(io.StringIO(r''' command-alias[0] split-pane=split-window command-alias[1] splitp=split-window command-alias[2] "server-info=show-messages -JT" ''')))) == { "command-alias": {"split-pane": "split-window", "splitp": "split-window", "server-info": "show-messages -JT"}}` and expect `True`; execute `explode_complex(explode_arrays({"terminal-features": {0: "xterm*:clipboard:ccolour:cstyle:focus", 1: "screen*:title"}}))` and expect `{'terminal-features': {0: 'xterm*:clipboard:ccolour:cstyle:focus', 1: 'screen*:title'}}`; execute `explode_complex(explode_arrays({"terminal-features": {0: "xterm*:clipboard:ccolour:cstyle:focus", 8: "screen*:title"}})) == SparseArray({'terminal-features': {0: 'xterm*:clipboard:ccolour:cstyle:focus', 8: 'screen*:title'}})` and expect `True`; execute `explode_complex(explode_arrays(parse_options_to_dict(io.StringIO(r''' terminal-overrides[0] xterm-256color:Tc terminal-overrides[1] *:U8=0 ''')))) == { "terminal-overrides": {"xterm-256color": {"Tc": None}, "*": {"U8": 0}}}` and expect `True`; execute `explode_complex(explode_arrays(parse_options_to_dict(io.StringIO(r''' user-keys[100] "\e[test" user-keys[6] "\e\n" user-keys[0] "\e[5;30012~" ''')))) == { "user-keys": {0: "\\e[5;30012~", 6: "\\e\\n", 100: "\\e[test"}}` and expect `True`; execute `explode_complex(explode_arrays(parse_options_to_dict(io.StringIO(r''' status-format[0] "#[align=left range=left #{E:status-left-style}]#[push-default]#{T;=/#{status-left-length}:status-left}#[pop-default]#[norange default]#[list=on align=#{status-justify}]#[list=left-marker]<#[list=right-marker]>#[list=on]#{W:#[range=window|#{window_index} #{E:window-status-style}#{?#{&&:#{window_last_flag},#{!=:#{E:window-status-last-style},default}}, #{E:window-status-last-style},}#{?#{&&:#{window_bell_flag},#{!=:#{E:window-status-bell-style},default}}, #{E:window-status-bell-style},#{?#{&&:#{||:#{window_activity_flag},#{window_silence_flag}},#{!=:#{E:window-status-activity-style},default}}, #{E:window-status-activity-style},}}]#[push-default]#{T:window-status-format}#[pop-default]#[norange default]#{?window_end_flag,,#{window-status-separator}},#[range=window|#{window_index} list=focus #{?#{!=:#{E:window-status-current-style},default},#{E:window-status-current-style},#{E:window-status-style}}#{?#{&&:#{window_last_flag},#{!=:#{E:window-status-last-style},default}}, #{E:window-status-last-style},}#{?#{&&:#{window_bell_flag},#{!=:#{E:window-status-bell-style},default}}, #{E:window-status-bell-style},#{?#{&&:#{||:#{window_activity_flag},#{window_silence_flag}},#{!=:#{E:window-status-activity-style},default}}, #{E:window-status-activity-style},}}]#[push-default]#{T:window-status-current-format}#[pop-default]#[norange list=on default]#{?window_end_flag,,#{window-status-separator}}}#[nolist align=right range=right #{E:status-right-style}]#[push-default]#{T;=/#{status-right-length}:status-right}#[pop-default]#[norange default]" status-format[1] "#[align=centre]#{P:#{?pane_active,#[reverse],}#{pane_index}[#{pane_width}x#{pane_height}]#[default] }" ''')))) == { "status-format": {0: "#[align=left range=left #{E:status-left-style}]#[push-default]#{T;=/#{status-left-length}:status-left}#[pop-default]#[norange default]#[list=on align=#{status-justify}]#[list=left-marker]<#[list=right-marker]>#[list=on]#{W:#[range=window|#{window_index} #{E:window-status-style}#{?#{&&:#{window_last_flag},#{!=:#{E:window-status-last-style},default}}, #{E:window-status-last-style},}#{?#{&&:#{window_bell_flag},#{!=:#{E:window-status-bell-style},default}}, #{E:window-status-bell-style},#{?#{&&:#{||:#{window_activity_flag},#{window_silence_flag}},#{!=:#{E:window-status-activity-style},default}}, #{E:window-status-activity-style},}}]#[push-default]#{T:window-status-format}#[pop-default]#[norange default]#{?window_end_flag,,#{window-status-separator}},#[range=window|#{window_index} list=focus #{?#{!=:#{E:window-status-current-style},default},#{E:window-status-current-style},#{E:window-status-style}}#{?#{&&:#{window_last_flag},#{!=:#{E:window-status-last-style},default}}, #{E:window-status-last-style},}#{?#{&&:#{window_bell_flag},#{!=:#{E:window-status-bell-style},default}}, #{E:window-status-bell-style},#{?#{&&:#{||:#{window_activity_flag},#{window_silence_flag}},#{!=:#{E:window-status-activity-style},default}}, #{E:window-status-activity-style},}}]#[push-default]#{T:window-status-current-format}#[pop-default]#[norange list=on default]#{?window_end_flag,,#{window-status-separator}}}#[nolist align=right range=right #{E:status-right-style}]#[push-default]#{T;=/#{status-right-length}:status-right}#[pop-default]#[norange default]", 1: "#[align=centre]#{P:#{?pane_active,#[reverse],}#{pane_index}[#{pane_width}x#{pane_height}]#[default] }", }}` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_options_py_libtmux_options_explode_complex_74022ddf0a | 3.2a through 3.7b | planned parity | -| src/libtmux/options.py::libtmux.options.handle_option_error | execute `result = server.cmd( 'set-option', 'unknown-option-name', )` without exception; execute `bool(isinstance(result.stderr, list) and len(result.stderr))` and expect `True`; execute `import pytest` without exception; execute `from libtmux import exc` without exception; execute `with pytest.raises(exc.OptionError): handle_option_error(result.stderr[0])` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_options_py_libtmux_options_handle_option_error_be7cdd74c1 | 3.2a through 3.7b | planned parity | -| src/libtmux/options.py::libtmux.options.parse_options_to_dict | execute `import io` without exception; execute `raw_options = io.StringIO("status-keys vi")` without exception; execute `parse_options_to_dict(raw_options) == {"status-keys": "vi"}` and expect `True`; execute `int_options = io.StringIO("message-limit 50")` without exception; execute `parse_options_to_dict(int_options) == {"message-limit": "50"}` and expect `True`; execute `empty_option = io.StringIO("user-keys")` without exception; execute `parse_options_to_dict(empty_option) == {"user-keys": None}` and expect `True`; execute `array_option = io.StringIO("command-alias[0] split-pane=split-window")` without exception; execute `parse_options_to_dict(array_option) == { "command-alias[0]": "split-pane=split-window"}` and expect `True`; execute `array_option = io.StringIO("command-alias[40] split-pane=split-window")` without exception; execute `parse_options_to_dict(array_option) == { "command-alias[40]": "split-pane=split-window"}` and expect `True`; execute `many_options = io.StringIO(r'''status-keys command-alias[0] split-pane=split-window ''')` without exception; execute `parse_options_to_dict(many_options) == { "command-alias[0]": "split-pane=split-window", "status-keys": None,}` and expect `True`; execute `many_more_options = io.StringIO(r''' terminal-features[0] xterm*:clipboard:ccolour:cstyle:focus terminal-features[1] screen*:title ''')` without exception; execute `parse_options_to_dict(many_more_options) == { "terminal-features[0]": "xterm*:clipboard:ccolour:cstyle:focus", "terminal-features[1]": "screen*:title",}` and expect `True`; execute `quoted_option = io.StringIO(r''' command-alias[0] "choose-session=choose-tree -s" ''')` without exception; execute `parse_options_to_dict(quoted_option) == { "command-alias[0]": "choose-session=choose-tree -s", }` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_options_py_libtmux_options_parse_options_to_dict_c132265e66 | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane | execute `pane` and expect `Pane(%1 Window(@1 1:..., Session($1 ...)))`; execute `pane in window.panes` and expect `True`; execute `pane.window` and expect `Window(@1 1:..., Session($1 ...))`; execute `pane.session` and expect `Session($1 ...)`; execute `with window.split() as pane: pane.send_keys('echo "Hello"') # Do work with the pane # Pane will be killed automatically when exiting the context` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_89b5a1a53c | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.at_bottom | execute `pane.pane_at_bottom` and expect `'1'`; execute `pane.at_bottom` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_at_bottom_6ca2a17fef | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.at_left | execute `pane.pane_at_left` and expect `'1'`; execute `pane.at_left` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_at_left_4e48fd214b | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.at_right | execute `pane.pane_at_right` and expect `'1'`; execute `pane.at_right` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_at_right_2dd8e8ca5d | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.at_top | execute `pane.pane_at_top` and expect `'1'`; execute `pane.at_top` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_at_top_968a50a65b | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.break_pane | execute `pane_to_break = window.split(shell='sleep 1m')` without exception; execute `new_window = pane_to_break.break_pane(window_name='broken')` without exception; execute `new_window.window_name` and expect `'broken'`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_break_pane_a7e77fef15 | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.capture_pane | execute `pane = window.split(shell='sh')` without exception; execute `pane.capture_pane()` and expect `['$']`; execute `pane.send_keys('echo "Hello world"', enter=True)` without exception; execute `pane.capture_pane()` and expect `['$ echo "Hello world"', 'Hello world', '$']`; execute `print(chr(10).join(pane.capture_pane()))` and expect `$ echo "Hello world" Hello world $`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_capture_pane_8fed04c38b | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.choose_buffer | execute `pane.choose_buffer()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_choose_buffer_f64d2f76c4 | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.choose_client | execute `pane.choose_client()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_choose_client_b4b49269f0 | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.choose_tree | execute `pane.choose_tree()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_choose_tree_4d42fc71ef | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.clear_history | execute `pane.clear_history()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_clear_history_28d6d8542e | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.clock_mode | execute `pane.clock_mode()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_clock_mode_634705470c | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.cmd | execute `pane.cmd('split-window', '-P').stdout[0]` and expect `'libtmux...:...'`; execute `Pane.from_pane_id(pane_id=pane.cmd( 'split-window', '-P', '-F#{pane_id}').stdout[0], server=pane.server)` and expect `Pane(%... Window(@... ...:..., Session($1 libtmux_...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_cmd_3248ce2f06 | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.copy_mode | execute `pane.copy_mode()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_copy_mode_e7635a3cab | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.customize_mode | execute `pane.customize_mode()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_customize_mode_22625c0e22 | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.display_panes | execute `with control_mode() as ctl: window.active_pane.display_panes()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_display_panes_be2176f2a4 | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.display_popup | execute `with control_mode() as ctl: pane.display_popup(command='true', close_on_exit=True)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_display_popup_ed5880c930 | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.find_window | execute `pane.find_window('sh')` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_find_window_fa049abe56 | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.from_env | execute `socket_path = server.cmd( # doctest: +HIDE "display-message", "-p", "-t", pane.pane_id, "#{socket_path}" ).stdout[0]` with doctest options `+ALLOW_UNICODE` without exception; execute `env = { # doctest: +HIDE "TMUX": f"{socket_path},1,0", "TMUX_PANE": pane.pane_id, }` with doctest options `+ALLOW_UNICODE` without exception; execute `Pane.from_env(env)` and expect `Pane(%1 Window(@1 1:..., Session($1 ...)))`; execute `Pane.from_env(env).pane_id == pane.pane_id` and expect `True`; execute `from libtmux import exc` without exception; execute `try: Pane.from_env({}) except exc.NotInsideTmux as e: print(e)` and expect `Not inside a tmux pane: $TMUX is unset or empty`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_from_env_752ca3f890 | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.from_pane_id | execute `Pane.from_pane_id(server=pane.server, pane_id=pane.pane_id)` and expect `Pane(%1 Window(@1 1:..., Session($1 ...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_from_pane_id_5b9ce3a290 | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.height | execute `pane.height.isdigit()` and expect `True`; execute `pane.height == pane.pane_height` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_height_2fb5ec6d49 | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.id | execute `pane.id` and expect `'%1'`; execute `pane.id == pane.pane_id` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_id_24dc3473ba | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.index | execute `pane.index` and expect `'0'`; execute `pane.index == pane.pane_index` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_index_f37cfd7ca6 | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.join | execute `pane_to_join = window.split(shell='sleep 1m')` without exception; execute `new_window = pane_to_join.break_pane()` without exception; execute `pane_to_join.join(window)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_join_308a926493 | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.kill | execute `pane_1 = pane.split()` without exception; execute `pane_1 in window.panes` and expect `True`; execute `pane_1.kill()` without exception; execute `pane_1 not in window.panes` and expect `True`; execute `pane.window.resize(height=100, width=100)` and expect `Window(@1 1...)`; execute `one_pane_to_rule_them_all = pane.split()` without exception; execute `other_panes = pane.split( ), pane.split()` without exception; execute `all([p in window.panes for p in other_panes])` and expect `True`; execute `one_pane_to_rule_them_all.kill(all_except=True)` without exception; execute `all([p not in window.panes for p in other_panes])` and expect `True`; execute `one_pane_to_rule_them_all in window.panes` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_kill_e25c6299dc | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.move | execute `pane_to_move = window.split(shell='sleep 1m')` without exception; execute `w2 = session.new_window(window_name='move_target')` without exception; execute `pane_to_move.move(w2)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_move_b01c6d05c7 | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.new_pane | execute `from libtmux.common import has_gte_version` without exception; execute `if has_gte_version("3.7"): floating = pane.new_pane(width=40, height=10, shell="sleep 5") is_floating = floating.pane_floating_flag else: is_floating = "1"` without exception; execute `is_floating` and expect `'1'`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_new_pane_11b0985861 | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.paste_buffer | execute `server.set_buffer('pasted_text')` without exception; execute `pane.paste_buffer()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_paste_buffer_3b3c32fdc0 | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.pipe | execute `pane.pipe('cat >> /tmp/output.txt')` without exception; execute `pane.pipe()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_pipe_af350fb3ca | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.refresh | execute `pane.refresh()` without exception; execute `pane.pane_id` and expect `'%1'`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_refresh_b2091c58de | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.reset | execute `pane.reset()` and expect `Pane(%... Window(@... ...:..., Session($1 libtmux_...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_reset_9e06d19ed4 | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.respawn | execute `pane = window.split(shell='sleep 1m')` without exception; execute `pane.respawn(kill=True, shell='sh')` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_respawn_b7ddfec773 | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.select | execute `pane = window.active_pane` without exception; execute `new_pane = window.split()` without exception; execute `pane.refresh()` without exception; execute `active_panes = [p for p in window.panes if p.pane_active == '1']` without exception; execute `pane in active_panes` and expect `True`; execute `new_pane in active_panes` and expect `False`; execute `new_pane.pane_active == '1'` and expect `False`; execute `new_pane.select()` and expect `Pane(...)`; execute `new_pane.pane_active == '1'` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_select_02aaa45270 | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.send_keys | execute `pane = window.split(shell='sh')` without exception; execute `pane.capture_pane()` and expect `['$']`; execute `pane.send_keys('echo "Hello world"', enter=True)` without exception; execute `pane.capture_pane()` and expect `['$ echo "Hello world"', 'Hello world', '$']`; execute `print('\n'.join(pane.capture_pane())) # doctest: +NORMALIZE_WHITESPACE` with doctest options `+NORMALIZE_WHITESPACE` and expect `$ echo "Hello world" Hello world $`; execute `pane.send_keys(reset=True)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_send_keys_bcca63c4d0 | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.send_prefix | execute `pane.send_prefix()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_send_prefix_75166732ce | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.set_title | execute `pane.set_title('my-title')` and expect `Pane(...)`; execute `pane.pane_title` and expect `'my-title'`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_set_title_3b3cf2e8a5 | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.split | execute `(pane.at_left, pane.at_right, pane.at_top, pane.at_bottom)` and expect `(True, True, True, True)`; execute `new_pane = pane.split()` without exception; execute `(new_pane.at_left, new_pane.at_right, new_pane.at_top, new_pane.at_bottom)` and expect `(True, True, False, True)`; execute `right_pane = pane.split(direction=PaneDirection.Right)` without exception; execute `(right_pane.at_left, right_pane.at_right, right_pane.at_top, right_pane.at_bottom)` and expect `(False, True, True, False)`; execute `left_pane = pane.split(direction=PaneDirection.Left)` without exception; execute `(left_pane.at_left, left_pane.at_right, left_pane.at_top, left_pane.at_bottom)` and expect `(True, False, True, False)`; execute `top_pane = pane.split(direction=PaneDirection.Above)` without exception; execute `(top_pane.at_left, top_pane.at_right, top_pane.at_top, top_pane.at_bottom)` and expect `(False, False, True, False)`; execute `pane = session.new_window().active_pane` without exception; execute `top_pane = pane.split(direction=PaneDirection.Above, full_window_split=True)` without exception; execute `(top_pane.at_left, top_pane.at_right, top_pane.at_top, top_pane.at_bottom)` and expect `(True, True, True, False)`; execute `bottom_pane = pane.split( direction=PaneDirection.Below, full_window_split=True)` without exception; execute `(bottom_pane.at_left, bottom_pane.at_right, bottom_pane.at_top, bottom_pane.at_bottom)` and expect `(True, True, False, True)`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_split_15e115b719 | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.swap | execute `pane1 = window.active_pane` without exception; execute `pane2 = window.split()` without exception; execute `pane1_id, pane2_id = pane1.pane_id, pane2.pane_id` without exception; execute `pane1.swap(pane2)` without exception; execute `pane1.refresh()` without exception; execute `pane2.refresh()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_swap_e3427460d0 | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.title | execute `pane.set_title('test-alias')` and expect `Pane(...)`; execute `pane.title == pane.pane_title` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_title_6d895c42c3 | 3.2a through 3.7b | planned parity | -| src/libtmux/pane.py::libtmux.pane.Pane.width | execute `pane.width.isdigit()` and expect `True`; execute `pane.width == pane.pane_width` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_width_0c5eae94ee | 3.2a through 3.7b | planned parity | -| src/libtmux/pytest_plugin.py::libtmux.pytest_plugin.TestServer | execute `server = Server() # Create server instance` without exception; execute `server.new_session()` and expect `Session($... ...)`; execute `server.is_alive()` and expect `True`; execute `server2 = Server()` without exception; execute `server2.socket_name != server.socket_name` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pytest_plugin_py_libtmux_pytest_plugin_TestServer_b769bdd044 | 3.2a through 3.7b | planned parity | -| src/libtmux/pytest_plugin.py::libtmux.pytest_plugin.control_mode | execute `from libtmux._internal.control_mode import ControlMode` without exception; execute `def test_example(control_mode): with control_mode() as ctl: assert ctl.client_name != ''` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pytest_plugin_py_libtmux_pytest_plugin_control_mode_bd791351b1 | 3.2a through 3.7b | planned parity | -| src/libtmux/pytest_plugin.py::libtmux.pytest_plugin.server | execute `from libtmux.server import Server` without exception; execute `def test_example(server: Server) -> None: assert isinstance(server, Server) session = server.new_session('my session') assert len(server.sessions) == 1 assert [session.name.startswith('my') for session in server.sessions]` without exception; execute `locals().keys()` and expect `dict_keys(...)`; execute `source = ''.join([e.source for e in request._pyfuncitem.dtest.examples][:3])` without exception; execute `pytester = request.getfixturevalue('pytester')` without exception; execute `pytester.makepyfile(**{'whatever.py': source})` and expect `PosixPath(...)`; execute `result = pytester.runpytest('whatever.py', '--disable-warnings')` and expect `===...`; execute `result.assert_outcomes(passed=1)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pytest_plugin_py_libtmux_pytest_plugin_server_acc91d3b27 | 3.2a through 3.7b | planned parity | -| src/libtmux/pytest_plugin.py::libtmux.pytest_plugin.session | execute `from libtmux.session import Session` without exception; execute `def test_example(session: "Session") -> None: assert isinstance(session.name, str) assert session.name.startswith('libtmux_') window = session.new_window(window_name='new one') assert window.name == 'new one'` without exception; execute `locals().keys()` and expect `dict_keys(...)`; execute `source = ''.join([e.source for e in request._pyfuncitem.dtest.examples][:3])` without exception; execute `pytester = request.getfixturevalue('pytester')` without exception; execute `pytester.makepyfile(**{'whatever.py': source})` and expect `PosixPath(...)`; execute `result = pytester.runpytest('whatever.py', '--disable-warnings')` and expect `===...`; execute `result.assert_outcomes(passed=1)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pytest_plugin_py_libtmux_pytest_plugin_session_cc63e7ba8b | 3.2a through 3.7b | planned parity | -| src/libtmux/pytest_plugin.py::libtmux.pytest_plugin.session_params | execute `import pytest` without exception; execute `from libtmux.session import Session` without exception; execute `@pytest.fixture def session_params(session_params): return { 'x': 800, 'y': 600, }` without exception; execute `def test_example(session: "Session") -> None: assert isinstance(session.name, str) assert session.name.startswith('libtmux_') window = session.new_window(window_name='new one') assert window.name == 'new one'` without exception; execute `locals().keys()` and expect `dict_keys(...)`; execute `source = ''.join([e.source for e in request._pyfuncitem.dtest.examples][:4])` without exception; execute `pytester = request.getfixturevalue('pytester')` without exception; execute `pytester.makepyfile(**{'whatever.py': source})` and expect `PosixPath(...)`; execute `result = pytester.runpytest('whatever.py', '--disable-warnings')` and expect `===...`; execute `result.assert_outcomes(passed=1)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pytest_plugin_py_libtmux_pytest_plugin_session_params_50f29f53fe | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server | execute `server` and expect `Server(socket_name=libtmux_test...)`; execute `server.sessions` and expect `[Session($1 ...)]`; execute `server.sessions[0].windows` and expect `[Window(@1 1:..., Session($1 ...))]`; execute `server.sessions[0].active_window` and expect `Window(@1 1:..., Session($1 ...))`; execute `server.sessions[0].active_pane` and expect `Pane(%1 Window(@1 1:..., Session($1 ...)))`; execute `with Server() as server: session = server.new_session() # Do work with the session # Server will be killed automatically when exiting the context` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_57e38b41fe | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server.attached_sessions | Given session rows whose session_attached values are '0', '1', and '2', Server.attachedSessions() returns only the '1' and '2' sessions by testing the attached count greater than zero; it never dispatches the Python noeq lookup. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_attached_sessions_96d2258b22 | 3.2a through 3.7b | deliberate Java correction | -| src/libtmux/server.py::libtmux.server.Server.bind_key | execute `server.bind_key('F12', 'display-message test', key_table='root')` without exception; execute `server.unbind_key('F12', key_table='root')` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_bind_key_2ad6079d5e | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server.clear_prompt_history | execute `from libtmux.common import has_gte_version` without exception; execute `if has_gte_version("3.3"): server.clear_prompt_history()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_clear_prompt_history_194828cc5f | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server.clients | execute `with control_mode() as ctl: names = [c.client_name for c in server.clients] ctl.client_name in names` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_clients_075e2b36ef | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server.cmd | execute `server.cmd('display-message', 'hi')` and expect `<libtmux.common.tmux_cmd object at ...>`; execute `server.cmd('new-session', '-d', '-P', '-F#{session_id}').stdout[0]` and expect `'$2'`; execute `session.cmd('new-window', '-P').stdout[0]` and expect `'libtmux...:2.0'`; execute `Window.from_window_id(window_id=session.cmd( 'new-window', '-P', '-F#{window_id}').stdout[0], server=session.server)` and expect `Window(@4 3:..., Session($1 libtmux_...))`; execute `window.cmd('split-window', '-P', '-F#{pane_id}').stdout[0]` and expect `'%5'`; execute `Pane.from_pane_id(pane_id=window.cmd( 'split-window', '-P', '-F#{pane_id}').stdout[0], server=window.server)` and expect `Pane(%... Window(@... ...:..., Session($1 libtmux_...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_cmd_ac953d76be | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server.command_prompt | execute `from libtmux.common import has_gte_version` without exception; execute `if has_gte_version("3.4"): with control_mode() as ctl: server.command_prompt( "set -g @cp_test '%1'", target_client=ctl.client_name, ) for key in ['h', 'i', 'Enter']: _ = server.cmd('send-keys', '-K', '-c', ctl.client_name, key) result = server.cmd('show-options', '-gv', '@cp_test').stdout[0] else: result = 'hi'` without exception; execute `result` and expect `'hi'`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_command_prompt_a66476935c | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server.confirm_before | execute `from libtmux.common import has_gte_version` without exception; execute `if has_gte_version("3.4"): with control_mode() as ctl: server.confirm_before( 'set -g @cf_test yes', target_client=ctl.client_name, ) _ = server.cmd('send-keys', '-K', '-c', ctl.client_name, 'y') result = server.cmd('show-options', '-gv', '@cf_test').stdout[0] else: result = 'yes'` without exception; execute `result` and expect `'yes'`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_confirm_before_9850ce21b4 | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server.delete_buffer | execute `server.set_buffer('to_delete', buffer_name='del_buf')` without exception; execute `server.delete_buffer(buffer_name='del_buf')` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_delete_buffer_468802c3c4 | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server.detach_all_clients | execute `with control_mode() as ctl: server.detach_all_clients(keep_client=ctl.client_name)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_detach_all_clients_a8327a2553 | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server.detach_client | execute `with control_mode() as ctl: server.detach_client(target_client=ctl.client_name)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_detach_client_3ee32134c7 | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server.display_menu | execute `captured = []` without exception; execute `def fake_cmd(name, *args, **_kw): captured.append((name, args)) return type('R', (), {'stderr': [], 'stdout': []})()` without exception; execute `monkeypatch.setattr(server, 'cmd', fake_cmd)` without exception; execute `server.display_menu('First', '1', 'select-pane', title='menu')` without exception; execute `captured[0][0]` and expect `'display-menu'`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_display_menu_13c14bf74d | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server.display_message | execute `result = server.display_message("#{version}", get_text=True)` without exception; execute `isinstance(result, list) and len(result) == 1` and expect `True`; execute `result = server.display_message("", get_text=True, all_formats=True)` without exception; execute `any("session_name=" in line for line in result)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_display_message_a97c79d3e0 | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server.from_env | execute `from libtmux.server import Server as TmuxServer # doctest: +HIDE` with doctest options `+ALLOW_UNICODE` without exception; execute `socket_path = server.cmd( # doctest: +HIDE "display-message", "-p", "-t", pane.pane_id, "#{socket_path}" ).stdout[0]` with doctest options `+ALLOW_UNICODE` without exception; execute `env = { # doctest: +HIDE "TMUX": f"{socket_path},1,0", "TMUX_PANE": pane.pane_id, }` with doctest options `+ALLOW_UNICODE` without exception; execute `TmuxServer.from_env(env)` and expect `Server(socket_path=...)`; execute `TmuxServer.from_env(env).is_alive()` and expect `True`; execute `try: TmuxServer.from_env({}) except exc.NotInsideTmux as e: print(e)` and expect `Not inside a tmux pane: $TMUX is unset or empty`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_from_env_b58bfdb55c | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server.if_shell | execute `server.if_shell('true', 'set -g @if_test yes')` without exception; execute `server.cmd('show-options', '-gv', '@if_test').stdout[0]` and expect `'yes'`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_if_shell_bfdd5e913f | 3.2a through 3.7b | planned parity | +| src/libtmux/options.py::libtmux.options.explode_arrays | execute `import io` without exception; execute `many_more_options = io.StringIO(r''' terminal-features[0] xterm*:clipboard:ccolour:cstyle:focus terminal-features[1] screen*:title ''')` without exception; execute `many_more_flat_dict = parse_options_to_dict(many_more_options)` without exception; execute `many_more_flat_dict == { "terminal-features[0]": "xterm*:clipboard:ccolour:cstyle:focus", "terminal-features[1]": "screen*:title",}` and expect `True`; execute `explode_arrays(many_more_flat_dict) == { "terminal-features": {0: "xterm*:clipboard:ccolour:cstyle:focus", 1: "screen*:title"}}` and expect `True`; execute `explode_arrays(parse_options_to_dict(io.StringIO(r''' terminal-features[0] xterm*:clipboard:ccolour:cstyle:focus terminal-features[5] screen*:title '''))) == { "terminal-features": {0: "xterm*:clipboard:ccolour:cstyle:focus", 5: "screen*:title"}}` and expect `True`; execute `from libtmux._internal.sparse_array import SparseArray` without exception; execute `hooks_output = io.StringIO(r''' session-renamed[0] display-message 'renamed' session-renamed[5] refresh-client pane-focus-in[0] run-shell 'echo focus' ''')` without exception; execute `hooks_exploded = explode_arrays( parse_options_to_dict(hooks_output), force_array=True, )` without exception; execute `isinstance(hooks_exploded["session-renamed"], SparseArray)` and expect `True`; execute `hooks_exploded["session-renamed"][0]` and expect `"display-message 'renamed'"`; execute `hooks_exploded["session-renamed"][5]` and expect `'refresh-client'`; execute `sorted(hooks_exploded["session-renamed"].keys())` and expect `[0, 5]`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_options_py_libtmux_options_explode_arrays_1e4dfcd9bd | 3.3a through 3.7c | planned parity | +| src/libtmux/options.py::libtmux.options.explode_complex | execute `import io` without exception; execute `explode_complex(explode_arrays(parse_options_to_dict(io.StringIO(r''' terminal-features[0] xterm*:clipboard:ccolour:cstyle:focus terminal-features[5] screen*:title '''))))` and expect `{'terminal-features': {'xterm*': ['clipboard', 'ccolour', 'cstyle', 'focus'], 'screen*': ['title']}}`; execute `explode_complex(explode_arrays(parse_options_to_dict(io.StringIO(r''' terminal-features[0] xterm*:clipboard:ccolour:cstyle:focus terminal-features[5] screen*:title ''')))) == { "terminal-features": {"xterm*": ["clipboard", "ccolour", "cstyle", "focus"], "screen*": ["title"]}}` and expect `True`; execute `explode_complex(explode_arrays(parse_options_to_dict(io.StringIO(r''' command-alias[0] split-pane=split-window command-alias[1] splitp=split-window command-alias[2] "server-info=show-messages -JT" ''')))) == { "command-alias": {"split-pane": "split-window", "splitp": "split-window", "server-info": "show-messages -JT"}}` and expect `True`; execute `explode_complex(explode_arrays({"terminal-features": {0: "xterm*:clipboard:ccolour:cstyle:focus", 1: "screen*:title"}}))` and expect `{'terminal-features': {0: 'xterm*:clipboard:ccolour:cstyle:focus', 1: 'screen*:title'}}`; execute `explode_complex(explode_arrays({"terminal-features": {0: "xterm*:clipboard:ccolour:cstyle:focus", 8: "screen*:title"}})) == SparseArray({'terminal-features': {0: 'xterm*:clipboard:ccolour:cstyle:focus', 8: 'screen*:title'}})` and expect `True`; execute `explode_complex(explode_arrays(parse_options_to_dict(io.StringIO(r''' terminal-overrides[0] xterm-256color:Tc terminal-overrides[1] *:U8=0 ''')))) == { "terminal-overrides": {"xterm-256color": {"Tc": None}, "*": {"U8": 0}}}` and expect `True`; execute `explode_complex(explode_arrays(parse_options_to_dict(io.StringIO(r''' user-keys[100] "\e[test" user-keys[6] "\e\n" user-keys[0] "\e[5;30012~" ''')))) == { "user-keys": {0: "\\e[5;30012~", 6: "\\e\\n", 100: "\\e[test"}}` and expect `True`; execute `explode_complex(explode_arrays(parse_options_to_dict(io.StringIO(r''' status-format[0] "#[align=left range=left #{E:status-left-style}]#[push-default]#{T;=/#{status-left-length}:status-left}#[pop-default]#[norange default]#[list=on align=#{status-justify}]#[list=left-marker]<#[list=right-marker]>#[list=on]#{W:#[range=window|#{window_index} #{E:window-status-style}#{?#{&&:#{window_last_flag},#{!=:#{E:window-status-last-style},default}}, #{E:window-status-last-style},}#{?#{&&:#{window_bell_flag},#{!=:#{E:window-status-bell-style},default}}, #{E:window-status-bell-style},#{?#{&&:#{||:#{window_activity_flag},#{window_silence_flag}},#{!=:#{E:window-status-activity-style},default}}, #{E:window-status-activity-style},}}]#[push-default]#{T:window-status-format}#[pop-default]#[norange default]#{?window_end_flag,,#{window-status-separator}},#[range=window|#{window_index} list=focus #{?#{!=:#{E:window-status-current-style},default},#{E:window-status-current-style},#{E:window-status-style}}#{?#{&&:#{window_last_flag},#{!=:#{E:window-status-last-style},default}}, #{E:window-status-last-style},}#{?#{&&:#{window_bell_flag},#{!=:#{E:window-status-bell-style},default}}, #{E:window-status-bell-style},#{?#{&&:#{||:#{window_activity_flag},#{window_silence_flag}},#{!=:#{E:window-status-activity-style},default}}, #{E:window-status-activity-style},}}]#[push-default]#{T:window-status-current-format}#[pop-default]#[norange list=on default]#{?window_end_flag,,#{window-status-separator}}}#[nolist align=right range=right #{E:status-right-style}]#[push-default]#{T;=/#{status-right-length}:status-right}#[pop-default]#[norange default]" status-format[1] "#[align=centre]#{P:#{?pane_active,#[reverse],}#{pane_index}[#{pane_width}x#{pane_height}]#[default] }" ''')))) == { "status-format": {0: "#[align=left range=left #{E:status-left-style}]#[push-default]#{T;=/#{status-left-length}:status-left}#[pop-default]#[norange default]#[list=on align=#{status-justify}]#[list=left-marker]<#[list=right-marker]>#[list=on]#{W:#[range=window|#{window_index} #{E:window-status-style}#{?#{&&:#{window_last_flag},#{!=:#{E:window-status-last-style},default}}, #{E:window-status-last-style},}#{?#{&&:#{window_bell_flag},#{!=:#{E:window-status-bell-style},default}}, #{E:window-status-bell-style},#{?#{&&:#{||:#{window_activity_flag},#{window_silence_flag}},#{!=:#{E:window-status-activity-style},default}}, #{E:window-status-activity-style},}}]#[push-default]#{T:window-status-format}#[pop-default]#[norange default]#{?window_end_flag,,#{window-status-separator}},#[range=window|#{window_index} list=focus #{?#{!=:#{E:window-status-current-style},default},#{E:window-status-current-style},#{E:window-status-style}}#{?#{&&:#{window_last_flag},#{!=:#{E:window-status-last-style},default}}, #{E:window-status-last-style},}#{?#{&&:#{window_bell_flag},#{!=:#{E:window-status-bell-style},default}}, #{E:window-status-bell-style},#{?#{&&:#{||:#{window_activity_flag},#{window_silence_flag}},#{!=:#{E:window-status-activity-style},default}}, #{E:window-status-activity-style},}}]#[push-default]#{T:window-status-current-format}#[pop-default]#[norange list=on default]#{?window_end_flag,,#{window-status-separator}}}#[nolist align=right range=right #{E:status-right-style}]#[push-default]#{T;=/#{status-right-length}:status-right}#[pop-default]#[norange default]", 1: "#[align=centre]#{P:#{?pane_active,#[reverse],}#{pane_index}[#{pane_width}x#{pane_height}]#[default] }", }}` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_options_py_libtmux_options_explode_complex_74022ddf0a | 3.3a through 3.7c | planned parity | +| src/libtmux/options.py::libtmux.options.handle_option_error | execute `result = server.cmd( 'set-option', 'unknown-option-name', )` without exception; execute `bool(isinstance(result.stderr, list) and len(result.stderr))` and expect `True`; execute `import pytest` without exception; execute `from libtmux import exc` without exception; execute `with pytest.raises(exc.OptionError): handle_option_error(result.stderr[0])` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_options_py_libtmux_options_handle_option_error_be7cdd74c1 | 3.3a through 3.7c | planned parity | +| src/libtmux/options.py::libtmux.options.parse_options_to_dict | execute `import io` without exception; execute `raw_options = io.StringIO("status-keys vi")` without exception; execute `parse_options_to_dict(raw_options) == {"status-keys": "vi"}` and expect `True`; execute `int_options = io.StringIO("message-limit 50")` without exception; execute `parse_options_to_dict(int_options) == {"message-limit": "50"}` and expect `True`; execute `empty_option = io.StringIO("user-keys")` without exception; execute `parse_options_to_dict(empty_option) == {"user-keys": None}` and expect `True`; execute `array_option = io.StringIO("command-alias[0] split-pane=split-window")` without exception; execute `parse_options_to_dict(array_option) == { "command-alias[0]": "split-pane=split-window"}` and expect `True`; execute `array_option = io.StringIO("command-alias[40] split-pane=split-window")` without exception; execute `parse_options_to_dict(array_option) == { "command-alias[40]": "split-pane=split-window"}` and expect `True`; execute `many_options = io.StringIO(r'''status-keys command-alias[0] split-pane=split-window ''')` without exception; execute `parse_options_to_dict(many_options) == { "command-alias[0]": "split-pane=split-window", "status-keys": None,}` and expect `True`; execute `many_more_options = io.StringIO(r''' terminal-features[0] xterm*:clipboard:ccolour:cstyle:focus terminal-features[1] screen*:title ''')` without exception; execute `parse_options_to_dict(many_more_options) == { "terminal-features[0]": "xterm*:clipboard:ccolour:cstyle:focus", "terminal-features[1]": "screen*:title",}` and expect `True`; execute `quoted_option = io.StringIO(r''' command-alias[0] "choose-session=choose-tree -s" ''')` without exception; execute `parse_options_to_dict(quoted_option) == { "command-alias[0]": "choose-session=choose-tree -s", }` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_options_py_libtmux_options_parse_options_to_dict_c132265e66 | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane | execute `pane` and expect `Pane(%1 Window(@1 1:..., Session($1 ...)))`; execute `pane in window.panes` and expect `True`; execute `pane.window` and expect `Window(@1 1:..., Session($1 ...))`; execute `pane.session` and expect `Session($1 ...)`; execute `with window.split() as pane: pane.send_keys('echo "Hello"') # Do work with the pane # Pane will be killed automatically when exiting the context` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_89b5a1a53c | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.at_bottom | execute `pane.pane_at_bottom` and expect `'1'`; execute `pane.at_bottom` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_at_bottom_6ca2a17fef | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.at_left | execute `pane.pane_at_left` and expect `'1'`; execute `pane.at_left` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_at_left_4e48fd214b | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.at_right | execute `pane.pane_at_right` and expect `'1'`; execute `pane.at_right` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_at_right_2dd8e8ca5d | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.at_top | execute `pane.pane_at_top` and expect `'1'`; execute `pane.at_top` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_at_top_968a50a65b | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.break_pane | execute `pane_to_break = window.split(shell='sleep 1m')` without exception; execute `new_window = pane_to_break.break_pane(window_name='broken')` without exception; execute `new_window.window_name` and expect `'broken'`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_break_pane_a7e77fef15 | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.capture_pane | execute `pane = window.split(shell='sh')` without exception; execute `pane.capture_pane()` and expect `['$']`; execute `pane.send_keys('echo "Hello world"', enter=True)` without exception; execute `pane.capture_pane()` and expect `['$ echo "Hello world"', 'Hello world', '$']`; execute `print(chr(10).join(pane.capture_pane()))` and expect `$ echo "Hello world" Hello world $`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_capture_pane_8fed04c38b | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.choose_buffer | execute `pane.choose_buffer()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_choose_buffer_f64d2f76c4 | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.choose_client | execute `pane.choose_client()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_choose_client_b4b49269f0 | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.choose_tree | execute `pane.choose_tree()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_choose_tree_4d42fc71ef | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.clear_history | execute `pane.clear_history()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_clear_history_28d6d8542e | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.clock_mode | execute `pane.clock_mode()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_clock_mode_634705470c | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.cmd | execute `pane.cmd('split-window', '-P').stdout[0]` and expect `'libtmux...:...'`; execute `Pane.from_pane_id(pane_id=pane.cmd( 'split-window', '-P', '-F#{pane_id}').stdout[0], server=pane.server)` and expect `Pane(%... Window(@... ...:..., Session($1 libtmux_...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_cmd_3248ce2f06 | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.copy_mode | execute `pane.copy_mode()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_copy_mode_e7635a3cab | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.customize_mode | execute `pane.customize_mode()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_customize_mode_22625c0e22 | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.display_panes | execute `with control_mode() as ctl: window.active_pane.display_panes()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_display_panes_be2176f2a4 | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.display_popup | execute `with control_mode() as ctl: pane.display_popup(command='true', close_on_exit=True)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_display_popup_ed5880c930 | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.find_window | execute `pane.find_window('sh')` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_find_window_fa049abe56 | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.from_env | execute `socket_path = server.cmd( # doctest: +HIDE "display-message", "-p", "-t", pane.pane_id, "#{socket_path}" ).stdout[0]` with doctest options `+ALLOW_UNICODE` without exception; execute `env = { # doctest: +HIDE "TMUX": f"{socket_path},1,0", "TMUX_PANE": pane.pane_id, }` with doctest options `+ALLOW_UNICODE` without exception; execute `Pane.from_env(env)` and expect `Pane(%1 Window(@1 1:..., Session($1 ...)))`; execute `Pane.from_env(env).pane_id == pane.pane_id` and expect `True`; execute `from libtmux import exc` without exception; execute `try: Pane.from_env({}) except exc.NotInsideTmux as e: print(e)` and expect `Not inside a tmux pane: $TMUX is unset or empty`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_from_env_752ca3f890 | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.from_pane_id | execute `Pane.from_pane_id(server=pane.server, pane_id=pane.pane_id)` and expect `Pane(%1 Window(@1 1:..., Session($1 ...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_from_pane_id_5b9ce3a290 | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.height | execute `pane.height.isdigit()` and expect `True`; execute `pane.height == pane.pane_height` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_height_2fb5ec6d49 | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.id | execute `pane.id` and expect `'%1'`; execute `pane.id == pane.pane_id` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_id_24dc3473ba | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.index | execute `pane.index` and expect `'0'`; execute `pane.index == pane.pane_index` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_index_f37cfd7ca6 | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.join | execute `pane_to_join = window.split(shell='sleep 1m')` without exception; execute `new_window = pane_to_join.break_pane()` without exception; execute `pane_to_join.join(window)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_join_308a926493 | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.kill | execute `pane_1 = pane.split()` without exception; execute `pane_1 in window.panes` and expect `True`; execute `pane_1.kill()` without exception; execute `pane_1 not in window.panes` and expect `True`; execute `pane.window.resize(height=100, width=100)` and expect `Window(@1 1...)`; execute `one_pane_to_rule_them_all = pane.split()` without exception; execute `other_panes = pane.split( ), pane.split()` without exception; execute `all([p in window.panes for p in other_panes])` and expect `True`; execute `one_pane_to_rule_them_all.kill(all_except=True)` without exception; execute `all([p not in window.panes for p in other_panes])` and expect `True`; execute `one_pane_to_rule_them_all in window.panes` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_kill_e25c6299dc | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.move | execute `pane_to_move = window.split(shell='sleep 1m')` without exception; execute `w2 = session.new_window(window_name='move_target')` without exception; execute `pane_to_move.move(w2)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_move_b01c6d05c7 | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.new_pane | execute `from libtmux.common import has_gte_version` without exception; execute `if has_gte_version("3.7"): floating = pane.new_pane(width=40, height=10, shell="sleep 5") is_floating = floating.pane_floating_flag else: is_floating = "1"` without exception; execute `is_floating` and expect `'1'`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_new_pane_11b0985861 | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.paste_buffer | execute `server.set_buffer('pasted_text')` without exception; execute `pane.paste_buffer()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_paste_buffer_3b3c32fdc0 | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.pipe | execute `pane.pipe('cat >> /tmp/output.txt')` without exception; execute `pane.pipe()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_pipe_af350fb3ca | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.refresh | execute `pane.refresh()` without exception; execute `pane.pane_id` and expect `'%1'`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_refresh_b2091c58de | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.reset | execute `pane.reset()` and expect `Pane(%... Window(@... ...:..., Session($1 libtmux_...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_reset_9e06d19ed4 | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.respawn | execute `pane = window.split(shell='sleep 1m')` without exception; execute `pane.respawn(kill=True, shell='sh')` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_respawn_b7ddfec773 | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.select | execute `pane = window.active_pane` without exception; execute `new_pane = window.split()` without exception; execute `pane.refresh()` without exception; execute `active_panes = [p for p in window.panes if p.pane_active == '1']` without exception; execute `pane in active_panes` and expect `True`; execute `new_pane in active_panes` and expect `False`; execute `new_pane.pane_active == '1'` and expect `False`; execute `new_pane.select()` and expect `Pane(...)`; execute `new_pane.pane_active == '1'` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_select_02aaa45270 | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.send_keys | execute `pane = window.split(shell='sh')` without exception; execute `pane.capture_pane()` and expect `['$']`; execute `pane.send_keys('echo "Hello world"', enter=True)` without exception; execute `pane.capture_pane()` and expect `['$ echo "Hello world"', 'Hello world', '$']`; execute `print('\n'.join(pane.capture_pane())) # doctest: +NORMALIZE_WHITESPACE` with doctest options `+NORMALIZE_WHITESPACE` and expect `$ echo "Hello world" Hello world $`; execute `pane.send_keys(reset=True)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_send_keys_bcca63c4d0 | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.send_prefix | execute `pane.send_prefix()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_send_prefix_75166732ce | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.set_title | execute `pane.set_title('my-title')` and expect `Pane(...)`; execute `pane.pane_title` and expect `'my-title'`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_set_title_3b3cf2e8a5 | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.split | execute `(pane.at_left, pane.at_right, pane.at_top, pane.at_bottom)` and expect `(True, True, True, True)`; execute `new_pane = pane.split()` without exception; execute `(new_pane.at_left, new_pane.at_right, new_pane.at_top, new_pane.at_bottom)` and expect `(True, True, False, True)`; execute `right_pane = pane.split(direction=PaneDirection.Right)` without exception; execute `(right_pane.at_left, right_pane.at_right, right_pane.at_top, right_pane.at_bottom)` and expect `(False, True, True, False)`; execute `left_pane = pane.split(direction=PaneDirection.Left)` without exception; execute `(left_pane.at_left, left_pane.at_right, left_pane.at_top, left_pane.at_bottom)` and expect `(True, False, True, False)`; execute `top_pane = pane.split(direction=PaneDirection.Above)` without exception; execute `(top_pane.at_left, top_pane.at_right, top_pane.at_top, top_pane.at_bottom)` and expect `(False, False, True, False)`; execute `pane = session.new_window().active_pane` without exception; execute `top_pane = pane.split(direction=PaneDirection.Above, full_window_split=True)` without exception; execute `(top_pane.at_left, top_pane.at_right, top_pane.at_top, top_pane.at_bottom)` and expect `(True, True, True, False)`; execute `bottom_pane = pane.split( direction=PaneDirection.Below, full_window_split=True)` without exception; execute `(bottom_pane.at_left, bottom_pane.at_right, bottom_pane.at_top, bottom_pane.at_bottom)` and expect `(True, True, False, True)`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_split_15e115b719 | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.swap | execute `pane1 = window.active_pane` without exception; execute `pane2 = window.split()` without exception; execute `pane1_id, pane2_id = pane1.pane_id, pane2.pane_id` without exception; execute `pane1.swap(pane2)` without exception; execute `pane1.refresh()` without exception; execute `pane2.refresh()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_swap_e3427460d0 | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.title | execute `pane.set_title('test-alias')` and expect `Pane(...)`; execute `pane.title == pane.pane_title` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_title_6d895c42c3 | 3.3a through 3.7c | planned parity | +| src/libtmux/pane.py::libtmux.pane.Pane.width | execute `pane.width.isdigit()` and expect `True`; execute `pane.width == pane.pane_width` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pane_py_libtmux_pane_Pane_width_0c5eae94ee | 3.3a through 3.7c | planned parity | +| src/libtmux/pytest_plugin.py::libtmux.pytest_plugin.TestServer | execute `server = Server() # Create server instance` without exception; execute `server.new_session()` and expect `Session($... ...)`; execute `server.is_alive()` and expect `True`; execute `server2 = Server()` without exception; execute `server2.socket_name != server.socket_name` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pytest_plugin_py_libtmux_pytest_plugin_TestServer_b769bdd044 | 3.3a through 3.7c | planned parity | +| src/libtmux/pytest_plugin.py::libtmux.pytest_plugin.control_mode | execute `from libtmux._internal.control_mode import ControlMode` without exception; execute `def test_example(control_mode): with control_mode() as ctl: assert ctl.client_name != ''` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pytest_plugin_py_libtmux_pytest_plugin_control_mode_bd791351b1 | 3.3a through 3.7c | planned parity | +| src/libtmux/pytest_plugin.py::libtmux.pytest_plugin.server | execute `from libtmux.server import Server` without exception; execute `def test_example(server: Server) -> None: assert isinstance(server, Server) session = server.new_session('my session') assert len(server.sessions) == 1 assert [session.name.startswith('my') for session in server.sessions]` without exception; execute `locals().keys()` and expect `dict_keys(...)`; execute `source = ''.join([e.source for e in request._pyfuncitem.dtest.examples][:3])` without exception; execute `pytester = request.getfixturevalue('pytester')` without exception; execute `pytester.makepyfile(**{'whatever.py': source})` and expect `PosixPath(...)`; execute `result = pytester.runpytest('whatever.py', '--disable-warnings')` and expect `===...`; execute `result.assert_outcomes(passed=1)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pytest_plugin_py_libtmux_pytest_plugin_server_acc91d3b27 | 3.3a through 3.7c | planned parity | +| src/libtmux/pytest_plugin.py::libtmux.pytest_plugin.session | execute `from libtmux.session import Session` without exception; execute `def test_example(session: "Session") -> None: assert isinstance(session.name, str) assert session.name.startswith('libtmux_') window = session.new_window(window_name='new one') assert window.name == 'new one'` without exception; execute `locals().keys()` and expect `dict_keys(...)`; execute `source = ''.join([e.source for e in request._pyfuncitem.dtest.examples][:3])` without exception; execute `pytester = request.getfixturevalue('pytester')` without exception; execute `pytester.makepyfile(**{'whatever.py': source})` and expect `PosixPath(...)`; execute `result = pytester.runpytest('whatever.py', '--disable-warnings')` and expect `===...`; execute `result.assert_outcomes(passed=1)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pytest_plugin_py_libtmux_pytest_plugin_session_cc63e7ba8b | 3.3a through 3.7c | planned parity | +| src/libtmux/pytest_plugin.py::libtmux.pytest_plugin.session_params | execute `import pytest` without exception; execute `from libtmux.session import Session` without exception; execute `@pytest.fixture def session_params(session_params): return { 'x': 800, 'y': 600, }` without exception; execute `def test_example(session: "Session") -> None: assert isinstance(session.name, str) assert session.name.startswith('libtmux_') window = session.new_window(window_name='new one') assert window.name == 'new one'` without exception; execute `locals().keys()` and expect `dict_keys(...)`; execute `source = ''.join([e.source for e in request._pyfuncitem.dtest.examples][:4])` without exception; execute `pytester = request.getfixturevalue('pytester')` without exception; execute `pytester.makepyfile(**{'whatever.py': source})` and expect `PosixPath(...)`; execute `result = pytester.runpytest('whatever.py', '--disable-warnings')` and expect `===...`; execute `result.assert_outcomes(passed=1)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_pytest_plugin_py_libtmux_pytest_plugin_session_params_50f29f53fe | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server | execute `server` and expect `Server(socket_name=libtmux_test...)`; execute `server.sessions` and expect `[Session($1 ...)]`; execute `server.sessions[0].windows` and expect `[Window(@1 1:..., Session($1 ...))]`; execute `server.sessions[0].active_window` and expect `Window(@1 1:..., Session($1 ...))`; execute `server.sessions[0].active_pane` and expect `Pane(%1 Window(@1 1:..., Session($1 ...)))`; execute `with Server() as server: session = server.new_session() # Do work with the session # Server will be killed automatically when exiting the context` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_57e38b41fe | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server.attached_sessions | Given session rows whose session_attached values are '0', '1', and '2', Server.attachedSessions() returns only the '1' and '2' sessions by testing the attached count greater than zero; it never dispatches the Python noeq lookup. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_attached_sessions_96d2258b22 | 3.3a through 3.7c | deliberate Java correction | +| src/libtmux/server.py::libtmux.server.Server.bind_key | execute `server.bind_key('F12', 'display-message test', key_table='root')` without exception; execute `server.unbind_key('F12', key_table='root')` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_bind_key_2ad6079d5e | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server.clear_prompt_history | execute `from libtmux.common import has_gte_version` without exception; execute `if has_gte_version("3.3"): server.clear_prompt_history()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_clear_prompt_history_194828cc5f | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server.clients | execute `with control_mode() as ctl: names = [c.client_name for c in server.clients] ctl.client_name in names` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_clients_075e2b36ef | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server.cmd | execute `server.cmd('display-message', 'hi')` and expect `<libtmux.common.tmux_cmd object at ...>`; execute `server.cmd('new-session', '-d', '-P', '-F#{session_id}').stdout[0]` and expect `'$2'`; execute `session.cmd('new-window', '-P').stdout[0]` and expect `'libtmux...:2.0'`; execute `Window.from_window_id(window_id=session.cmd( 'new-window', '-P', '-F#{window_id}').stdout[0], server=session.server)` and expect `Window(@4 3:..., Session($1 libtmux_...))`; execute `window.cmd('split-window', '-P', '-F#{pane_id}').stdout[0]` and expect `'%5'`; execute `Pane.from_pane_id(pane_id=window.cmd( 'split-window', '-P', '-F#{pane_id}').stdout[0], server=window.server)` and expect `Pane(%... Window(@... ...:..., Session($1 libtmux_...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_cmd_ac953d76be | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server.command_prompt | execute `from libtmux.common import has_gte_version` without exception; execute `if has_gte_version("3.4"): with control_mode() as ctl: server.command_prompt( "set -g @cp_test '%1'", target_client=ctl.client_name, ) for key in ['h', 'i', 'Enter']: _ = server.cmd('send-keys', '-K', '-c', ctl.client_name, key) result = server.cmd('show-options', '-gv', '@cp_test').stdout[0] else: result = 'hi'` without exception; execute `result` and expect `'hi'`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_command_prompt_a66476935c | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server.confirm_before | execute `from libtmux.common import has_gte_version` without exception; execute `if has_gte_version("3.4"): with control_mode() as ctl: server.confirm_before( 'set -g @cf_test yes', target_client=ctl.client_name, ) _ = server.cmd('send-keys', '-K', '-c', ctl.client_name, 'y') result = server.cmd('show-options', '-gv', '@cf_test').stdout[0] else: result = 'yes'` without exception; execute `result` and expect `'yes'`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_confirm_before_9850ce21b4 | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server.delete_buffer | execute `server.set_buffer('to_delete', buffer_name='del_buf')` without exception; execute `server.delete_buffer(buffer_name='del_buf')` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_delete_buffer_468802c3c4 | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server.detach_all_clients | execute `with control_mode() as ctl: server.detach_all_clients(keep_client=ctl.client_name)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_detach_all_clients_a8327a2553 | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server.detach_client | execute `with control_mode() as ctl: server.detach_client(target_client=ctl.client_name)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_detach_client_3ee32134c7 | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server.display_menu | execute `captured = []` without exception; execute `def fake_cmd(name, *args, **_kw): captured.append((name, args)) return type('R', (), {'stderr': [], 'stdout': []})()` without exception; execute `monkeypatch.setattr(server, 'cmd', fake_cmd)` without exception; execute `server.display_menu('First', '1', 'select-pane', title='menu')` without exception; execute `captured[0][0]` and expect `'display-menu'`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_display_menu_13c14bf74d | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server.display_message | execute `result = server.display_message("#{version}", get_text=True)` without exception; execute `isinstance(result, list) and len(result) == 1` and expect `True`; execute `result = server.display_message("", get_text=True, all_formats=True)` without exception; execute `any("session_name=" in line for line in result)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_display_message_a97c79d3e0 | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server.from_env | execute `from libtmux.server import Server as TmuxServer # doctest: +HIDE` with doctest options `+ALLOW_UNICODE` without exception; execute `socket_path = server.cmd( # doctest: +HIDE "display-message", "-p", "-t", pane.pane_id, "#{socket_path}" ).stdout[0]` with doctest options `+ALLOW_UNICODE` without exception; execute `env = { # doctest: +HIDE "TMUX": f"{socket_path},1,0", "TMUX_PANE": pane.pane_id, }` with doctest options `+ALLOW_UNICODE` without exception; execute `TmuxServer.from_env(env)` and expect `Server(socket_path=...)`; execute `TmuxServer.from_env(env).is_alive()` and expect `True`; execute `try: TmuxServer.from_env({}) except exc.NotInsideTmux as e: print(e)` and expect `Not inside a tmux pane: $TMUX is unset or empty`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_from_env_b58bfdb55c | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server.if_shell | execute `server.if_shell('true', 'set -g @if_test yes')` without exception; execute `server.cmd('show-options', '-gv', '@if_test').stdout[0]` and expect `'yes'`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_if_shell_bfdd5e913f | 3.3a through 3.7c | planned parity | | src/libtmux/server.py::libtmux.server.Server.is_alive | execute `tmux = Server(socket_name="no_exist")` without exception; execute `assert not tmux.is_alive()` without exception. | documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_is_alive_f9b9133a35 | not tmux-bound | planned parity | | src/libtmux/server.py::libtmux.server.Server.kill | execute `svr = Server(socket_name="testing")` without exception; execute `svr` and expect `Server(socket_name=testing)`; execute `svr.new_session()` and expect `Session(...)`; execute `svr.is_alive()` and expect `True`; execute `svr.kill()` without exception; execute `svr.is_alive()` and expect `False`. | documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_kill_35fdecdce9 | not tmux-bound | planned parity | -| src/libtmux/server.py::libtmux.server.Server.list_buffers | execute `server.set_buffer('buf_data')` without exception; execute `result = server.list_buffers()` without exception; execute `len(result) >= 1` and expect `True`; execute `server.set_buffer('hello', buffer_name='gap6_demo')` without exception; execute `'gap6_demo' in server.list_buffers(format_string='#{buffer_name}')` and expect `True`; execute `matches = server.list_buffers( format_string='#{buffer_name}', filter='#{m:gap6_*,#{buffer_name}}', )` without exception; execute `'gap6_demo' in matches` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_list_buffers_48773b833a | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server.list_clients | execute `isinstance(server.list_clients(), list)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_list_clients_bac98f9236 | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server.list_commands | execute `result = server.list_commands(command_name='send-keys')` without exception; execute `len(result) >= 1` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_list_commands_69a447b6cf | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server.list_keys | execute `result = server.list_keys()` without exception; execute `isinstance(result, list)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_list_keys_c755b7257d | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server.load_buffer | execute `import pathlib` without exception; execute `path = pathlib.Path(request.config.rootdir) / '..' / 'tmp_load.txt'` without exception; execute `_ = path.write_text('loaded')` without exception; execute `server.load_buffer(str(path), buffer_name='loaded_buf')` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_load_buffer_b6b92f939b | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server.lock_client | execute `with control_mode() as ctl: server.lock_client()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_lock_client_2e125fb771 | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server.lock_server | execute `with control_mode() as ctl: server.lock_server()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_lock_server_2297d65e57 | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server.new_session | execute `server.new_session()` and expect `Session($2 2)`; execute `server.new_session()` and expect `Session($3 3)`; execute `server.new_session(session_name='my session')` and expect `Session($4 my session)`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_new_session_08418c7bfe | 3.2a through 3.7b | planned parity | +| src/libtmux/server.py::libtmux.server.Server.list_buffers | execute `server.set_buffer('buf_data')` without exception; execute `result = server.list_buffers()` without exception; execute `len(result) >= 1` and expect `True`; execute `server.set_buffer('hello', buffer_name='gap6_demo')` without exception; execute `'gap6_demo' in server.list_buffers(format_string='#{buffer_name}')` and expect `True`; execute `matches = server.list_buffers( format_string='#{buffer_name}', filter='#{m:gap6_*,#{buffer_name}}', )` without exception; execute `'gap6_demo' in matches` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_list_buffers_48773b833a | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server.list_clients | execute `isinstance(server.list_clients(), list)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_list_clients_bac98f9236 | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server.list_commands | execute `result = server.list_commands(command_name='send-keys')` without exception; execute `len(result) >= 1` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_list_commands_69a447b6cf | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server.list_keys | execute `result = server.list_keys()` without exception; execute `isinstance(result, list)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_list_keys_c755b7257d | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server.load_buffer | execute `import pathlib` without exception; execute `path = pathlib.Path(request.config.rootdir) / '..' / 'tmp_load.txt'` without exception; execute `_ = path.write_text('loaded')` without exception; execute `server.load_buffer(str(path), buffer_name='loaded_buf')` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_load_buffer_b6b92f939b | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server.lock_client | execute `with control_mode() as ctl: server.lock_client()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_lock_client_2e125fb771 | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server.lock_server | execute `with control_mode() as ctl: server.lock_server()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_lock_server_2297d65e57 | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server.new_session | execute `server.new_session()` and expect `Session($2 2)`; execute `server.new_session()` and expect `Session($3 3)`; execute `server.new_session(session_name='my session')` and expect `Session($4 my session)`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_new_session_08418c7bfe | 3.3a through 3.7c | planned parity | | src/libtmux/server.py::libtmux.server.Server.raise_if_dead | execute `tmux = Server(socket_name="no_exist")` without exception; execute `try: tmux.raise_if_dead() except Exception as e: print(type(e))` and expect `<class 'subprocess.CalledProcessError'>`. | documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_raise_if_dead_44ee0e8059 | not tmux-bound | planned parity | -| src/libtmux/server.py::libtmux.server.Server.refresh_client | execute `with control_mode() as ctl: server.refresh_client()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_refresh_client_e5e214385b | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server.run_shell | execute `result = server.run_shell('true')` without exception; execute `isinstance(result, list)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_run_shell_23c807ff8c | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server.save_buffer | execute `import pathlib` without exception; execute `server.set_buffer('save_me')` without exception; execute `path = pathlib.Path(request.config.rootdir) / '..' / 'tmp_save.txt'` without exception; execute `server.save_buffer(str(path))` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_save_buffer_1e163deb5e | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server.search_panes | execute `sess = server.new_session(session_name='gap7_pane_demo')` without exception; execute `window = sess.active_window` without exception; execute `target_pane = window.split()` without exception; execute `matches = server.search_panes( filter=f'#{{m:{target_pane.pane_id},#{{pane_id}}}}' )` without exception; execute `[p.pane_id for p in matches] == [target_pane.pane_id]` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_search_panes_ace9238c91 | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server.search_sessions | execute `server.new_session(session_name='gap7_alpha')` and expect `Session($... gap7_alpha)`; execute `server.new_session(session_name='other_beta')` and expect `Session($... other_beta)`; execute `matches = server.search_sessions(filter='#{m:gap7_*,#{session_name}}')` without exception; execute `[s.session_name for s in matches]` and expect `['gap7_alpha']`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_search_sessions_63266e09fc | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server.search_windows | execute `sess = server.new_session(session_name='gap7_win_demo')` without exception; execute `_ = sess.new_window(window_name='gap7_target')` without exception; execute `_ = sess.new_window(window_name='other_window')` without exception; execute `matches = server.search_windows(filter='#{m:gap7_*,#{window_name}}')` without exception; execute `any(w.window_name == 'gap7_target' for w in matches)` and expect `True`; execute `any(w.window_name == 'other_window' for w in matches)` and expect `False`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_search_windows_989f156b66 | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server.server_access | execute `from libtmux.common import has_gte_version` without exception; execute `if has_gte_version("3.3"): result = server.server_access(list_access=True) assert isinstance(result, list)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_server_access_1c74bee94f | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server.set_buffer | execute `server.set_buffer('hello')` without exception; execute `server.show_buffer()` and expect `'hello'`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_set_buffer_135fc0776b | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server.show_buffer | execute `server.set_buffer('test_data')` without exception; execute `server.show_buffer()` and expect `'test_data'`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_show_buffer_c14fd1870c | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server.show_messages | execute `with control_mode() as ctl: result = server.show_messages(target_client=ctl.client_name)` without exception; execute `isinstance(result, list)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_show_messages_d9d52ef73f | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server.show_prompt_history | execute `from libtmux.common import has_gte_version` without exception; execute `if has_gte_version("3.3"): result = server.show_prompt_history() else: result = []` without exception; execute `isinstance(result, list)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_show_prompt_history_a9e4067d38 | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server.source_file | execute `import pathlib` without exception; execute `conf = pathlib.Path(request.config.rootdir) / '..' / 'tmp_src.conf'` without exception; execute `_ = conf.write_text('set -g @test_source yes')` without exception; execute `server.source_file(str(conf))` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_source_file_9505d86519 | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server.start_server | execute `server.start_server()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_start_server_6525c77f33 | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server.suspend_client | execute `with control_mode() as ctl: server.suspend_client()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_suspend_client_d073b2cc5b | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server.unbind_key | execute `server.bind_key('F11', 'display-message test', key_table='root')` without exception; execute `server.unbind_key('F11', key_table='root')` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_unbind_key_2b54a48018 | 3.2a through 3.7b | planned parity | -| src/libtmux/server.py::libtmux.server.Server.wait_for | execute `server.new_session(session_name='wait_test')` and expect `Session(...)`; execute `server.wait_for('test_channel', set_flag=True)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_wait_for_d9ac7b9cee | 3.2a through 3.7b | planned parity | -| src/libtmux/session.py::libtmux.session.Session | execute `session` and expect `Session($1 ...)`; execute `session.windows` and expect `[Window(@1 ...:..., Session($1 ...)]`; execute `session.active_window` and expect `Window(@1 ...:..., Session($1 ...))`; execute `session.active_pane` and expect `Pane(%1 Window(@1 ...:..., Session($1 ...)))`; execute `with server.new_session() as session: window = session.new_window() # Do work with the window # Session will be killed automatically when exiting the context` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_session_py_libtmux_session_Session_dd81995c9e | 3.2a through 3.7b | planned parity | -| src/libtmux/session.py::libtmux.session.Session.attach | execute `session = server.new_session()` without exception; execute `session not in server.attached_sessions` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_session_py_libtmux_session_Session_attach_89d15c2919 | 3.2a through 3.7b | planned parity | -| src/libtmux/session.py::libtmux.session.Session.cmd | execute `session.cmd('new-window', '-P').stdout[0]` and expect `'libtmux...:....0'`; execute `Window.from_window_id(window_id=session.cmd( 'new-window', '-P', '-F#{window_id}').stdout[0], server=session.server)` and expect `Window(@... ...:..., Session($1 libtmux_...))`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_session_py_libtmux_session_Session_cmd_cbbe5d98aa | 3.2a through 3.7b | planned parity | -| src/libtmux/session.py::libtmux.session.Session.detach_client | execute `with control_mode() as ctl: session.detach_client()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_session_py_libtmux_session_Session_detach_client_a026827392 | 3.2a through 3.7b | planned parity | -| src/libtmux/session.py::libtmux.session.Session.from_env | execute `socket_path = server.cmd( # doctest: +HIDE "display-message", "-p", "-t", pane.pane_id, "#{socket_path}" ).stdout[0]` with doctest options `+ALLOW_UNICODE` without exception; execute `env = {"TMUX": f"{socket_path},1,999", "TMUX_PANE": pane.pane_id}` without exception; execute `Session.from_env(env).session_id == session.session_id` and expect `True`; execute `Session.from_env(env)` and expect `Session($1 ...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_session_py_libtmux_session_Session_from_env_0fe4a8131e | 3.2a through 3.7b | planned parity | -| src/libtmux/session.py::libtmux.session.Session.from_session_id | execute `Session.from_session_id( server=session.server, session_id=session.session_id )` and expect `Session($1 ...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_session_py_libtmux_session_Session_from_session_id_477d46c325 | 3.2a through 3.7b | planned parity | -| src/libtmux/session.py::libtmux.session.Session.id | execute `session.id` and expect `'$1'`; execute `session.id == session.session_id` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_session_py_libtmux_session_Session_id_3dd701484f | 3.2a through 3.7b | planned parity | -| src/libtmux/session.py::libtmux.session.Session.kill | execute `session_1 = server.new_session()` without exception; execute `session_1 in server.sessions` and expect `True`; execute `session_1.kill()` without exception; execute `session_1 not in server.sessions` and expect `True`; execute `one_session_to_rule_them_all = server.new_session()` without exception; execute `other_sessions = server.new_session( ), server.new_session()` without exception; execute `all([w in server.sessions for w in other_sessions])` and expect `True`; execute `one_session_to_rule_them_all.kill(all_except=True)` without exception; execute `all([w not in server.sessions for w in other_sessions])` and expect `True`; execute `one_session_to_rule_them_all in server.sessions` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_session_py_libtmux_session_Session_kill_d778a9ed8d | 3.2a through 3.7b | planned parity | -| src/libtmux/session.py::libtmux.session.Session.last_window | execute `w1 = session.new_window(window_name='lw_a')` without exception; execute `w2 = session.new_window(window_name='lw_b', attach=True)` without exception; execute `session.last_window()` and expect `Window(...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_session_py_libtmux_session_Session_last_window_26d2d1488f | 3.2a through 3.7b | planned parity | -| src/libtmux/session.py::libtmux.session.Session.lock_session | execute `session.lock_session()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_session_py_libtmux_session_Session_lock_session_0ff9f949d4 | 3.2a through 3.7b | planned parity | -| src/libtmux/session.py::libtmux.session.Session.name | execute `session.name` and expect `'libtmux_...'`; execute `session.name == session.session_name` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_session_py_libtmux_session_Session_name_b84ddae345 | 3.2a through 3.7b | planned parity | -| src/libtmux/session.py::libtmux.session.Session.new_window | execute `window_initial = session.new_window(window_name='Example')` without exception; execute `window_initial` and expect `Window(@... 2:Example, Session($1 libtmux_...))`; execute `window_initial.window_index` and expect `'2'`; execute `window_before = session.new_window( window_name='Window before', direction=WindowDirection.Before)` without exception; execute `window_initial.refresh()` without exception; execute `window_before` and expect `Window(@... 1:Window before, Session($1 libtmux_...))`; execute `window_initial` and expect `Window(@... 3:Example, Session($1 libtmux_...))`; execute `window_after = session.new_window( window_name='Window after', direction=WindowDirection.After)` without exception; execute `window_initial.refresh()` without exception; execute `window_after.refresh()` without exception; execute `window_after` and expect `Window(@... 3:Window after, Session($1 libtmux_...))`; execute `window_initial` and expect `Window(@... 4:Example, Session($1 libtmux_...))`; execute `window_before` and expect `Window(@... 1:Window before, Session($1 libtmux_...))`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_session_py_libtmux_session_Session_new_window_e4664195cd | 3.2a through 3.7b | planned parity | -| src/libtmux/session.py::libtmux.session.Session.next_window | execute `w = session.new_window(window_name='nw_test')` without exception; execute `session.next_window()` and expect `Window(...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_session_py_libtmux_session_Session_next_window_9f16a9321d | 3.2a through 3.7b | planned parity | -| src/libtmux/session.py::libtmux.session.Session.previous_window | execute `w = session.new_window(window_name='pw_test')` without exception; execute `session.previous_window()` and expect `Window(...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_session_py_libtmux_session_Session_previous_window_40c7f40e6b | 3.2a through 3.7b | planned parity | -| src/libtmux/session.py::libtmux.session.Session.search_panes | execute `target_pane = session.active_window.split()` without exception; execute `matches = session.search_panes( filter=f'#{{m:{target_pane.pane_id},#{{pane_id}}}}' )` without exception; execute `[p.pane_id for p in matches] == [target_pane.pane_id]` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_session_py_libtmux_session_Session_search_panes_5d4648af7c | 3.2a through 3.7b | planned parity | -| src/libtmux/session.py::libtmux.session.Session.search_windows | execute `_ = session.new_window(window_name='gap7s_target')` without exception; execute `_ = session.new_window(window_name='other_window')` without exception; execute `matches = session.search_windows(filter='#{m:gap7s_*,#{window_name}}')` without exception; execute `[w.window_name for w in matches]` and expect `['gap7s_target']`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_session_py_libtmux_session_Session_search_windows_539ba119c0 | 3.2a through 3.7b | planned parity | +| src/libtmux/server.py::libtmux.server.Server.refresh_client | execute `with control_mode() as ctl: server.refresh_client()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_refresh_client_e5e214385b | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server.run_shell | execute `result = server.run_shell('true')` without exception; execute `isinstance(result, list)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_run_shell_23c807ff8c | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server.save_buffer | execute `import pathlib` without exception; execute `server.set_buffer('save_me')` without exception; execute `path = pathlib.Path(request.config.rootdir) / '..' / 'tmp_save.txt'` without exception; execute `server.save_buffer(str(path))` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_save_buffer_1e163deb5e | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server.search_panes | execute `sess = server.new_session(session_name='gap7_pane_demo')` without exception; execute `window = sess.active_window` without exception; execute `target_pane = window.split()` without exception; execute `matches = server.search_panes( filter=f'#{{m:{target_pane.pane_id},#{{pane_id}}}}' )` without exception; execute `[p.pane_id for p in matches] == [target_pane.pane_id]` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_search_panes_ace9238c91 | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server.search_sessions | execute `server.new_session(session_name='gap7_alpha')` and expect `Session($... gap7_alpha)`; execute `server.new_session(session_name='other_beta')` and expect `Session($... other_beta)`; execute `matches = server.search_sessions(filter='#{m:gap7_*,#{session_name}}')` without exception; execute `[s.session_name for s in matches]` and expect `['gap7_alpha']`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_search_sessions_63266e09fc | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server.search_windows | execute `sess = server.new_session(session_name='gap7_win_demo')` without exception; execute `_ = sess.new_window(window_name='gap7_target')` without exception; execute `_ = sess.new_window(window_name='other_window')` without exception; execute `matches = server.search_windows(filter='#{m:gap7_*,#{window_name}}')` without exception; execute `any(w.window_name == 'gap7_target' for w in matches)` and expect `True`; execute `any(w.window_name == 'other_window' for w in matches)` and expect `False`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_search_windows_989f156b66 | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server.server_access | execute `from libtmux.common import has_gte_version` without exception; execute `if has_gte_version("3.3"): result = server.server_access(list_access=True) assert isinstance(result, list)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_server_access_1c74bee94f | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server.set_buffer | execute `server.set_buffer('hello')` without exception; execute `server.show_buffer()` and expect `'hello'`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_set_buffer_135fc0776b | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server.show_buffer | execute `server.set_buffer('test_data')` without exception; execute `server.show_buffer()` and expect `'test_data'`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_show_buffer_c14fd1870c | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server.show_messages | execute `with control_mode() as ctl: result = server.show_messages(target_client=ctl.client_name)` without exception; execute `isinstance(result, list)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_show_messages_d9d52ef73f | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server.show_prompt_history | execute `from libtmux.common import has_gte_version` without exception; execute `if has_gte_version("3.3"): result = server.show_prompt_history() else: result = []` without exception; execute `isinstance(result, list)` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_show_prompt_history_a9e4067d38 | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server.source_file | execute `import pathlib` without exception; execute `conf = pathlib.Path(request.config.rootdir) / '..' / 'tmp_src.conf'` without exception; execute `_ = conf.write_text('set -g @test_source yes')` without exception; execute `server.source_file(str(conf))` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_source_file_9505d86519 | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server.start_server | execute `server.start_server()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_start_server_6525c77f33 | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server.suspend_client | execute `with control_mode() as ctl: server.suspend_client()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_suspend_client_d073b2cc5b | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server.unbind_key | execute `server.bind_key('F11', 'display-message test', key_table='root')` without exception; execute `server.unbind_key('F11', key_table='root')` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_unbind_key_2b54a48018 | 3.3a through 3.7c | planned parity | +| src/libtmux/server.py::libtmux.server.Server.wait_for | execute `server.new_session(session_name='wait_test')` and expect `Session(...)`; execute `server.wait_for('test_channel', set_flag=True)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_server_py_libtmux_server_Server_wait_for_d9ac7b9cee | 3.3a through 3.7c | planned parity | +| src/libtmux/session.py::libtmux.session.Session | execute `session` and expect `Session($1 ...)`; execute `session.windows` and expect `[Window(@1 ...:..., Session($1 ...)]`; execute `session.active_window` and expect `Window(@1 ...:..., Session($1 ...))`; execute `session.active_pane` and expect `Pane(%1 Window(@1 ...:..., Session($1 ...)))`; execute `with server.new_session() as session: window = session.new_window() # Do work with the window # Session will be killed automatically when exiting the context` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_session_py_libtmux_session_Session_dd81995c9e | 3.3a through 3.7c | planned parity | +| src/libtmux/session.py::libtmux.session.Session.attach | execute `session = server.new_session()` without exception; execute `session not in server.attached_sessions` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_session_py_libtmux_session_Session_attach_89d15c2919 | 3.3a through 3.7c | planned parity | +| src/libtmux/session.py::libtmux.session.Session.cmd | execute `session.cmd('new-window', '-P').stdout[0]` and expect `'libtmux...:....0'`; execute `Window.from_window_id(window_id=session.cmd( 'new-window', '-P', '-F#{window_id}').stdout[0], server=session.server)` and expect `Window(@... ...:..., Session($1 libtmux_...))`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_session_py_libtmux_session_Session_cmd_cbbe5d98aa | 3.3a through 3.7c | planned parity | +| src/libtmux/session.py::libtmux.session.Session.detach_client | execute `with control_mode() as ctl: session.detach_client()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_session_py_libtmux_session_Session_detach_client_a026827392 | 3.3a through 3.7c | planned parity | +| src/libtmux/session.py::libtmux.session.Session.from_env | execute `socket_path = server.cmd( # doctest: +HIDE "display-message", "-p", "-t", pane.pane_id, "#{socket_path}" ).stdout[0]` with doctest options `+ALLOW_UNICODE` without exception; execute `env = {"TMUX": f"{socket_path},1,999", "TMUX_PANE": pane.pane_id}` without exception; execute `Session.from_env(env).session_id == session.session_id` and expect `True`; execute `Session.from_env(env)` and expect `Session($1 ...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_session_py_libtmux_session_Session_from_env_0fe4a8131e | 3.3a through 3.7c | planned parity | +| src/libtmux/session.py::libtmux.session.Session.from_session_id | execute `Session.from_session_id( server=session.server, session_id=session.session_id )` and expect `Session($1 ...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_session_py_libtmux_session_Session_from_session_id_477d46c325 | 3.3a through 3.7c | planned parity | +| src/libtmux/session.py::libtmux.session.Session.id | execute `session.id` and expect `'$1'`; execute `session.id == session.session_id` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_session_py_libtmux_session_Session_id_3dd701484f | 3.3a through 3.7c | planned parity | +| src/libtmux/session.py::libtmux.session.Session.kill | execute `session_1 = server.new_session()` without exception; execute `session_1 in server.sessions` and expect `True`; execute `session_1.kill()` without exception; execute `session_1 not in server.sessions` and expect `True`; execute `one_session_to_rule_them_all = server.new_session()` without exception; execute `other_sessions = server.new_session( ), server.new_session()` without exception; execute `all([w in server.sessions for w in other_sessions])` and expect `True`; execute `one_session_to_rule_them_all.kill(all_except=True)` without exception; execute `all([w not in server.sessions for w in other_sessions])` and expect `True`; execute `one_session_to_rule_them_all in server.sessions` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_session_py_libtmux_session_Session_kill_d778a9ed8d | 3.3a through 3.7c | planned parity | +| src/libtmux/session.py::libtmux.session.Session.last_window | execute `w1 = session.new_window(window_name='lw_a')` without exception; execute `w2 = session.new_window(window_name='lw_b', attach=True)` without exception; execute `session.last_window()` and expect `Window(...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_session_py_libtmux_session_Session_last_window_26d2d1488f | 3.3a through 3.7c | planned parity | +| src/libtmux/session.py::libtmux.session.Session.lock_session | execute `session.lock_session()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_session_py_libtmux_session_Session_lock_session_0ff9f949d4 | 3.3a through 3.7c | planned parity | +| src/libtmux/session.py::libtmux.session.Session.name | execute `session.name` and expect `'libtmux_...'`; execute `session.name == session.session_name` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_session_py_libtmux_session_Session_name_b84ddae345 | 3.3a through 3.7c | planned parity | +| src/libtmux/session.py::libtmux.session.Session.new_window | execute `window_initial = session.new_window(window_name='Example')` without exception; execute `window_initial` and expect `Window(@... 2:Example, Session($1 libtmux_...))`; execute `window_initial.window_index` and expect `'2'`; execute `window_before = session.new_window( window_name='Window before', direction=WindowDirection.Before)` without exception; execute `window_initial.refresh()` without exception; execute `window_before` and expect `Window(@... 1:Window before, Session($1 libtmux_...))`; execute `window_initial` and expect `Window(@... 3:Example, Session($1 libtmux_...))`; execute `window_after = session.new_window( window_name='Window after', direction=WindowDirection.After)` without exception; execute `window_initial.refresh()` without exception; execute `window_after.refresh()` without exception; execute `window_after` and expect `Window(@... 3:Window after, Session($1 libtmux_...))`; execute `window_initial` and expect `Window(@... 4:Example, Session($1 libtmux_...))`; execute `window_before` and expect `Window(@... 1:Window before, Session($1 libtmux_...))`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_session_py_libtmux_session_Session_new_window_e4664195cd | 3.3a through 3.7c | planned parity | +| src/libtmux/session.py::libtmux.session.Session.next_window | execute `w = session.new_window(window_name='nw_test')` without exception; execute `session.next_window()` and expect `Window(...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_session_py_libtmux_session_Session_next_window_9f16a9321d | 3.3a through 3.7c | planned parity | +| src/libtmux/session.py::libtmux.session.Session.previous_window | execute `w = session.new_window(window_name='pw_test')` without exception; execute `session.previous_window()` and expect `Window(...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_session_py_libtmux_session_Session_previous_window_40c7f40e6b | 3.3a through 3.7c | planned parity | +| src/libtmux/session.py::libtmux.session.Session.search_panes | execute `target_pane = session.active_window.split()` without exception; execute `matches = session.search_panes( filter=f'#{{m:{target_pane.pane_id},#{{pane_id}}}}' )` without exception; execute `[p.pane_id for p in matches] == [target_pane.pane_id]` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_session_py_libtmux_session_Session_search_panes_5d4648af7c | 3.3a through 3.7c | planned parity | +| src/libtmux/session.py::libtmux.session.Session.search_windows | execute `_ = session.new_window(window_name='gap7s_target')` without exception; execute `_ = session.new_window(window_name='other_window')` without exception; execute `matches = session.search_windows(filter='#{m:gap7s_*,#{window_name}}')` without exception; execute `[w.window_name for w in matches]` and expect `['gap7s_target']`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_session_py_libtmux_session_Session_search_windows_539ba119c0 | 3.3a through 3.7c | planned parity | | src/libtmux/test/random.py::libtmux.test.random.RandomStrSequence.__init__ | execute `rng = RandomStrSequence() # pragma: no cover` without exception; execute `next(rng) # pragma: no cover` and expect `'...'`; execute `len(next(rng)) # pragma: no cover` and expect `8`; execute `type(next(rng)) # pragma: no cover` and expect `<class 'str'>`. | documentation contract test | PythonBehaviorParityTest#src_libtmux_test_random_py_libtmux_test_random_RandomStrSequence_init_9185b29fc3 | not tmux-bound | planned parity | -| src/libtmux/test/random.py::libtmux.test.random.get_test_session_name | execute `get_test_session_name(server=server) # pragma: no cover` and expect `'libtmux_...'`; execute `name1 = get_test_session_name(server=server) # pragma: no cover` without exception; execute `name2 = get_test_session_name(server=server) # pragma: no cover` without exception; execute `name1 != name2 # pragma: no cover` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_test_random_py_libtmux_test_random_get_test_session_name_73d632a4e5 | 3.2a through 3.7b | planned parity | -| src/libtmux/test/random.py::libtmux.test.random.get_test_window_name | execute `get_test_window_name(session=session) # pragma: no cover` and expect `'libtmux_...'`; execute `name1 = get_test_window_name(session=session) # pragma: no cover` without exception; execute `name2 = get_test_window_name(session=session) # pragma: no cover` without exception; execute `name1 != name2 # pragma: no cover` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_test_random_py_libtmux_test_random_get_test_window_name_bd6e68b630 | 3.2a through 3.7b | planned parity | -| src/libtmux/test/retry.py::libtmux.test.retry.retry_until | execute `def fn(): p = session.active_window.active_pane return p.pane_current_path is not None` without exception; execute `retry_until(fn)` and expect `True`; execute `assert retry_until(fn, raises=False)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_test_retry_py_libtmux_test_retry_retry_until_9aca6b0f7a | 3.2a through 3.7b | planned parity | -| src/libtmux/test/temporary.py::libtmux.test.temporary.temp_session | execute `with temp_session(server) as session: session.new_window(window_name='my window')` and expect `Window(@3 2:my window, Session($... ...))`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_test_temporary_py_libtmux_test_temporary_temp_session_8f0f5e105b | 3.2a through 3.7b | planned parity | -| src/libtmux/test/temporary.py::libtmux.test.temporary.temp_window | execute `with temp_window(session) as window: window` and expect `Window(@2 2:... Session($1 libtmux_...))`; execute `with temp_window(session) as window: window.split()` and expect `Pane(%4 Window(@3 2:libtmux_..., Session($1 libtmux_...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_test_temporary_py_libtmux_test_temporary_temp_window_e4d8c7c838 | 3.2a through 3.7b | planned parity | -| src/libtmux/window.py::libtmux.window.Window | execute `window = session.new_window('My project', attach=True)` without exception; execute `window` and expect `Window(@2 2:My project, Session($... ...))`; execute `window.panes` and expect `[Pane(...)]`; execute `window.active_pane` and expect `Pane(...)`; execute `window.session` and expect `Session(...)`; execute `window.window_id == session.active_window.window_id` and expect `True`; execute `window == session.active_window` and expect `True`; execute `window in session.windows` and expect `True`; execute `with session.new_window() as window: pane = window.split() # Do work with the pane # Window will be killed automatically when exiting the context` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_55c6fc7924 | 3.2a through 3.7b | planned parity | -| src/libtmux/window.py::libtmux.window.Window.cmd | execute `window.cmd('split-window', '-P', '-F#{pane_id}').stdout[0]` and expect `'%...'`; execute `Pane.from_pane_id(pane_id=session.cmd( 'split-window', '-P', '-F#{pane_id}').stdout[0], server=session.server)` and expect `Pane(%... Window(@... ...:..., Session($1 libtmux_...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_cmd_a1c9fd2353 | 3.2a through 3.7b | planned parity | -| src/libtmux/window.py::libtmux.window.Window.display_message | execute `result = window.display_message("#{window_id}", get_text=True)` without exception; execute `result[0].startswith("@")` and expect `True`; execute `result = window.display_message( "#{window_zoomed_flag}", get_text=True )` without exception; execute `result[0] in {"0", "1"}` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_display_message_7bbef1fecf | 3.2a through 3.7b | planned parity | -| src/libtmux/window.py::libtmux.window.Window.from_env | execute `socket_path = server.cmd( # doctest: +HIDE "display-message", "-p", "-t", pane.pane_id, "#{socket_path}" ).stdout[0]` with doctest options `+ALLOW_UNICODE` without exception; execute `env = { # doctest: +HIDE "TMUX": f"{socket_path},1,0", "TMUX_PANE": pane.pane_id, }` with doctest options `+ALLOW_UNICODE` without exception; execute `Window.from_env(env)` and expect `Window(@1 1:..., Session($1 ...))`; execute `_ = session.new_window(window_name="foreground", attach=True)` without exception; execute `Window.from_env(env).window_id == window.window_id` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_from_env_d2f7ecc8c4 | 3.2a through 3.7b | planned parity | -| src/libtmux/window.py::libtmux.window.Window.from_window_id | execute `Window.from_window_id(server=window.server, window_id=window.window_id)` and expect `Window(@1 1:..., Session($1 ...))`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_from_window_id_6b5204f062 | 3.2a through 3.7b | planned parity | -| src/libtmux/window.py::libtmux.window.Window.height | execute `window.height.isdigit()` and expect `True`; execute `window.height == window.window_height` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_height_850114ffbe | 3.2a through 3.7b | planned parity | -| src/libtmux/window.py::libtmux.window.Window.id | execute `window.id` and expect `'@1'`; execute `window.id == window.window_id` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_id_50e7f4b6ad | 3.2a through 3.7b | planned parity | -| src/libtmux/window.py::libtmux.window.Window.index | execute `window.index` and expect `'1'`; execute `window.index == window.window_index` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_index_308add77f4 | 3.2a through 3.7b | planned parity | -| src/libtmux/window.py::libtmux.window.Window.kill | execute `window_1 = session.new_window()` without exception; execute `window_1 in session.windows` and expect `True`; execute `window_1.kill()` without exception; execute `window_1 not in session.windows` and expect `True`; execute `one_window_to_rule_them_all = session.new_window()` without exception; execute `other_windows = session.new_window( ), session.new_window()` without exception; execute `all([w in session.windows for w in other_windows])` and expect `True`; execute `one_window_to_rule_them_all.kill(all_except=True)` without exception; execute `all([w not in session.windows for w in other_windows])` and expect `True`; execute `one_window_to_rule_them_all in session.windows` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_kill_3a41259e19 | 3.2a through 3.7b | planned parity | -| src/libtmux/window.py::libtmux.window.Window.last_pane | execute `pane1 = window.active_pane` without exception; execute `pane2 = window.split()` without exception; execute `pane2.select()` and expect `Pane(...)`; execute `result = window.last_pane()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_last_pane_858a47cafb | 3.2a through 3.7b | planned parity | -| src/libtmux/window.py::libtmux.window.Window.link | execute `w = session.new_window(window_name='link_test')` without exception; execute `s2 = server.new_session(session_name='link_target')` without exception; execute `w.link(s2)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_link_15453f5dea | 3.2a through 3.7b | planned parity | -| src/libtmux/window.py::libtmux.window.Window.linked_sessions | execute `window = session.new_window(window_name="solo", attach=False)` without exception; execute `[s.session_name for s in window.linked_sessions] == [session.session_name]` and expect `True`; execute `guest = server.new_session(session_name="guest")` without exception; execute `target = f"{guest.session_id}:"` without exception; execute `_ = server.cmd("link-window", "-d", "-s", window.window_id, "-t", target)` without exception; execute `sorted(s.session_name for s in window.linked_sessions) == sorted( [session.session_name, "guest"] )` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_linked_sessions_a1cc1102f9 | 3.2a through 3.7b | planned parity | -| src/libtmux/window.py::libtmux.window.Window.name | execute `window.name` and expect `'...'`; execute `window.name == window.window_name` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_name_57ed13f1f9 | 3.2a through 3.7b | planned parity | -| src/libtmux/window.py::libtmux.window.Window.new_pane | execute `from libtmux.common import has_gte_version` without exception; execute `if has_gte_version("3.7"): floating = window.new_pane(width=40, height=10, shell="sleep 5") is_floating = floating.pane_floating_flag else: is_floating = "1"` without exception; execute `is_floating` and expect `'1'`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_new_pane_f7ca47ae60 | 3.2a through 3.7b | planned parity | -| src/libtmux/window.py::libtmux.window.Window.new_window | execute `window_initial = session.new_window(window_name='Example')` without exception; execute `window_initial` and expect `Window(@... 2:Example, Session($1 libtmux_...))`; execute `window_initial.window_index` and expect `'2'`; execute `window_before = window_initial.new_window( window_name='Window before', direction=WindowDirection.Before)` without exception; execute `window_initial.refresh()` without exception; execute `window_before` and expect `Window(@... 2:Window before, Session($1 libtmux_...))`; execute `window_initial` and expect `Window(@... 3:Example, Session($1 libtmux_...))`; execute `window_after = window_initial.new_window( window_name='Window after', direction=WindowDirection.After)` without exception; execute `window_initial.refresh()` without exception; execute `window_after.refresh()` without exception; execute `window_after` and expect `Window(@... 4:Window after, Session($1 libtmux_...))`; execute `window_initial` and expect `Window(@... 3:Example, Session($1 libtmux_...))`; execute `window_before` and expect `Window(@... 2:Window before, Session($1 libtmux_...))`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_new_window_4e77c412b9 | 3.2a through 3.7b | planned parity | -| src/libtmux/window.py::libtmux.window.Window.next_layout | execute `pane1 = window.active_pane` without exception; execute `pane2 = window.split()` without exception; execute `window.next_layout()` and expect `Window(...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_next_layout_63cca8ed9e | 3.2a through 3.7b | planned parity | -| src/libtmux/window.py::libtmux.window.Window.previous_layout | execute `pane1 = window.active_pane` without exception; execute `pane2 = window.split()` without exception; execute `window.previous_layout()` and expect `Window(...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_previous_layout_fa635c6fff | 3.2a through 3.7b | planned parity | -| src/libtmux/window.py::libtmux.window.Window.refresh | execute `window.refresh()` without exception; execute `window.window_id` and expect `'@1'`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_refresh_52082da539 | 3.2a through 3.7b | planned parity | -| src/libtmux/window.py::libtmux.window.Window.rename_window | execute `window = session.active_window` without exception; execute `window.rename_window('My project')` and expect `Window(@1 1:My project, Session($1 ...))`; execute `window.rename_window('New name')` and expect `Window(@1 1:New name, Session($1 ...))`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_rename_window_5a09364ebd | 3.2a through 3.7b | planned parity | -| src/libtmux/window.py::libtmux.window.Window.respawn | execute `window = session.new_window(window_name='respawn_test')` without exception; execute `window.respawn(kill=True, shell='sh')` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_respawn_b6f1560fe2 | 3.2a through 3.7b | planned parity | -| src/libtmux/window.py::libtmux.window.Window.rotate | execute `pane1 = window.active_pane` without exception; execute `pane2 = window.split()` without exception; execute `window.rotate()` and expect `Window(...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_rotate_ddf3350b16 | 3.2a through 3.7b | planned parity | -| src/libtmux/window.py::libtmux.window.Window.search_panes | execute `target_pane = window.split()` without exception; execute `matches = window.search_panes( filter=f'#{{m:{target_pane.pane_id},#{{pane_id}}}}' )` without exception; execute `[p.pane_id for p in matches] == [target_pane.pane_id]` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_search_panes_8a2e34eaaa | 3.2a through 3.7b | planned parity | -| src/libtmux/window.py::libtmux.window.Window.select | execute `window = session.active_window` without exception; execute `new_window = session.new_window()` without exception; execute `session.refresh()` without exception; execute `active_windows = [w for w in session.windows if w.window_active == '1']` without exception; execute `new_window.window_active == '1'` and expect `False`; execute `new_window.select()` and expect `Window(...)`; execute `new_window.window_active == '1'` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_select_95f3054322 | 3.2a through 3.7b | planned parity | -| src/libtmux/window.py::libtmux.window.Window.swap | execute `w1 = session.new_window(window_name='swap_a')` without exception; execute `w2 = session.new_window(window_name='swap_b')` without exception; execute `w1_idx = w1.window_index` without exception; execute `w2_idx = w2.window_index` without exception; execute `w1.swap(w2)` without exception; execute `w1.window_index == w2_idx` and expect `True`; execute `w2.window_index == w1_idx` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_swap_80dddba00b | 3.2a through 3.7b | planned parity | -| src/libtmux/window.py::libtmux.window.Window.unlink | execute `w = session.new_window(window_name='unlink_test')` without exception; execute `s2 = server.new_session(session_name='unlink_s2')` without exception; execute `w.link(s2)` without exception; execute `linked = [x for x in s2.windows if x.window_name == 'unlink_test']` without exception; execute `linked[0].unlink()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_unlink_7d34a50a88 | 3.2a through 3.7b | planned parity | -| src/libtmux/window.py::libtmux.window.Window.width | execute `window.width.isdigit()` and expect `True`; execute `window.width == window.window_width` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_width_afa998b606 | 3.2a through 3.7b | planned parity | +| src/libtmux/test/random.py::libtmux.test.random.get_test_session_name | execute `get_test_session_name(server=server) # pragma: no cover` and expect `'libtmux_...'`; execute `name1 = get_test_session_name(server=server) # pragma: no cover` without exception; execute `name2 = get_test_session_name(server=server) # pragma: no cover` without exception; execute `name1 != name2 # pragma: no cover` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_test_random_py_libtmux_test_random_get_test_session_name_73d632a4e5 | 3.3a through 3.7c | planned parity | +| src/libtmux/test/random.py::libtmux.test.random.get_test_window_name | execute `get_test_window_name(session=session) # pragma: no cover` and expect `'libtmux_...'`; execute `name1 = get_test_window_name(session=session) # pragma: no cover` without exception; execute `name2 = get_test_window_name(session=session) # pragma: no cover` without exception; execute `name1 != name2 # pragma: no cover` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_test_random_py_libtmux_test_random_get_test_window_name_bd6e68b630 | 3.3a through 3.7c | planned parity | +| src/libtmux/test/retry.py::libtmux.test.retry.retry_until | execute `def fn(): p = session.active_window.active_pane return p.pane_current_path is not None` without exception; execute `retry_until(fn)` and expect `True`; execute `assert retry_until(fn, raises=False)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_test_retry_py_libtmux_test_retry_retry_until_9aca6b0f7a | 3.3a through 3.7c | planned parity | +| src/libtmux/test/temporary.py::libtmux.test.temporary.temp_session | execute `with temp_session(server) as session: session.new_window(window_name='my window')` and expect `Window(@3 2:my window, Session($... ...))`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_test_temporary_py_libtmux_test_temporary_temp_session_8f0f5e105b | 3.3a through 3.7c | planned parity | +| src/libtmux/test/temporary.py::libtmux.test.temporary.temp_window | execute `with temp_window(session) as window: window` and expect `Window(@2 2:... Session($1 libtmux_...))`; execute `with temp_window(session) as window: window.split()` and expect `Pane(%4 Window(@3 2:libtmux_..., Session($1 libtmux_...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_test_temporary_py_libtmux_test_temporary_temp_window_e4d8c7c838 | 3.3a through 3.7c | planned parity | +| src/libtmux/window.py::libtmux.window.Window | execute `window = session.new_window('My project', attach=True)` without exception; execute `window` and expect `Window(@2 2:My project, Session($... ...))`; execute `window.panes` and expect `[Pane(...)]`; execute `window.active_pane` and expect `Pane(...)`; execute `window.session` and expect `Session(...)`; execute `window.window_id == session.active_window.window_id` and expect `True`; execute `window == session.active_window` and expect `True`; execute `window in session.windows` and expect `True`; execute `with session.new_window() as window: pane = window.split() # Do work with the pane # Window will be killed automatically when exiting the context` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_55c6fc7924 | 3.3a through 3.7c | planned parity | +| src/libtmux/window.py::libtmux.window.Window.cmd | execute `window.cmd('split-window', '-P', '-F#{pane_id}').stdout[0]` and expect `'%...'`; execute `Pane.from_pane_id(pane_id=session.cmd( 'split-window', '-P', '-F#{pane_id}').stdout[0], server=session.server)` and expect `Pane(%... Window(@... ...:..., Session($1 libtmux_...)))`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_cmd_a1c9fd2353 | 3.3a through 3.7c | planned parity | +| src/libtmux/window.py::libtmux.window.Window.display_message | execute `result = window.display_message("#{window_id}", get_text=True)` without exception; execute `result[0].startswith("@")` and expect `True`; execute `result = window.display_message( "#{window_zoomed_flag}", get_text=True )` without exception; execute `result[0] in {"0", "1"}` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_display_message_7bbef1fecf | 3.3a through 3.7c | planned parity | +| src/libtmux/window.py::libtmux.window.Window.from_env | execute `socket_path = server.cmd( # doctest: +HIDE "display-message", "-p", "-t", pane.pane_id, "#{socket_path}" ).stdout[0]` with doctest options `+ALLOW_UNICODE` without exception; execute `env = { # doctest: +HIDE "TMUX": f"{socket_path},1,0", "TMUX_PANE": pane.pane_id, }` with doctest options `+ALLOW_UNICODE` without exception; execute `Window.from_env(env)` and expect `Window(@1 1:..., Session($1 ...))`; execute `_ = session.new_window(window_name="foreground", attach=True)` without exception; execute `Window.from_env(env).window_id == window.window_id` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_from_env_d2f7ecc8c4 | 3.3a through 3.7c | planned parity | +| src/libtmux/window.py::libtmux.window.Window.from_window_id | execute `Window.from_window_id(server=window.server, window_id=window.window_id)` and expect `Window(@1 1:..., Session($1 ...))`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_from_window_id_6b5204f062 | 3.3a through 3.7c | planned parity | +| src/libtmux/window.py::libtmux.window.Window.height | execute `window.height.isdigit()` and expect `True`; execute `window.height == window.window_height` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_height_850114ffbe | 3.3a through 3.7c | planned parity | +| src/libtmux/window.py::libtmux.window.Window.id | execute `window.id` and expect `'@1'`; execute `window.id == window.window_id` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_id_50e7f4b6ad | 3.3a through 3.7c | planned parity | +| src/libtmux/window.py::libtmux.window.Window.index | execute `window.index` and expect `'1'`; execute `window.index == window.window_index` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_index_308add77f4 | 3.3a through 3.7c | planned parity | +| src/libtmux/window.py::libtmux.window.Window.kill | execute `window_1 = session.new_window()` without exception; execute `window_1 in session.windows` and expect `True`; execute `window_1.kill()` without exception; execute `window_1 not in session.windows` and expect `True`; execute `one_window_to_rule_them_all = session.new_window()` without exception; execute `other_windows = session.new_window( ), session.new_window()` without exception; execute `all([w in session.windows for w in other_windows])` and expect `True`; execute `one_window_to_rule_them_all.kill(all_except=True)` without exception; execute `all([w not in session.windows for w in other_windows])` and expect `True`; execute `one_window_to_rule_them_all in session.windows` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_kill_3a41259e19 | 3.3a through 3.7c | planned parity | +| src/libtmux/window.py::libtmux.window.Window.last_pane | execute `pane1 = window.active_pane` without exception; execute `pane2 = window.split()` without exception; execute `pane2.select()` and expect `Pane(...)`; execute `result = window.last_pane()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_last_pane_858a47cafb | 3.3a through 3.7c | planned parity | +| src/libtmux/window.py::libtmux.window.Window.link | execute `w = session.new_window(window_name='link_test')` without exception; execute `s2 = server.new_session(session_name='link_target')` without exception; execute `w.link(s2)` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_link_15453f5dea | 3.3a through 3.7c | planned parity | +| src/libtmux/window.py::libtmux.window.Window.linked_sessions | execute `window = session.new_window(window_name="solo", attach=False)` without exception; execute `[s.session_name for s in window.linked_sessions] == [session.session_name]` and expect `True`; execute `guest = server.new_session(session_name="guest")` without exception; execute `target = f"{guest.session_id}:"` without exception; execute `_ = server.cmd("link-window", "-d", "-s", window.window_id, "-t", target)` without exception; execute `sorted(s.session_name for s in window.linked_sessions) == sorted( [session.session_name, "guest"] )` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_linked_sessions_a1cc1102f9 | 3.3a through 3.7c | planned parity | +| src/libtmux/window.py::libtmux.window.Window.name | execute `window.name` and expect `'...'`; execute `window.name == window.window_name` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_name_57ed13f1f9 | 3.3a through 3.7c | planned parity | +| src/libtmux/window.py::libtmux.window.Window.new_pane | execute `from libtmux.common import has_gte_version` without exception; execute `if has_gte_version("3.7"): floating = window.new_pane(width=40, height=10, shell="sleep 5") is_floating = floating.pane_floating_flag else: is_floating = "1"` without exception; execute `is_floating` and expect `'1'`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_new_pane_f7ca47ae60 | 3.3a through 3.7c | planned parity | +| src/libtmux/window.py::libtmux.window.Window.new_window | execute `window_initial = session.new_window(window_name='Example')` without exception; execute `window_initial` and expect `Window(@... 2:Example, Session($1 libtmux_...))`; execute `window_initial.window_index` and expect `'2'`; execute `window_before = window_initial.new_window( window_name='Window before', direction=WindowDirection.Before)` without exception; execute `window_initial.refresh()` without exception; execute `window_before` and expect `Window(@... 2:Window before, Session($1 libtmux_...))`; execute `window_initial` and expect `Window(@... 3:Example, Session($1 libtmux_...))`; execute `window_after = window_initial.new_window( window_name='Window after', direction=WindowDirection.After)` without exception; execute `window_initial.refresh()` without exception; execute `window_after.refresh()` without exception; execute `window_after` and expect `Window(@... 4:Window after, Session($1 libtmux_...))`; execute `window_initial` and expect `Window(@... 3:Example, Session($1 libtmux_...))`; execute `window_before` and expect `Window(@... 2:Window before, Session($1 libtmux_...))`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_new_window_4e77c412b9 | 3.3a through 3.7c | planned parity | +| src/libtmux/window.py::libtmux.window.Window.next_layout | execute `pane1 = window.active_pane` without exception; execute `pane2 = window.split()` without exception; execute `window.next_layout()` and expect `Window(...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_next_layout_63cca8ed9e | 3.3a through 3.7c | planned parity | +| src/libtmux/window.py::libtmux.window.Window.previous_layout | execute `pane1 = window.active_pane` without exception; execute `pane2 = window.split()` without exception; execute `window.previous_layout()` and expect `Window(...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_previous_layout_fa635c6fff | 3.3a through 3.7c | planned parity | +| src/libtmux/window.py::libtmux.window.Window.refresh | execute `window.refresh()` without exception; execute `window.window_id` and expect `'@1'`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_refresh_52082da539 | 3.3a through 3.7c | planned parity | +| src/libtmux/window.py::libtmux.window.Window.rename_window | execute `window = session.active_window` without exception; execute `window.rename_window('My project')` and expect `Window(@1 1:My project, Session($1 ...))`; execute `window.rename_window('New name')` and expect `Window(@1 1:New name, Session($1 ...))`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_rename_window_5a09364ebd | 3.3a through 3.7c | planned parity | +| src/libtmux/window.py::libtmux.window.Window.respawn | execute `window = session.new_window(window_name='respawn_test')` without exception; execute `window.respawn(kill=True, shell='sh')` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_respawn_b6f1560fe2 | 3.3a through 3.7c | planned parity | +| src/libtmux/window.py::libtmux.window.Window.rotate | execute `pane1 = window.active_pane` without exception; execute `pane2 = window.split()` without exception; execute `window.rotate()` and expect `Window(...)`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_rotate_ddf3350b16 | 3.3a through 3.7c | planned parity | +| src/libtmux/window.py::libtmux.window.Window.search_panes | execute `target_pane = window.split()` without exception; execute `matches = window.search_panes( filter=f'#{{m:{target_pane.pane_id},#{{pane_id}}}}' )` without exception; execute `[p.pane_id for p in matches] == [target_pane.pane_id]` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_search_panes_8a2e34eaaa | 3.3a through 3.7c | planned parity | +| src/libtmux/window.py::libtmux.window.Window.select | execute `window = session.active_window` without exception; execute `new_window = session.new_window()` without exception; execute `session.refresh()` without exception; execute `active_windows = [w for w in session.windows if w.window_active == '1']` without exception; execute `new_window.window_active == '1'` and expect `False`; execute `new_window.select()` and expect `Window(...)`; execute `new_window.window_active == '1'` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_select_95f3054322 | 3.3a through 3.7c | planned parity | +| src/libtmux/window.py::libtmux.window.Window.swap | execute `w1 = session.new_window(window_name='swap_a')` without exception; execute `w2 = session.new_window(window_name='swap_b')` without exception; execute `w1_idx = w1.window_index` without exception; execute `w2_idx = w2.window_index` without exception; execute `w1.swap(w2)` without exception; execute `w1.window_index == w2_idx` and expect `True`; execute `w2.window_index == w1_idx` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_swap_80dddba00b | 3.3a through 3.7c | planned parity | +| src/libtmux/window.py::libtmux.window.Window.unlink | execute `w = session.new_window(window_name='unlink_test')` without exception; execute `s2 = server.new_session(session_name='unlink_s2')` without exception; execute `w.link(s2)` without exception; execute `linked = [x for x in s2.windows if x.window_name == 'unlink_test']` without exception; execute `linked[0].unlink()` without exception. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_unlink_7d34a50a88 | 3.3a through 3.7c | planned parity | +| src/libtmux/window.py::libtmux.window.Window.width | execute `window.width.isdigit()` and expect `True`; execute `window.width == window.window_width` and expect `True`. | real-tmux documentation contract test | PythonBehaviorParityTest#src_libtmux_window_py_libtmux_window_Window_width_afa998b606 | 3.3a through 3.7c | planned parity | | tests/_internal/test_query_list.py::test_filter[items0-None-expected_result0] | case inputs `expected_result=[Obj(test=1, fruit=[])]`, `filter_expr=None`, `items=[Obj(test=1, fruit=[])]`; execute `QueryList(items)`; when `filter_expr is not None and isinstance(filter_expr, dict)`: execute `qs.filter(**filter_expr)`; assert `qs.filter(**filter_expr) == expected_result`; when `filter_expr is not None and not (isinstance(filter_expr, dict))`: execute `qs.filter(filter_expr)`; assert `qs.filter(filter_expr) == expected_result`; when `not (filter_expr is not None)`: execute `qs.filter()`; assert `qs.filter() == expected_result`; when `len(expected_result) == 0`: execute `missing.match('No objects found')`; expect `pytest.raises(ObjectDoesNotExist)`; assert `missing.match('No objects found')`; when `len(expected_result) == 1 and isinstance(filter_expr, dict)`: assert `qs.get(**filter_expr) == expected_result[0]`; when `len(expected_result) == 1 and not (isinstance(filter_expr, dict))`: assert `qs.get(filter_expr) == expected_result[0]`; when `not (len(expected_result) == 1)`: execute `multiple.match('Multiple objects returned')`; expect `pytest.raises(MultipleObjectsReturned)`; assert `multiple.match('Multiple objects returned')`. | unit test | PythonBehaviorParityTest#sts_internal_test_query_list_py_test_filter_items0_None_expected_result0_53c1675603 | not tmux-bound | planned parity | | tests/_internal/test_query_list.py::test_filter[items1-filter_expr1-expected_result1] | case inputs `expected_result=[Obj(test=1, fruit=[])]`, `filter_expr={'test': 1}`, `items=[Obj(test=1, fruit=[])]`; execute `QueryList(items)`; when `filter_expr is not None and isinstance(filter_expr, dict)`: execute `qs.filter(**filter_expr)`; assert `qs.filter(**filter_expr) == expected_result`; when `filter_expr is not None and not (isinstance(filter_expr, dict))`: execute `qs.filter(filter_expr)`; assert `qs.filter(filter_expr) == expected_result`; when `not (filter_expr is not None)`: execute `qs.filter()`; assert `qs.filter() == expected_result`; when `len(expected_result) == 0`: execute `missing.match('No objects found')`; expect `pytest.raises(ObjectDoesNotExist)`; assert `missing.match('No objects found')`; when `len(expected_result) == 1 and isinstance(filter_expr, dict)`: assert `qs.get(**filter_expr) == expected_result[0]`; when `len(expected_result) == 1 and not (isinstance(filter_expr, dict))`: assert `qs.get(filter_expr) == expected_result[0]`; when `not (len(expected_result) == 1)`: execute `multiple.match('Multiple objects returned')`; expect `pytest.raises(MultipleObjectsReturned)`; assert `multiple.match('Multiple objects returned')`. | unit test | PythonBehaviorParityTest#rnal_test_query_list_py_test_filter_items1_filter_expr1_expected_result1_7f67f6fa31 | not tmux-bound | planned parity | | tests/_internal/test_query_list.py::test_filter[items10-filter_expr10-expected_result10] | case inputs `expected_result=[{'fruit': 'apple', 'banana': <object object at 0x...>}]`, `filter_expr={'fruit__exact': 'apple'}`, `items=[{'fruit': 'apple', 'banana': <object object at 0x...>}]`; execute `QueryList(items)`; when `filter_expr is not None and isinstance(filter_expr, dict)`: execute `qs.filter(**filter_expr)`; assert `qs.filter(**filter_expr) == expected_result`; when `filter_expr is not None and not (isinstance(filter_expr, dict))`: execute `qs.filter(filter_expr)`; assert `qs.filter(filter_expr) == expected_result`; when `not (filter_expr is not None)`: execute `qs.filter()`; assert `qs.filter() == expected_result`; when `len(expected_result) == 0`: execute `missing.match('No objects found')`; expect `pytest.raises(ObjectDoesNotExist)`; assert `missing.match('No objects found')`; when `len(expected_result) == 1 and isinstance(filter_expr, dict)`: assert `qs.get(**filter_expr) == expected_result[0]`; when `len(expected_result) == 1 and not (isinstance(filter_expr, dict))`: assert `qs.get(filter_expr) == expected_result[0]`; when `not (len(expected_result) == 1)`: execute `multiple.match('Multiple objects returned')`; expect `pytest.raises(MultipleObjectsReturned)`; assert `multiple.match('Multiple objects returned')`. | unit test | PythonBehaviorParityTest#l_test_query_list_py_test_filter_items10_filter_expr10_expected_result10_fd80c52077 | not tmux-bound | planned parity | @@ -496,53 +496,53 @@ under [`tests/`](https://github.com/tmux-python/libtmux/blob/c4a980b/tests) and | tests/legacy_api/test_common.py::test_has_lte_version | Test has_lti_version(); execute `has_lte_version('4.0a')`; `has_lte_version('4.0')`; `has_lte_version(str(get_version()))`; `get_version()`; `has_lte_version('1.7')`; `has_lte_version('1.7b')`; assert `has_lte_version('4.0a')`; `has_lte_version('4.0')`; `has_lte_version(str(get_version()))`; `not has_lte_version('1.7')`; `not has_lte_version('1.7b')`. | unit test | PythonBehaviorParityTest#tests_legacy_api_test_common_py_test_has_lte_version_914f2bfbb3 | not tmux-bound | planned parity | | tests/legacy_api/test_common.py::test_has_version | Test has_version(); execute `has_version(str(get_version()))`; `get_version()`; assert `has_version(str(get_version()))`. | unit test | PythonBehaviorParityTest#tests_legacy_api_test_common_py_test_has_version_4cd963b9e1 | not tmux-bound | planned parity | | tests/legacy_api/test_common.py::test_ignores_letter_versions | Tests version utilities ignores letters such as 1.8b; fixtures `monkeypatch`; execute `has_minimum_version()`; `has_version('1.8')`; `has_version('1.8a')`; `has_version('1.9a')`; assert `result`; `isinstance(has_version('1.8'), bool)`; `isinstance(has_version('1.8a'), bool)`; `isinstance(has_version('1.9a'), bool)`. | unit test | PythonBehaviorParityTest#tests_legacy_api_test_common_py_test_ignores_letter_versions_fdc1319d63 | not tmux-bound | planned parity | -| tests/legacy_api/test_common.py::test_session_check_name[-True-empty] | Verify session_check_name(); case inputs `exc_msg_regex='empty'`, `raises=True`, `session_name=''`; when `raises`: execute `session_check_name(session_name)`; expect `pytest.raises(exc.BadSessionName)`; when `raises and exc_msg_regex is not None`: execute `exc_info.match(exc_msg_regex)`; assert `exc_info.match(exc_msg_regex)`; when `not (raises)`: execute `session_check_name(session_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_legacy_api_test_common_py_test_session_check_name_True_empty_9a40f61272 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_common.py::test_session_check_name[None-True-empty] | Verify session_check_name(); case inputs `exc_msg_regex='empty'`, `raises=True`, `session_name=None`; when `raises`: execute `session_check_name(session_name)`; expect `pytest.raises(exc.BadSessionName)`; when `raises and exc_msg_regex is not None`: execute `exc_info.match(exc_msg_regex)`; assert `exc_info.match(exc_msg_regex)`; when `not (raises)`: execute `session_check_name(session_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_legacy_api_test_common_py_test_session_check_name_None_True_empty_f79de29799 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_common.py::test_session_check_name[ajf8a3fa83fads,,,a-False-None] | Verify session_check_name(); case inputs `exc_msg_regex=None`, `raises=False`, `session_name='ajf8a3fa83fads,,,a'`; when `raises`: execute `session_check_name(session_name)`; expect `pytest.raises(exc.BadSessionName)`; when `raises and exc_msg_regex is not None`: execute `exc_info.match(exc_msg_regex)`; assert `exc_info.match(exc_msg_regex)`; when `not (raises)`: execute `session_check_name(session_name)`. | real-tmux integration test | PythonBehaviorParityTest#y_api_test_common_py_test_session_check_name_ajf8a3fa83fads_a_False_None_6679721f6d | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_common.py::test_session_check_name[my great session.-True-contains periods] | Verify session_check_name(); case inputs `exc_msg_regex='contains periods'`, `raises=True`, `session_name='my great session.'`; when `raises`: execute `session_check_name(session_name)`; expect `pytest.raises(exc.BadSessionName)`; when `raises and exc_msg_regex is not None`: execute `exc_info.match(exc_msg_regex)`; assert `exc_info.match(exc_msg_regex)`; when `not (raises)`: execute `session_check_name(session_name)`. | real-tmux integration test | PythonBehaviorParityTest#common_py_test_session_check_name_my_great_session_True_contains_periods_91c14cb936 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_common.py::test_session_check_name[name: great session-True-contains colons] | Verify session_check_name(); case inputs `exc_msg_regex='contains colons'`, `raises=True`, `session_name='name: great session'`; when `raises`: execute `session_check_name(session_name)`; expect `pytest.raises(exc.BadSessionName)`; when `raises and exc_msg_regex is not None`: execute `exc_info.match(exc_msg_regex)`; assert `exc_info.match(exc_msg_regex)`; when `not (raises)`: execute `session_check_name(session_name)`. | real-tmux integration test | PythonBehaviorParityTest#ommon_py_test_session_check_name_name_great_session_True_contains_colons_f9f12dc54c | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_common.py::test_session_check_name[new great session-False-None] | Verify session_check_name(); case inputs `exc_msg_regex=None`, `raises=False`, `session_name='new great session'`; when `raises`: execute `session_check_name(session_name)`; expect `pytest.raises(exc.BadSessionName)`; when `raises and exc_msg_regex is not None`: execute `exc_info.match(exc_msg_regex)`; assert `exc_info.match(exc_msg_regex)`; when `not (raises)`: execute `session_check_name(session_name)`. | real-tmux integration test | PythonBehaviorParityTest#_api_test_common_py_test_session_check_name_new_great_session_False_None_49fd9371c8 | 3.2a through 3.7b | planned parity | +| tests/legacy_api/test_common.py::test_session_check_name[-True-empty] | Verify session_check_name(); case inputs `exc_msg_regex='empty'`, `raises=True`, `session_name=''`; when `raises`: execute `session_check_name(session_name)`; expect `pytest.raises(exc.BadSessionName)`; when `raises and exc_msg_regex is not None`: execute `exc_info.match(exc_msg_regex)`; assert `exc_info.match(exc_msg_regex)`; when `not (raises)`: execute `session_check_name(session_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_legacy_api_test_common_py_test_session_check_name_True_empty_9a40f61272 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_common.py::test_session_check_name[None-True-empty] | Verify session_check_name(); case inputs `exc_msg_regex='empty'`, `raises=True`, `session_name=None`; when `raises`: execute `session_check_name(session_name)`; expect `pytest.raises(exc.BadSessionName)`; when `raises and exc_msg_regex is not None`: execute `exc_info.match(exc_msg_regex)`; assert `exc_info.match(exc_msg_regex)`; when `not (raises)`: execute `session_check_name(session_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_legacy_api_test_common_py_test_session_check_name_None_True_empty_f79de29799 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_common.py::test_session_check_name[ajf8a3fa83fads,,,a-False-None] | Verify session_check_name(); case inputs `exc_msg_regex=None`, `raises=False`, `session_name='ajf8a3fa83fads,,,a'`; when `raises`: execute `session_check_name(session_name)`; expect `pytest.raises(exc.BadSessionName)`; when `raises and exc_msg_regex is not None`: execute `exc_info.match(exc_msg_regex)`; assert `exc_info.match(exc_msg_regex)`; when `not (raises)`: execute `session_check_name(session_name)`. | real-tmux integration test | PythonBehaviorParityTest#y_api_test_common_py_test_session_check_name_ajf8a3fa83fads_a_False_None_6679721f6d | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_common.py::test_session_check_name[my great session.-True-contains periods] | Verify session_check_name(); case inputs `exc_msg_regex='contains periods'`, `raises=True`, `session_name='my great session.'`; when `raises`: execute `session_check_name(session_name)`; expect `pytest.raises(exc.BadSessionName)`; when `raises and exc_msg_regex is not None`: execute `exc_info.match(exc_msg_regex)`; assert `exc_info.match(exc_msg_regex)`; when `not (raises)`: execute `session_check_name(session_name)`. | real-tmux integration test | PythonBehaviorParityTest#common_py_test_session_check_name_my_great_session_True_contains_periods_91c14cb936 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_common.py::test_session_check_name[name: great session-True-contains colons] | Verify session_check_name(); case inputs `exc_msg_regex='contains colons'`, `raises=True`, `session_name='name: great session'`; when `raises`: execute `session_check_name(session_name)`; expect `pytest.raises(exc.BadSessionName)`; when `raises and exc_msg_regex is not None`: execute `exc_info.match(exc_msg_regex)`; assert `exc_info.match(exc_msg_regex)`; when `not (raises)`: execute `session_check_name(session_name)`. | real-tmux integration test | PythonBehaviorParityTest#ommon_py_test_session_check_name_name_great_session_True_contains_colons_f9f12dc54c | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_common.py::test_session_check_name[new great session-False-None] | Verify session_check_name(); case inputs `exc_msg_regex=None`, `raises=False`, `session_name='new great session'`; when `raises`: execute `session_check_name(session_name)`; expect `pytest.raises(exc.BadSessionName)`; when `raises and exc_msg_regex is not None`: execute `exc_info.match(exc_msg_regex)`; assert `exc_info.match(exc_msg_regex)`; when `not (raises)`: execute `session_check_name(session_name)`. | real-tmux integration test | PythonBehaviorParityTest#_api_test_common_py_test_session_check_name_new_great_session_False_None_49fd9371c8 | 3.3a through 3.7c | planned parity | | tests/legacy_api/test_common.py::test_tmux_cmd_raises_on_not_found | Verify raises if tmux command not found; fixtures `monkeypatch`; execute `tmux_cmd('-V')`; expect `pytest.raises(exc.TmuxCommandNotFound)`. | unit test | PythonBehaviorParityTest#tests_legacy_api_test_common_py_test_tmux_cmd_raises_on_not_found_7542dab548 | not tmux-bound | planned parity | -| tests/legacy_api/test_common.py::test_tmux_cmd_unicode | Verify tmux commands with unicode; fixtures `session`; execute `session.cmd('new-window', '-t', 3, '-n', 'юникод', '-F', 'Ελληνικά')`. | real-tmux integration test | PythonBehaviorParityTest#tests_legacy_api_test_common_py_test_tmux_cmd_unicode_7fa5256541 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_pane.py::test_pane_get_raises_deprecated_error | Test Pane.get() raises DeprecatedError; fixtures `session`; expect `pytest.raises(exc.DeprecatedError, match='Pane\\.get\\(\\) was deprecated')`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_legacy_api_test_pane_py_test_pane_get_raises_deprecated_error_c685b5f026 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_pane.py::test_pane_getitem_raises_deprecated_error | Test Pane.__getitem__() raises DeprecatedError; fixtures `session`; expect `pytest.raises(exc.DeprecatedError, match='Pane\\[key\\] lookup was deprecated')`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_legacy_api_test_pane_py_test_pane_getitem_raises_deprecated_error_5db8171483 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_pane.py::test_resize_pane_raises_deprecated_error | Test Pane.resize_pane() raises DeprecatedError; fixtures `session`; execute `pane.resize_pane(height=4)`; expect `pytest.raises(exc.DeprecatedError, match='Pane\\.resize_pane\\(\\) was deprecated')`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_legacy_api_test_pane_py_test_resize_pane_raises_deprecated_error_db2eda8365 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_pane.py::test_select_pane_raises_deprecated_error | Test Pane.select_pane() raises DeprecatedError; fixtures `session`; execute `pane.select_pane()`; expect `pytest.raises(exc.DeprecatedError, match='Pane\\.select_pane\\(\\) was deprecated')`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_legacy_api_test_pane_py_test_select_pane_raises_deprecated_error_fba5bc5886 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_pane.py::test_split_window_raises_deprecated_error | Test Pane.split_window() raises DeprecatedError; fixtures `session`; execute `pane.split_window()`; expect `pytest.raises(exc.DeprecatedError, match='Pane\\.split_window\\(\\) was deprecated')`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_legacy_api_test_pane_py_test_split_window_raises_deprecated_error_1aa48b9172 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_server.py::test_kill_server_raises_deprecated_error | Test Server.kill_server() raises exc.DeprecatedError; fixtures `server`; execute `server.kill_server()`; expect `pytest.raises(exc.DeprecatedError, match='Server\\.kill_server\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#tests_legacy_api_test_server_py_test_kill_server_raises_deprecated_error_83f9f3f7a0 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_server.py::test_server__list_panes_raises_deprecated_error | Test Server._list_panes() raises exc.DeprecatedError; fixtures `server`; execute `server._list_panes()`; expect `pytest.raises(exc.DeprecatedError, match='Server\\._list_panes\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#legacy_api_test_server_py_test_server_list_panes_raises_deprecated_error_92490325b3 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_server.py::test_server__list_sessions_raises_deprecated_error | Test Server._list_sessions() raises exc.DeprecatedError; fixtures `server`; execute `server._list_sessions()`; expect `pytest.raises(exc.DeprecatedError, match='Server\\._list_sessions\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#acy_api_test_server_py_test_server_list_sessions_raises_deprecated_error_100f982346 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_server.py::test_server__list_windows_raises_deprecated_error | Test Server._list_windows() raises exc.DeprecatedError; fixtures `server`; execute `server._list_windows()`; expect `pytest.raises(exc.DeprecatedError, match='Server\\._list_windows\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#gacy_api_test_server_py_test_server_list_windows_raises_deprecated_error_6dda9a1130 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_server.py::test_server__sessions_raises_deprecated_error | Test Server._sessions raises exc.DeprecatedError; fixtures `server`; expect `pytest.raises(exc.DeprecatedError, match='Server\\._sessions was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#s_legacy_api_test_server_py_test_server_sessions_raises_deprecated_error_0b0467bd46 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_server.py::test_server__update_panes_raises_deprecated_error | Test Server._update_panes() raises exc.DeprecatedError; fixtures `server`; execute `server._update_panes()`; expect `pytest.raises(exc.DeprecatedError, match='Server\\._update_panes\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#gacy_api_test_server_py_test_server_update_panes_raises_deprecated_error_b138b79715 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_server.py::test_server__update_windows_raises_deprecated_error | Test Server._update_windows() raises exc.DeprecatedError; fixtures `server`; execute `server._update_windows()`; expect `pytest.raises(exc.DeprecatedError, match='Server\\._update_windows\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#cy_api_test_server_py_test_server_update_windows_raises_deprecated_error_60e6de39f5 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_server.py::test_server_children_raises_deprecated_error | Test Server.children raises exc.DeprecatedError; fixtures `server`; expect `pytest.raises(exc.DeprecatedError, match='Server\\.children was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#s_legacy_api_test_server_py_test_server_children_raises_deprecated_error_693a4e97c5 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_server.py::test_server_find_where_raises_deprecated_error | Test Server.find_where() raises exc.DeprecatedError; fixtures `server`; execute `server.find_where({'session_name': 'test'})`; expect `pytest.raises(exc.DeprecatedError, match='Server\\.find_where\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#legacy_api_test_server_py_test_server_find_where_raises_deprecated_error_5d45d1f23e | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_server.py::test_server_get_by_id_raises_deprecated_error | Test Server.get_by_id() raises exc.DeprecatedError; fixtures `server`; execute `server.get_by_id('$0')`; expect `pytest.raises(exc.DeprecatedError, match='Server\\.get_by_id\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#_legacy_api_test_server_py_test_server_get_by_id_raises_deprecated_error_93585abef2 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_server.py::test_server_list_sessions_raises_deprecated_error | Test Server.list_sessions() raises exc.DeprecatedError; fixtures `server`; execute `server.list_sessions()`; expect `pytest.raises(exc.DeprecatedError, match='Server\\.list_sessions\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#acy_api_test_server_py_test_server_list_sessions_raises_deprecated_error_3ea9a309a2 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_server.py::test_server_where_raises_deprecated_error | Test Server.where() raises exc.DeprecatedError; fixtures `server`; execute `server.where({'session_name': 'test'})`; expect `pytest.raises(exc.DeprecatedError, match='Server\\.where\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#ests_legacy_api_test_server_py_test_server_where_raises_deprecated_error_06a79f8f2e | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_session.py::test_attach_session_raises_deprecated_error | Test Session.attach_session() raises exc.DeprecatedError; fixtures `session`; execute `session.attach_session()`; expect `pytest.raises(exc.DeprecatedError, match='Session\\.attach_session\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#s_legacy_api_test_session_py_test_attach_session_raises_deprecated_error_eb4c32f1c6 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_session.py::test_attached_pane_raises_deprecated_error | Test Session.attached_pane raises exc.DeprecatedError; fixtures `session`; expect `pytest.raises(exc.DeprecatedError, match='Session\\.attached_pane was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#ts_legacy_api_test_session_py_test_attached_pane_raises_deprecated_error_92b3e0ed25 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_session.py::test_attached_window_raises_deprecated_error | Test Session.attached_window raises exc.DeprecatedError; fixtures `session`; expect `pytest.raises(exc.DeprecatedError, match='Session\\.attached_window was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#_legacy_api_test_session_py_test_attached_window_raises_deprecated_error_0e6f0927c5 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_session.py::test_kill_session_raises_deprecated_error | Test Session.kill_session() raises exc.DeprecatedError; fixtures `server`; execute `server.new_session(session_name='test_kill_session', detach=True)`; `new_session.kill_session()`; `new_session.kill()`; expect `pytest.raises(exc.DeprecatedError, match='Session\\.kill_session\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#sts_legacy_api_test_session_py_test_kill_session_raises_deprecated_error_bedfb1eae5 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_session.py::test_session__list_windows_raises_deprecated_error | Test Session._list_windows() raises exc.DeprecatedError; fixtures `session`; execute `session._list_windows()`; expect `pytest.raises(exc.DeprecatedError, match='Session\\._list_windows\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#cy_api_test_session_py_test_session_list_windows_raises_deprecated_error_fd0ed21403 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_session.py::test_session__windows_raises_deprecated_error | Test Session._windows raises exc.DeprecatedError; fixtures `session`; expect `pytest.raises(exc.DeprecatedError, match='Session\\._windows was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#_legacy_api_test_session_py_test_session_windows_raises_deprecated_error_b479441f8f | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_session.py::test_session_children_raises_deprecated_error | Test Session.children raises exc.DeprecatedError; fixtures `session`; expect `pytest.raises(exc.DeprecatedError, match='Session\\.children was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#legacy_api_test_session_py_test_session_children_raises_deprecated_error_7931b2ad06 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_session.py::test_session_find_where_raises_deprecated_error | Test Session.find_where() raises exc.DeprecatedError; fixtures `session`; execute `session.find_where({'window_name': 'test'})`; expect `pytest.raises(exc.DeprecatedError, match='Session\\.find_where\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#gacy_api_test_session_py_test_session_find_where_raises_deprecated_error_710a5495c0 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_session.py::test_session_get_by_id_raises_deprecated_error | Test Session.get_by_id() raises exc.DeprecatedError; fixtures `session`; execute `session.get_by_id('@0')`; expect `pytest.raises(exc.DeprecatedError, match='Session\\.get_by_id\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#egacy_api_test_session_py_test_session_get_by_id_raises_deprecated_error_817a478a6b | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_session.py::test_session_get_raises_deprecated_error | Test Session.get() raises exc.DeprecatedError; fixtures `session`; expect `pytest.raises(exc.DeprecatedError, match='Session\\.get\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#ests_legacy_api_test_session_py_test_session_get_raises_deprecated_error_a4c0492453 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_session.py::test_session_getitem_raises_deprecated_error | Test Session.__getitem__() raises exc.DeprecatedError; fixtures `session`; expect `pytest.raises(exc.DeprecatedError, match='Session\\[key\\] lookup was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#_legacy_api_test_session_py_test_session_getitem_raises_deprecated_error_2f10d5a199 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_session.py::test_session_list_windows_raises_deprecated_error | Test Session.list_windows() raises exc.DeprecatedError; fixtures `session`; execute `session.list_windows()`; expect `pytest.raises(exc.DeprecatedError, match='Session\\.list_windows\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#cy_api_test_session_py_test_session_list_windows_raises_deprecated_error_17a8f6dfb4 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_session.py::test_session_where_raises_deprecated_error | Test Session.where() raises exc.DeprecatedError; fixtures `session`; execute `session.where({'window_name': 'test'})`; expect `pytest.raises(exc.DeprecatedError, match='Session\\.where\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#ts_legacy_api_test_session_py_test_session_where_raises_deprecated_error_56f771dca4 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_tmuxobject.py::test_server_find_where_raises_deprecated_error | Test Server.find_where() raises exc.DeprecatedError; fixtures `server`; execute `server.find_where({'session_name': 'test'})`; expect `pytest.raises(exc.DeprecatedError, match='Server\\.find_where\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#cy_api_test_tmuxobject_py_test_server_find_where_raises_deprecated_error_c760af2b7f | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_tmuxobject.py::test_server_get_by_id_raises_deprecated_error | Test Server.get_by_id() raises exc.DeprecatedError; fixtures `server`; execute `server.get_by_id('$0')`; expect `pytest.raises(exc.DeprecatedError, match='Server\\.get_by_id\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#acy_api_test_tmuxobject_py_test_server_get_by_id_raises_deprecated_error_3832535554 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_tmuxobject.py::test_server_where_raises_deprecated_error | Test Server.where() raises exc.DeprecatedError; fixtures `server`; execute `server.where({'session_name': 'test'})`; expect `pytest.raises(exc.DeprecatedError, match='Server\\.where\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#_legacy_api_test_tmuxobject_py_test_server_where_raises_deprecated_error_eb953219b3 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_tmuxobject.py::test_session_find_where_raises_deprecated_error | Test Session.find_where() raises exc.DeprecatedError; fixtures `session`; execute `session.find_where({'window_name': 'test'})`; expect `pytest.raises(exc.DeprecatedError, match='Session\\.find_where\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#y_api_test_tmuxobject_py_test_session_find_where_raises_deprecated_error_8dfea1c4b0 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_tmuxobject.py::test_session_get_by_id_raises_deprecated_error | Test Session.get_by_id() raises exc.DeprecatedError; fixtures `session`; execute `session.get_by_id('@0')`; expect `pytest.raises(exc.DeprecatedError, match='Session\\.get_by_id\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#cy_api_test_tmuxobject_py_test_session_get_by_id_raises_deprecated_error_b641f63804 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_tmuxobject.py::test_session_where_raises_deprecated_error | Test Session.where() raises exc.DeprecatedError; fixtures `session`; execute `session.where({'window_name': 'test'})`; expect `pytest.raises(exc.DeprecatedError, match='Session\\.where\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#legacy_api_test_tmuxobject_py_test_session_where_raises_deprecated_error_e479b95b22 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_tmuxobject.py::test_window_find_where_raises_deprecated_error | Test Window.find_where() raises exc.DeprecatedError; fixtures `session`; execute `window.find_where({'pane_id': '%0'})`; expect `pytest.raises(exc.DeprecatedError, match='Window\\.find_where\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#cy_api_test_tmuxobject_py_test_window_find_where_raises_deprecated_error_644ad807b3 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_tmuxobject.py::test_window_get_by_id_raises_deprecated_error | Test Window.get_by_id() raises exc.DeprecatedError; fixtures `session`; execute `window.get_by_id('%0')`; expect `pytest.raises(exc.DeprecatedError, match='Window\\.get_by_id\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#acy_api_test_tmuxobject_py_test_window_get_by_id_raises_deprecated_error_e0cb1674f3 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_tmuxobject.py::test_window_where_raises_deprecated_error | Test Window.where() raises exc.DeprecatedError; fixtures `session`; execute `window.where({'pane_id': '%0'})`; expect `pytest.raises(exc.DeprecatedError, match='Window\\.where\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#_legacy_api_test_tmuxobject_py_test_window_where_raises_deprecated_error_22a774544e | 3.2a through 3.7b | planned parity | +| tests/legacy_api/test_common.py::test_tmux_cmd_unicode | Verify tmux commands with unicode; fixtures `session`; execute `session.cmd('new-window', '-t', 3, '-n', 'юникод', '-F', 'Ελληνικά')`. | real-tmux integration test | PythonBehaviorParityTest#tests_legacy_api_test_common_py_test_tmux_cmd_unicode_7fa5256541 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_pane.py::test_pane_get_raises_deprecated_error | Test Pane.get() raises DeprecatedError; fixtures `session`; expect `pytest.raises(exc.DeprecatedError, match='Pane\\.get\\(\\) was deprecated')`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_legacy_api_test_pane_py_test_pane_get_raises_deprecated_error_c685b5f026 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_pane.py::test_pane_getitem_raises_deprecated_error | Test Pane.__getitem__() raises DeprecatedError; fixtures `session`; expect `pytest.raises(exc.DeprecatedError, match='Pane\\[key\\] lookup was deprecated')`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_legacy_api_test_pane_py_test_pane_getitem_raises_deprecated_error_5db8171483 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_pane.py::test_resize_pane_raises_deprecated_error | Test Pane.resize_pane() raises DeprecatedError; fixtures `session`; execute `pane.resize_pane(height=4)`; expect `pytest.raises(exc.DeprecatedError, match='Pane\\.resize_pane\\(\\) was deprecated')`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_legacy_api_test_pane_py_test_resize_pane_raises_deprecated_error_db2eda8365 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_pane.py::test_select_pane_raises_deprecated_error | Test Pane.select_pane() raises DeprecatedError; fixtures `session`; execute `pane.select_pane()`; expect `pytest.raises(exc.DeprecatedError, match='Pane\\.select_pane\\(\\) was deprecated')`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_legacy_api_test_pane_py_test_select_pane_raises_deprecated_error_fba5bc5886 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_pane.py::test_split_window_raises_deprecated_error | Test Pane.split_window() raises DeprecatedError; fixtures `session`; execute `pane.split_window()`; expect `pytest.raises(exc.DeprecatedError, match='Pane\\.split_window\\(\\) was deprecated')`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_legacy_api_test_pane_py_test_split_window_raises_deprecated_error_1aa48b9172 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_server.py::test_kill_server_raises_deprecated_error | Test Server.kill_server() raises exc.DeprecatedError; fixtures `server`; execute `server.kill_server()`; expect `pytest.raises(exc.DeprecatedError, match='Server\\.kill_server\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#tests_legacy_api_test_server_py_test_kill_server_raises_deprecated_error_83f9f3f7a0 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_server.py::test_server__list_panes_raises_deprecated_error | Test Server._list_panes() raises exc.DeprecatedError; fixtures `server`; execute `server._list_panes()`; expect `pytest.raises(exc.DeprecatedError, match='Server\\._list_panes\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#legacy_api_test_server_py_test_server_list_panes_raises_deprecated_error_92490325b3 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_server.py::test_server__list_sessions_raises_deprecated_error | Test Server._list_sessions() raises exc.DeprecatedError; fixtures `server`; execute `server._list_sessions()`; expect `pytest.raises(exc.DeprecatedError, match='Server\\._list_sessions\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#acy_api_test_server_py_test_server_list_sessions_raises_deprecated_error_100f982346 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_server.py::test_server__list_windows_raises_deprecated_error | Test Server._list_windows() raises exc.DeprecatedError; fixtures `server`; execute `server._list_windows()`; expect `pytest.raises(exc.DeprecatedError, match='Server\\._list_windows\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#gacy_api_test_server_py_test_server_list_windows_raises_deprecated_error_6dda9a1130 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_server.py::test_server__sessions_raises_deprecated_error | Test Server._sessions raises exc.DeprecatedError; fixtures `server`; expect `pytest.raises(exc.DeprecatedError, match='Server\\._sessions was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#s_legacy_api_test_server_py_test_server_sessions_raises_deprecated_error_0b0467bd46 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_server.py::test_server__update_panes_raises_deprecated_error | Test Server._update_panes() raises exc.DeprecatedError; fixtures `server`; execute `server._update_panes()`; expect `pytest.raises(exc.DeprecatedError, match='Server\\._update_panes\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#gacy_api_test_server_py_test_server_update_panes_raises_deprecated_error_b138b79715 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_server.py::test_server__update_windows_raises_deprecated_error | Test Server._update_windows() raises exc.DeprecatedError; fixtures `server`; execute `server._update_windows()`; expect `pytest.raises(exc.DeprecatedError, match='Server\\._update_windows\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#cy_api_test_server_py_test_server_update_windows_raises_deprecated_error_60e6de39f5 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_server.py::test_server_children_raises_deprecated_error | Test Server.children raises exc.DeprecatedError; fixtures `server`; expect `pytest.raises(exc.DeprecatedError, match='Server\\.children was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#s_legacy_api_test_server_py_test_server_children_raises_deprecated_error_693a4e97c5 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_server.py::test_server_find_where_raises_deprecated_error | Test Server.find_where() raises exc.DeprecatedError; fixtures `server`; execute `server.find_where({'session_name': 'test'})`; expect `pytest.raises(exc.DeprecatedError, match='Server\\.find_where\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#legacy_api_test_server_py_test_server_find_where_raises_deprecated_error_5d45d1f23e | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_server.py::test_server_get_by_id_raises_deprecated_error | Test Server.get_by_id() raises exc.DeprecatedError; fixtures `server`; execute `server.get_by_id('$0')`; expect `pytest.raises(exc.DeprecatedError, match='Server\\.get_by_id\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#_legacy_api_test_server_py_test_server_get_by_id_raises_deprecated_error_93585abef2 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_server.py::test_server_list_sessions_raises_deprecated_error | Test Server.list_sessions() raises exc.DeprecatedError; fixtures `server`; execute `server.list_sessions()`; expect `pytest.raises(exc.DeprecatedError, match='Server\\.list_sessions\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#acy_api_test_server_py_test_server_list_sessions_raises_deprecated_error_3ea9a309a2 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_server.py::test_server_where_raises_deprecated_error | Test Server.where() raises exc.DeprecatedError; fixtures `server`; execute `server.where({'session_name': 'test'})`; expect `pytest.raises(exc.DeprecatedError, match='Server\\.where\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#ests_legacy_api_test_server_py_test_server_where_raises_deprecated_error_06a79f8f2e | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_session.py::test_attach_session_raises_deprecated_error | Test Session.attach_session() raises exc.DeprecatedError; fixtures `session`; execute `session.attach_session()`; expect `pytest.raises(exc.DeprecatedError, match='Session\\.attach_session\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#s_legacy_api_test_session_py_test_attach_session_raises_deprecated_error_eb4c32f1c6 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_session.py::test_attached_pane_raises_deprecated_error | Test Session.attached_pane raises exc.DeprecatedError; fixtures `session`; expect `pytest.raises(exc.DeprecatedError, match='Session\\.attached_pane was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#ts_legacy_api_test_session_py_test_attached_pane_raises_deprecated_error_92b3e0ed25 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_session.py::test_attached_window_raises_deprecated_error | Test Session.attached_window raises exc.DeprecatedError; fixtures `session`; expect `pytest.raises(exc.DeprecatedError, match='Session\\.attached_window was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#_legacy_api_test_session_py_test_attached_window_raises_deprecated_error_0e6f0927c5 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_session.py::test_kill_session_raises_deprecated_error | Test Session.kill_session() raises exc.DeprecatedError; fixtures `server`; execute `server.new_session(session_name='test_kill_session', detach=True)`; `new_session.kill_session()`; `new_session.kill()`; expect `pytest.raises(exc.DeprecatedError, match='Session\\.kill_session\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#sts_legacy_api_test_session_py_test_kill_session_raises_deprecated_error_bedfb1eae5 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_session.py::test_session__list_windows_raises_deprecated_error | Test Session._list_windows() raises exc.DeprecatedError; fixtures `session`; execute `session._list_windows()`; expect `pytest.raises(exc.DeprecatedError, match='Session\\._list_windows\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#cy_api_test_session_py_test_session_list_windows_raises_deprecated_error_fd0ed21403 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_session.py::test_session__windows_raises_deprecated_error | Test Session._windows raises exc.DeprecatedError; fixtures `session`; expect `pytest.raises(exc.DeprecatedError, match='Session\\._windows was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#_legacy_api_test_session_py_test_session_windows_raises_deprecated_error_b479441f8f | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_session.py::test_session_children_raises_deprecated_error | Test Session.children raises exc.DeprecatedError; fixtures `session`; expect `pytest.raises(exc.DeprecatedError, match='Session\\.children was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#legacy_api_test_session_py_test_session_children_raises_deprecated_error_7931b2ad06 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_session.py::test_session_find_where_raises_deprecated_error | Test Session.find_where() raises exc.DeprecatedError; fixtures `session`; execute `session.find_where({'window_name': 'test'})`; expect `pytest.raises(exc.DeprecatedError, match='Session\\.find_where\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#gacy_api_test_session_py_test_session_find_where_raises_deprecated_error_710a5495c0 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_session.py::test_session_get_by_id_raises_deprecated_error | Test Session.get_by_id() raises exc.DeprecatedError; fixtures `session`; execute `session.get_by_id('@0')`; expect `pytest.raises(exc.DeprecatedError, match='Session\\.get_by_id\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#egacy_api_test_session_py_test_session_get_by_id_raises_deprecated_error_817a478a6b | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_session.py::test_session_get_raises_deprecated_error | Test Session.get() raises exc.DeprecatedError; fixtures `session`; expect `pytest.raises(exc.DeprecatedError, match='Session\\.get\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#ests_legacy_api_test_session_py_test_session_get_raises_deprecated_error_a4c0492453 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_session.py::test_session_getitem_raises_deprecated_error | Test Session.__getitem__() raises exc.DeprecatedError; fixtures `session`; expect `pytest.raises(exc.DeprecatedError, match='Session\\[key\\] lookup was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#_legacy_api_test_session_py_test_session_getitem_raises_deprecated_error_2f10d5a199 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_session.py::test_session_list_windows_raises_deprecated_error | Test Session.list_windows() raises exc.DeprecatedError; fixtures `session`; execute `session.list_windows()`; expect `pytest.raises(exc.DeprecatedError, match='Session\\.list_windows\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#cy_api_test_session_py_test_session_list_windows_raises_deprecated_error_17a8f6dfb4 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_session.py::test_session_where_raises_deprecated_error | Test Session.where() raises exc.DeprecatedError; fixtures `session`; execute `session.where({'window_name': 'test'})`; expect `pytest.raises(exc.DeprecatedError, match='Session\\.where\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#ts_legacy_api_test_session_py_test_session_where_raises_deprecated_error_56f771dca4 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_tmuxobject.py::test_server_find_where_raises_deprecated_error | Test Server.find_where() raises exc.DeprecatedError; fixtures `server`; execute `server.find_where({'session_name': 'test'})`; expect `pytest.raises(exc.DeprecatedError, match='Server\\.find_where\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#cy_api_test_tmuxobject_py_test_server_find_where_raises_deprecated_error_c760af2b7f | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_tmuxobject.py::test_server_get_by_id_raises_deprecated_error | Test Server.get_by_id() raises exc.DeprecatedError; fixtures `server`; execute `server.get_by_id('$0')`; expect `pytest.raises(exc.DeprecatedError, match='Server\\.get_by_id\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#acy_api_test_tmuxobject_py_test_server_get_by_id_raises_deprecated_error_3832535554 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_tmuxobject.py::test_server_where_raises_deprecated_error | Test Server.where() raises exc.DeprecatedError; fixtures `server`; execute `server.where({'session_name': 'test'})`; expect `pytest.raises(exc.DeprecatedError, match='Server\\.where\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#_legacy_api_test_tmuxobject_py_test_server_where_raises_deprecated_error_eb953219b3 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_tmuxobject.py::test_session_find_where_raises_deprecated_error | Test Session.find_where() raises exc.DeprecatedError; fixtures `session`; execute `session.find_where({'window_name': 'test'})`; expect `pytest.raises(exc.DeprecatedError, match='Session\\.find_where\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#y_api_test_tmuxobject_py_test_session_find_where_raises_deprecated_error_8dfea1c4b0 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_tmuxobject.py::test_session_get_by_id_raises_deprecated_error | Test Session.get_by_id() raises exc.DeprecatedError; fixtures `session`; execute `session.get_by_id('@0')`; expect `pytest.raises(exc.DeprecatedError, match='Session\\.get_by_id\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#cy_api_test_tmuxobject_py_test_session_get_by_id_raises_deprecated_error_b641f63804 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_tmuxobject.py::test_session_where_raises_deprecated_error | Test Session.where() raises exc.DeprecatedError; fixtures `session`; execute `session.where({'window_name': 'test'})`; expect `pytest.raises(exc.DeprecatedError, match='Session\\.where\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#legacy_api_test_tmuxobject_py_test_session_where_raises_deprecated_error_e479b95b22 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_tmuxobject.py::test_window_find_where_raises_deprecated_error | Test Window.find_where() raises exc.DeprecatedError; fixtures `session`; execute `window.find_where({'pane_id': '%0'})`; expect `pytest.raises(exc.DeprecatedError, match='Window\\.find_where\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#cy_api_test_tmuxobject_py_test_window_find_where_raises_deprecated_error_644ad807b3 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_tmuxobject.py::test_window_get_by_id_raises_deprecated_error | Test Window.get_by_id() raises exc.DeprecatedError; fixtures `session`; execute `window.get_by_id('%0')`; expect `pytest.raises(exc.DeprecatedError, match='Window\\.get_by_id\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#acy_api_test_tmuxobject_py_test_window_get_by_id_raises_deprecated_error_e0cb1674f3 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_tmuxobject.py::test_window_where_raises_deprecated_error | Test Window.where() raises exc.DeprecatedError; fixtures `session`; execute `window.where({'pane_id': '%0'})`; expect `pytest.raises(exc.DeprecatedError, match='Window\\.where\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#_legacy_api_test_tmuxobject_py_test_window_where_raises_deprecated_error_22a774544e | 3.3a through 3.7c | planned parity | | tests/legacy_api/test_version.py::test_version[1.0.0-next.1] | Assert LooseVersion constructor against various version strings; case inputs `version='1.0.0-next.1'`; execute `LooseVersion(version)`; assert `LooseVersion(version)`. | unit test | PythonBehaviorParityTest#tests_legacy_api_test_version_py_test_version_1_0_0_next_1_4240bb7736 | not tmux-bound | planned parity | | tests/legacy_api/test_version.py::test_version[1.0.0-next] | Assert LooseVersion constructor against various version strings; case inputs `version='1.0.0-next'`; execute `LooseVersion(version)`; assert `LooseVersion(version)`. | unit test | PythonBehaviorParityTest#tests_legacy_api_test_version_py_test_version_1_0_0_next_d2910c5b41 | not tmux-bound | planned parity | | tests/legacy_api/test_version.py::test_version[1.0.0] | Assert LooseVersion constructor against various version strings; case inputs `version='1.0.0'`; execute `LooseVersion(version)`; assert `LooseVersion(version)`. | unit test | PythonBehaviorParityTest#tests_legacy_api_test_version_py_test_version_1_0_0_83b8418b14 | not tmux-bound | planned parity | @@ -560,22 +560,22 @@ under [`tests/`](https://github.com/tmux-python/libtmux/blob/c4a980b/tests) and | tests/legacy_api/test_version.py::test_version_compare[1-lt-1.0.0-openbsd-False] | Assert version comparisons; case inputs `a='1'`, `b='1.0.0-openbsd'`, `op=<built-in function lt>`, `raises=False`; execute `op(LooseVersion(a), LooseVersion(b))`; `t.cast('type[Exception]', raises)`; `t.cast('RaisesExc', does_not_raise())`; `does_not_raise()`; `LooseVersion(a)`; `LooseVersion(b)`; assert `op(LooseVersion(a), LooseVersion(b))`. | unit test | PythonBehaviorParityTest#legacy_api_test_version_py_test_version_compare_1_lt_1_0_0_openbsd_False_0947730d1b | not tmux-bound | planned parity | | tests/legacy_api/test_version.py::test_version_compare[1-lt-1.0.0p1-False] | Assert version comparisons; case inputs `a='1'`, `b='1.0.0p1'`, `op=<built-in function lt>`, `raises=False`; execute `op(LooseVersion(a), LooseVersion(b))`; `t.cast('type[Exception]', raises)`; `t.cast('RaisesExc', does_not_raise())`; `does_not_raise()`; `LooseVersion(a)`; `LooseVersion(b)`; assert `op(LooseVersion(a), LooseVersion(b))`. | unit test | PythonBehaviorParityTest#tests_legacy_api_test_version_py_test_version_compare_1_lt_1_0_0p1_False_f3a2e23e61 | not tmux-bound | planned parity | | tests/legacy_api/test_version.py::test_version_compare[1.0.0c-gt-1.0.0b-False] | Assert version comparisons; case inputs `a='1.0.0c'`, `b='1.0.0b'`, `op=<built-in function gt>`, `raises=False`; execute `op(LooseVersion(a), LooseVersion(b))`; `t.cast('type[Exception]', raises)`; `t.cast('RaisesExc', does_not_raise())`; `does_not_raise()`; `LooseVersion(a)`; `LooseVersion(b)`; assert `op(LooseVersion(a), LooseVersion(b))`. | unit test | PythonBehaviorParityTest#s_legacy_api_test_version_py_test_version_compare_1_0_0c_gt_1_0_0b_False_b68f114e02 | not tmux-bound | planned parity | -| tests/legacy_api/test_window.py::test_attached_pane_raises_deprecated_error | Test Window.attached_pane raises exc.DeprecatedError; fixtures `session`; expect `pytest.raises(exc.DeprecatedError, match='Window\\.attached_pane was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#sts_legacy_api_test_window_py_test_attached_pane_raises_deprecated_error_cd1f418b96 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_window.py::test_kill_window_raises_deprecated_error | Test Window.kill_window() raises exc.DeprecatedError; fixtures `session`; execute `session.new_window(window_name='extra_window')`; `window.kill_window()`; expect `pytest.raises(exc.DeprecatedError, match='Window\\.kill_window\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#tests_legacy_api_test_window_py_test_kill_window_raises_deprecated_error_2e9f0078b9 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_window.py::test_select_window_raises_deprecated_error | Test Window.select_window() raises exc.DeprecatedError; fixtures `session`; execute `window.select_window()`; expect `pytest.raises(exc.DeprecatedError, match='Window\\.select_window\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#sts_legacy_api_test_window_py_test_select_window_raises_deprecated_error_2fb44c2734 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_window.py::test_set_window_option_emits_deprecation_warning | Test Window.set_window_option() emits DeprecationWarning; fixtures `session`; execute `window.set_window_option('main-pane-height', 20)`; expect `pytest.warns(DeprecationWarning, match='Window\\.set_window_option\\(\\) is deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#gacy_api_test_window_py_test_set_window_option_emits_deprecation_warning_8b60198717 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_window.py::test_show_window_option_emits_deprecation_warning | Test Window.show_window_option() emits DeprecationWarning; fixtures `session`; execute `window.show_window_option('main-pane-height')`; expect `pytest.warns(DeprecationWarning, match='Window\\.show_window_option\\(\\) is deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#acy_api_test_window_py_test_show_window_option_emits_deprecation_warning_51acf1e29b | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_window.py::test_show_window_options_emits_deprecation_warning | Test Window.show_window_options() emits DeprecationWarning; fixtures `session`; execute `window.show_window_options()`; expect `pytest.warns(DeprecationWarning, match='Window\\.show_window_options\\(\\) is deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#cy_api_test_window_py_test_show_window_options_emits_deprecation_warning_adbdce0fe2 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_window.py::test_split_window_raises_deprecated_error | Test Window.split_window() raises exc.DeprecatedError; fixtures `session`; execute `window.split_window()`; expect `pytest.raises(exc.DeprecatedError, match='Window\\.split_window\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#ests_legacy_api_test_window_py_test_split_window_raises_deprecated_error_635a4ccca5 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_window.py::test_window__list_panes_raises_deprecated_error | Test Window._list_panes() raises exc.DeprecatedError; fixtures `session`; execute `window._list_panes()`; expect `pytest.raises(exc.DeprecatedError, match='Window\\._list_panes\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#legacy_api_test_window_py_test_window_list_panes_raises_deprecated_error_27eecd0fca | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_window.py::test_window__panes_raises_deprecated_error | Test Window._panes raises exc.DeprecatedError; fixtures `session`; expect `pytest.raises(exc.DeprecatedError, match='Window\\._panes was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#ests_legacy_api_test_window_py_test_window_panes_raises_deprecated_error_bf9ac5bdb2 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_window.py::test_window_children_raises_deprecated_error | Test Window.children raises exc.DeprecatedError; fixtures `session`; expect `pytest.raises(exc.DeprecatedError, match='Window\\.children was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#s_legacy_api_test_window_py_test_window_children_raises_deprecated_error_f48b74fd8e | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_window.py::test_window_find_where_raises_deprecated_error | Test Window.find_where() raises exc.DeprecatedError; fixtures `session`; execute `window.find_where({'pane_id': '%0'})`; expect `pytest.raises(exc.DeprecatedError, match='Window\\.find_where\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#legacy_api_test_window_py_test_window_find_where_raises_deprecated_error_db7b512349 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_window.py::test_window_get_by_id_raises_deprecated_error | Test Window.get_by_id() raises exc.DeprecatedError; fixtures `session`; execute `window.get_by_id('%0')`; expect `pytest.raises(exc.DeprecatedError, match='Window\\.get_by_id\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#_legacy_api_test_window_py_test_window_get_by_id_raises_deprecated_error_f1a001095f | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_window.py::test_window_get_raises_deprecated_error | Test Window.get() raises exc.DeprecatedError; fixtures `session`; expect `pytest.raises(exc.DeprecatedError, match='Window\\.get\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#tests_legacy_api_test_window_py_test_window_get_raises_deprecated_error_91782ec9fe | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_window.py::test_window_getitem_raises_deprecated_error | Test Window.__getitem__() raises exc.DeprecatedError; fixtures `session`; expect `pytest.raises(exc.DeprecatedError, match='Window\\[key\\] lookup was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#ts_legacy_api_test_window_py_test_window_getitem_raises_deprecated_error_c34a2392f0 | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_window.py::test_window_list_panes_raises_deprecated_error | Test Window.list_panes() raises exc.DeprecatedError; fixtures `session`; execute `window.list_panes()`; expect `pytest.raises(exc.DeprecatedError, match='Window\\.list_panes\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#legacy_api_test_window_py_test_window_list_panes_raises_deprecated_error_e8767aea1d | 3.2a through 3.7b | planned parity | -| tests/legacy_api/test_window.py::test_window_where_raises_deprecated_error | Test Window.where() raises exc.DeprecatedError; fixtures `session`; execute `window.where({'pane_id': '%0'})`; expect `pytest.raises(exc.DeprecatedError, match='Window\\.where\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#ests_legacy_api_test_window_py_test_window_where_raises_deprecated_error_4ea334bb4b | 3.2a through 3.7b | planned parity | +| tests/legacy_api/test_window.py::test_attached_pane_raises_deprecated_error | Test Window.attached_pane raises exc.DeprecatedError; fixtures `session`; expect `pytest.raises(exc.DeprecatedError, match='Window\\.attached_pane was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#sts_legacy_api_test_window_py_test_attached_pane_raises_deprecated_error_cd1f418b96 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_window.py::test_kill_window_raises_deprecated_error | Test Window.kill_window() raises exc.DeprecatedError; fixtures `session`; execute `session.new_window(window_name='extra_window')`; `window.kill_window()`; expect `pytest.raises(exc.DeprecatedError, match='Window\\.kill_window\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#tests_legacy_api_test_window_py_test_kill_window_raises_deprecated_error_2e9f0078b9 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_window.py::test_select_window_raises_deprecated_error | Test Window.select_window() raises exc.DeprecatedError; fixtures `session`; execute `window.select_window()`; expect `pytest.raises(exc.DeprecatedError, match='Window\\.select_window\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#sts_legacy_api_test_window_py_test_select_window_raises_deprecated_error_2fb44c2734 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_window.py::test_set_window_option_emits_deprecation_warning | Test Window.set_window_option() emits DeprecationWarning; fixtures `session`; execute `window.set_window_option('main-pane-height', 20)`; expect `pytest.warns(DeprecationWarning, match='Window\\.set_window_option\\(\\) is deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#gacy_api_test_window_py_test_set_window_option_emits_deprecation_warning_8b60198717 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_window.py::test_show_window_option_emits_deprecation_warning | Test Window.show_window_option() emits DeprecationWarning; fixtures `session`; execute `window.show_window_option('main-pane-height')`; expect `pytest.warns(DeprecationWarning, match='Window\\.show_window_option\\(\\) is deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#acy_api_test_window_py_test_show_window_option_emits_deprecation_warning_51acf1e29b | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_window.py::test_show_window_options_emits_deprecation_warning | Test Window.show_window_options() emits DeprecationWarning; fixtures `session`; execute `window.show_window_options()`; expect `pytest.warns(DeprecationWarning, match='Window\\.show_window_options\\(\\) is deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#cy_api_test_window_py_test_show_window_options_emits_deprecation_warning_adbdce0fe2 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_window.py::test_split_window_raises_deprecated_error | Test Window.split_window() raises exc.DeprecatedError; fixtures `session`; execute `window.split_window()`; expect `pytest.raises(exc.DeprecatedError, match='Window\\.split_window\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#ests_legacy_api_test_window_py_test_split_window_raises_deprecated_error_635a4ccca5 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_window.py::test_window__list_panes_raises_deprecated_error | Test Window._list_panes() raises exc.DeprecatedError; fixtures `session`; execute `window._list_panes()`; expect `pytest.raises(exc.DeprecatedError, match='Window\\._list_panes\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#legacy_api_test_window_py_test_window_list_panes_raises_deprecated_error_27eecd0fca | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_window.py::test_window__panes_raises_deprecated_error | Test Window._panes raises exc.DeprecatedError; fixtures `session`; expect `pytest.raises(exc.DeprecatedError, match='Window\\._panes was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#ests_legacy_api_test_window_py_test_window_panes_raises_deprecated_error_bf9ac5bdb2 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_window.py::test_window_children_raises_deprecated_error | Test Window.children raises exc.DeprecatedError; fixtures `session`; expect `pytest.raises(exc.DeprecatedError, match='Window\\.children was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#s_legacy_api_test_window_py_test_window_children_raises_deprecated_error_f48b74fd8e | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_window.py::test_window_find_where_raises_deprecated_error | Test Window.find_where() raises exc.DeprecatedError; fixtures `session`; execute `window.find_where({'pane_id': '%0'})`; expect `pytest.raises(exc.DeprecatedError, match='Window\\.find_where\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#legacy_api_test_window_py_test_window_find_where_raises_deprecated_error_db7b512349 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_window.py::test_window_get_by_id_raises_deprecated_error | Test Window.get_by_id() raises exc.DeprecatedError; fixtures `session`; execute `window.get_by_id('%0')`; expect `pytest.raises(exc.DeprecatedError, match='Window\\.get_by_id\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#_legacy_api_test_window_py_test_window_get_by_id_raises_deprecated_error_f1a001095f | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_window.py::test_window_get_raises_deprecated_error | Test Window.get() raises exc.DeprecatedError; fixtures `session`; expect `pytest.raises(exc.DeprecatedError, match='Window\\.get\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#tests_legacy_api_test_window_py_test_window_get_raises_deprecated_error_91782ec9fe | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_window.py::test_window_getitem_raises_deprecated_error | Test Window.__getitem__() raises exc.DeprecatedError; fixtures `session`; expect `pytest.raises(exc.DeprecatedError, match='Window\\[key\\] lookup was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#ts_legacy_api_test_window_py_test_window_getitem_raises_deprecated_error_c34a2392f0 | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_window.py::test_window_list_panes_raises_deprecated_error | Test Window.list_panes() raises exc.DeprecatedError; fixtures `session`; execute `window.list_panes()`; expect `pytest.raises(exc.DeprecatedError, match='Window\\.list_panes\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#legacy_api_test_window_py_test_window_list_panes_raises_deprecated_error_e8767aea1d | 3.3a through 3.7c | planned parity | +| tests/legacy_api/test_window.py::test_window_where_raises_deprecated_error | Test Window.where() raises exc.DeprecatedError; fixtures `session`; execute `window.where({'pane_id': '%0'})`; expect `pytest.raises(exc.DeprecatedError, match='Window\\.where\\(\\) was deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#ests_legacy_api_test_window_py_test_window_where_raises_deprecated_error_4ea334bb4b | 3.3a through 3.7c | planned parity | | tests/test/test_constants.py::test_retry_interval_seconds_default | Test RETRY_INTERVAL_SECONDS default value; assert `RETRY_INTERVAL_SECONDS == 0.05`. | unit test | PythonBehaviorParityTest#tests_test_test_constants_py_test_retry_interval_seconds_default_c96f0d8a4e | not tmux-bound | planned parity | | tests/test/test_constants.py::test_retry_interval_seconds_env | Test RETRY_INTERVAL_SECONDS can be configured via environment variable; fixtures `monkeypatch`; execute `reload(libtmux.test.constants)`; assert `libtmux.test.constants.RETRY_INTERVAL_SECONDS == 0.1`. | unit test | PythonBehaviorParityTest#tests_test_test_constants_py_test_retry_interval_seconds_env_b3c4d57585 | not tmux-bound | planned parity | | tests/test/test_constants.py::test_retry_timeout_seconds_default | Test RETRY_TIMEOUT_SECONDS default value; assert `RETRY_TIMEOUT_SECONDS == 8`. | unit test | PythonBehaviorParityTest#tests_test_test_constants_py_test_retry_timeout_seconds_default_732eff539d | not tmux-bound | planned parity | @@ -588,21 +588,21 @@ under [`tests/`](https://github.com/tmux-python/libtmux/blob/c4a980b/tests) and | tests/test/test_environment.py::test_environment_var_guard_unset | Test unsetting environment variables with EnvironmentVarGuard; execute `EnvironmentVarGuard()`; `env.unset('TEST_EXISTING_VAR')`; `env.unset('TEST_NON_EXISTENT_VAR')`; `env.__exit__(None, None, None)`; assert `'TEST_EXISTING_VAR' not in os.environ`; `os.environ['TEST_EXISTING_VAR'] == 'original_value'`. | unit test | PythonBehaviorParityTest#tests_test_test_environment_py_test_environment_var_guard_unset_8086242665 | not tmux-bound | planned parity | | tests/test/test_environment.py::test_environment_var_guard_unset_and_reset | Test unsetting and then resetting a variable; execute `EnvironmentVarGuard()`; `env.unset('TEST_VAR1')`; `env.set('TEST_VAR1', 'new_value1')`; `env.set('TEST_VAR2', 'new_value2')`; `env.unset('TEST_VAR2')`; `env.__exit__(None, None, None)`; assert `'TEST_VAR1' not in os.environ`; `os.environ['TEST_VAR1'] == 'new_value1'`; `'TEST_VAR2' not in os.environ`; `os.environ['TEST_VAR1'] == 'value1'`; `os.environ['TEST_VAR2'] == 'value2'`. | unit test | PythonBehaviorParityTest#ests_test_test_environment_py_test_environment_var_guard_unset_and_reset_a9aed2afe7 | not tmux-bound | planned parity | | tests/test/test_environment.py::test_environment_var_guard_unset_previously_set | Test unsetting a variable that was previously set in the same context; execute `EnvironmentVarGuard()`; `env.set('TEST_NEW_VAR', 'new_value')`; `env.unset('TEST_NEW_VAR')`; assert `'TEST_NEW_VAR' in os.environ`; `os.environ['TEST_NEW_VAR'] == 'new_value'`; `'TEST_NEW_VAR' not in os.environ`. | unit test | PythonBehaviorParityTest#test_test_environment_py_test_environment_var_guard_unset_previously_set_c0e53b059c | not tmux-bound | planned parity | -| tests/test/test_random.py::test_collisions_with_real_objects | Test collision behavior using real tmux objects instead of mocks; fixtures `server`, `session`; execute `server.new_session(session_name1)`; `server.new_session(session_name2)`; `server.has_session(session_name1)`; `server.has_session(session_name2)`; `server.has_session(result)`; `session.new_window(window_name=window_name1)`; assert `server.has_session(session_name1)`; `server.has_session(session_name2)`; `result.startswith(prefix)`; `result != session_name1`; `result != session_name2`; `not server.has_session(result)`; `any((w.window_name == window_name1 for w in session.windows))`; `any((w.window_name == window_name2 for w in session.windows))`; `result != window_name1`; `result != window_name2`; `not any((w.window_name == result for w in session.windows))`; when `window1`: execute `window1.kill()`; when `window2`: execute `window2.kill()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_random_py_test_collisions_with_real_objects_c73ef59ae2 | 3.2a through 3.7b | planned parity | -| tests/test/test_random.py::test_get_test_session_name_collision_handling | Test that get_test_session_name handles collisions properly; fixtures `server`, `monkeypatch`; execute `get_test_session_name(server)`; `session_name.startswith(TEST_SESSION_PREFIX)`; assert `call_count == 2`; `session_name.startswith(TEST_SESSION_PREFIX)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_random_py_test_get_test_session_name_collision_handling_44e82833e0 | 3.2a through 3.7b | planned parity | -| tests/test/test_random.py::test_get_test_session_name_custom_prefix | Test get_test_session_name with custom prefix; fixtures `server`; execute `server.has_session(result)`; `get_test_session_name(server=server, prefix=prefix)`; `result.startswith(prefix)`; assert `isinstance(result, str)`; `result.startswith(prefix)`; `len(result) == len(prefix) + 8`; `not server.has_session(result)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_random_py_test_get_test_session_name_custom_prefix_93d8de90ce | 3.2a through 3.7b | planned parity | -| tests/test/test_random.py::test_get_test_session_name_default_prefix | Test get_test_session_name with default prefix; fixtures `server`; execute `server.has_session(result)`; `get_test_session_name(server=server)`; `result.startswith(TEST_SESSION_PREFIX)`; assert `isinstance(result, str)`; `result.startswith(TEST_SESSION_PREFIX)`; `len(result) == len(TEST_SESSION_PREFIX) + 8`; `not server.has_session(result)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_random_py_test_get_test_session_name_default_prefix_f2e42e2c73 | 3.2a through 3.7b | planned parity | -| tests/test/test_random.py::test_get_test_session_name_doctest_examples | Test the doctest examples for get_test_session_name; fixtures `server`; execute `get_test_session_name(server=server)`; `result.startswith(TEST_SESSION_PREFIX)`; assert `result.startswith(TEST_SESSION_PREFIX)`; `len(result) == len(TEST_SESSION_PREFIX) + 8`; `result1 != result2`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_random_py_test_get_test_session_name_doctest_examples_c6410244df | 3.2a through 3.7b | planned parity | -| tests/test/test_random.py::test_get_test_session_name_implementation_details | Test specific implementation details of get_test_session_name function; fixtures `server`, `monkeypatch`; execute `server.new_session(collision_name)`; `get_test_session_name(server, prefix=prefix)`; `result.startswith(prefix)`; assert `result != collision_name`; `result.startswith(prefix)`. | real-tmux integration test | PythonBehaviorParityTest#ts_test_test_random_py_test_get_test_session_name_implementation_details_53ddea23bf | 3.2a through 3.7b | planned parity | -| tests/test/test_random.py::test_get_test_session_name_loop_behavior | Test the loop behavior in get_test_session_name using real sessions; fixtures `server`; execute `server.new_session(first_name)`; `server.has_session(first_name)`; `server.has_session(second_name)`; `server.new_session(second_name)`; `server.has_session(third_name)`; `get_test_session_name(server=server)`; assert `first_name != second_name`; `server.has_session(first_name)`; `not server.has_session(second_name)`; `first_name != third_name`; `second_name != third_name`; `server.has_session(second_name)`; `not server.has_session(third_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_random_py_test_get_test_session_name_loop_behavior_401ae81bad | 3.2a through 3.7b | planned parity | -| tests/test/test_random.py::test_get_test_window_name_branch_coverage | Test branch coverage for get_test_window_name function; fixtures `session`; execute `session.new_window(window_name=collision_name)`; `get_test_window_name(session, prefix=prefix)`; `result.startswith(prefix)`; assert `result != collision_name`; `result.startswith(prefix)`; when `window`: execute `window.kill()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_random_py_test_get_test_window_name_branch_coverage_fa55610e5b | 3.2a through 3.7b | planned parity | -| tests/test/test_random.py::test_get_test_window_name_collision_handling | Test that get_test_window_name handles collisions properly; fixtures `session`, `monkeypatch`; execute `session.new_window(window_name=first_name)`; `get_test_window_name(session, prefix=prefix)`; `window_name.startswith(prefix)`; `any((w.window_name == window_name for w in session.windows))`; `any((w.window_name == first_name for w in session.windows))`; assert `window_name != first_name`; `window_name.startswith(prefix)`; `not any((w.window_name == window_name for w in session.windows))`; `any((w.window_name == first_name for w in session.windows))`; when `window`: execute `window.kill()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_random_py_test_get_test_window_name_collision_handling_daac1a3b01 | 3.2a through 3.7b | planned parity | -| tests/test/test_random.py::test_get_test_window_name_custom_prefix | Test get_test_window_name with custom prefix; fixtures `session`; execute `get_test_window_name(session=session, prefix=prefix)`; `result.startswith(prefix)`; `any((w.window_name == result for w in session.windows))`; assert `isinstance(result, str)`; `result.startswith(prefix)`; `len(result) == len(prefix) + 8`; `not any((w.window_name == result for w in session.windows))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_random_py_test_get_test_window_name_custom_prefix_0a06addb6e | 3.2a through 3.7b | planned parity | -| tests/test/test_random.py::test_get_test_window_name_default_prefix | Test get_test_window_name with default prefix; fixtures `session`; execute `get_test_window_name(session=session)`; `result.startswith(TEST_SESSION_PREFIX)`; `any((w.window_name == result for w in session.windows))`; assert `isinstance(result, str)`; `result.startswith(TEST_SESSION_PREFIX)`; `len(result) == len(TEST_SESSION_PREFIX) + 8`; `not any((w.window_name == result for w in session.windows))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_random_py_test_get_test_window_name_default_prefix_8d00d82cee | 3.2a through 3.7b | planned parity | -| tests/test/test_random.py::test_get_test_window_name_doctest_examples | Test the doctest examples for get_test_window_name; fixtures `session`; execute `get_test_window_name(session=session)`; `result.startswith(TEST_SESSION_PREFIX)`; assert `result.startswith(TEST_SESSION_PREFIX)`; `len(result) == len(TEST_SESSION_PREFIX) + 8`; `result1 != result2`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_random_py_test_get_test_window_name_doctest_examples_66bb5af877 | 3.2a through 3.7b | planned parity | -| tests/test/test_random.py::test_get_test_window_name_loop_behavior | Test the loop behavior in get_test_window_name using real windows; fixtures `session`; execute `session.new_window(window_name=first_name)`; `session.new_window(window_name=second_name)`; `get_test_window_name(session=session)`; `any((w.window_name == first_name for w in session.windows))`; `any((w.window_name == second_name for w in session.windows))`; `any((w.window_name == third_name for w in session.windows))`; assert `first_name != second_name`; `any((w.window_name == first_name for w in session.windows))`; `not any((w.window_name == second_name for w in session.windows))`; `first_name != third_name`; `second_name != third_name`; `any((w.window_name == second_name for w in session.windows))`; `not any((w.window_name == third_name for w in session.windows))`; when `window2`: execute `window2.kill()`; when `window`: execute `window.kill()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_random_py_test_get_test_window_name_loop_behavior_7941ff860d | 3.2a through 3.7b | planned parity | -| tests/test/test_random.py::test_get_test_window_name_null_prefix | Test that get_test_window_name with None prefix raises an assertion error; execute `t.cast('Session', object())`; `object()`; `get_test_window_name(mock_session, prefix=None)`; expect `pytest.raises(AssertionError)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_random_py_test_get_test_window_name_null_prefix_73135fa901 | 3.2a through 3.7b | planned parity | -| tests/test/test_random.py::test_get_test_window_name_requires_prefix | Test that get_test_window_name requires a prefix; execute `get_test_window_name(session=t.cast('Session', object()), prefix=None)`; `t.cast('Session', object())`; `object()`; expect `pytest.raises(AssertionError)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_random_py_test_get_test_window_name_requires_prefix_7be81373c7 | 3.2a through 3.7b | planned parity | +| tests/test/test_random.py::test_collisions_with_real_objects | Test collision behavior using real tmux objects instead of mocks; fixtures `server`, `session`; execute `server.new_session(session_name1)`; `server.new_session(session_name2)`; `server.has_session(session_name1)`; `server.has_session(session_name2)`; `server.has_session(result)`; `session.new_window(window_name=window_name1)`; assert `server.has_session(session_name1)`; `server.has_session(session_name2)`; `result.startswith(prefix)`; `result != session_name1`; `result != session_name2`; `not server.has_session(result)`; `any((w.window_name == window_name1 for w in session.windows))`; `any((w.window_name == window_name2 for w in session.windows))`; `result != window_name1`; `result != window_name2`; `not any((w.window_name == result for w in session.windows))`; when `window1`: execute `window1.kill()`; when `window2`: execute `window2.kill()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_random_py_test_collisions_with_real_objects_c73ef59ae2 | 3.3a through 3.7c | planned parity | +| tests/test/test_random.py::test_get_test_session_name_collision_handling | Test that get_test_session_name handles collisions properly; fixtures `server`, `monkeypatch`; execute `get_test_session_name(server)`; `session_name.startswith(TEST_SESSION_PREFIX)`; assert `call_count == 2`; `session_name.startswith(TEST_SESSION_PREFIX)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_random_py_test_get_test_session_name_collision_handling_44e82833e0 | 3.3a through 3.7c | planned parity | +| tests/test/test_random.py::test_get_test_session_name_custom_prefix | Test get_test_session_name with custom prefix; fixtures `server`; execute `server.has_session(result)`; `get_test_session_name(server=server, prefix=prefix)`; `result.startswith(prefix)`; assert `isinstance(result, str)`; `result.startswith(prefix)`; `len(result) == len(prefix) + 8`; `not server.has_session(result)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_random_py_test_get_test_session_name_custom_prefix_93d8de90ce | 3.3a through 3.7c | planned parity | +| tests/test/test_random.py::test_get_test_session_name_default_prefix | Test get_test_session_name with default prefix; fixtures `server`; execute `server.has_session(result)`; `get_test_session_name(server=server)`; `result.startswith(TEST_SESSION_PREFIX)`; assert `isinstance(result, str)`; `result.startswith(TEST_SESSION_PREFIX)`; `len(result) == len(TEST_SESSION_PREFIX) + 8`; `not server.has_session(result)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_random_py_test_get_test_session_name_default_prefix_f2e42e2c73 | 3.3a through 3.7c | planned parity | +| tests/test/test_random.py::test_get_test_session_name_doctest_examples | Test the doctest examples for get_test_session_name; fixtures `server`; execute `get_test_session_name(server=server)`; `result.startswith(TEST_SESSION_PREFIX)`; assert `result.startswith(TEST_SESSION_PREFIX)`; `len(result) == len(TEST_SESSION_PREFIX) + 8`; `result1 != result2`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_random_py_test_get_test_session_name_doctest_examples_c6410244df | 3.3a through 3.7c | planned parity | +| tests/test/test_random.py::test_get_test_session_name_implementation_details | Test specific implementation details of get_test_session_name function; fixtures `server`, `monkeypatch`; execute `server.new_session(collision_name)`; `get_test_session_name(server, prefix=prefix)`; `result.startswith(prefix)`; assert `result != collision_name`; `result.startswith(prefix)`. | real-tmux integration test | PythonBehaviorParityTest#ts_test_test_random_py_test_get_test_session_name_implementation_details_53ddea23bf | 3.3a through 3.7c | planned parity | +| tests/test/test_random.py::test_get_test_session_name_loop_behavior | Test the loop behavior in get_test_session_name using real sessions; fixtures `server`; execute `server.new_session(first_name)`; `server.has_session(first_name)`; `server.has_session(second_name)`; `server.new_session(second_name)`; `server.has_session(third_name)`; `get_test_session_name(server=server)`; assert `first_name != second_name`; `server.has_session(first_name)`; `not server.has_session(second_name)`; `first_name != third_name`; `second_name != third_name`; `server.has_session(second_name)`; `not server.has_session(third_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_random_py_test_get_test_session_name_loop_behavior_401ae81bad | 3.3a through 3.7c | planned parity | +| tests/test/test_random.py::test_get_test_window_name_branch_coverage | Test branch coverage for get_test_window_name function; fixtures `session`; execute `session.new_window(window_name=collision_name)`; `get_test_window_name(session, prefix=prefix)`; `result.startswith(prefix)`; assert `result != collision_name`; `result.startswith(prefix)`; when `window`: execute `window.kill()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_random_py_test_get_test_window_name_branch_coverage_fa55610e5b | 3.3a through 3.7c | planned parity | +| tests/test/test_random.py::test_get_test_window_name_collision_handling | Test that get_test_window_name handles collisions properly; fixtures `session`, `monkeypatch`; execute `session.new_window(window_name=first_name)`; `get_test_window_name(session, prefix=prefix)`; `window_name.startswith(prefix)`; `any((w.window_name == window_name for w in session.windows))`; `any((w.window_name == first_name for w in session.windows))`; assert `window_name != first_name`; `window_name.startswith(prefix)`; `not any((w.window_name == window_name for w in session.windows))`; `any((w.window_name == first_name for w in session.windows))`; when `window`: execute `window.kill()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_random_py_test_get_test_window_name_collision_handling_daac1a3b01 | 3.3a through 3.7c | planned parity | +| tests/test/test_random.py::test_get_test_window_name_custom_prefix | Test get_test_window_name with custom prefix; fixtures `session`; execute `get_test_window_name(session=session, prefix=prefix)`; `result.startswith(prefix)`; `any((w.window_name == result for w in session.windows))`; assert `isinstance(result, str)`; `result.startswith(prefix)`; `len(result) == len(prefix) + 8`; `not any((w.window_name == result for w in session.windows))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_random_py_test_get_test_window_name_custom_prefix_0a06addb6e | 3.3a through 3.7c | planned parity | +| tests/test/test_random.py::test_get_test_window_name_default_prefix | Test get_test_window_name with default prefix; fixtures `session`; execute `get_test_window_name(session=session)`; `result.startswith(TEST_SESSION_PREFIX)`; `any((w.window_name == result for w in session.windows))`; assert `isinstance(result, str)`; `result.startswith(TEST_SESSION_PREFIX)`; `len(result) == len(TEST_SESSION_PREFIX) + 8`; `not any((w.window_name == result for w in session.windows))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_random_py_test_get_test_window_name_default_prefix_8d00d82cee | 3.3a through 3.7c | planned parity | +| tests/test/test_random.py::test_get_test_window_name_doctest_examples | Test the doctest examples for get_test_window_name; fixtures `session`; execute `get_test_window_name(session=session)`; `result.startswith(TEST_SESSION_PREFIX)`; assert `result.startswith(TEST_SESSION_PREFIX)`; `len(result) == len(TEST_SESSION_PREFIX) + 8`; `result1 != result2`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_random_py_test_get_test_window_name_doctest_examples_66bb5af877 | 3.3a through 3.7c | planned parity | +| tests/test/test_random.py::test_get_test_window_name_loop_behavior | Test the loop behavior in get_test_window_name using real windows; fixtures `session`; execute `session.new_window(window_name=first_name)`; `session.new_window(window_name=second_name)`; `get_test_window_name(session=session)`; `any((w.window_name == first_name for w in session.windows))`; `any((w.window_name == second_name for w in session.windows))`; `any((w.window_name == third_name for w in session.windows))`; assert `first_name != second_name`; `any((w.window_name == first_name for w in session.windows))`; `not any((w.window_name == second_name for w in session.windows))`; `first_name != third_name`; `second_name != third_name`; `any((w.window_name == second_name for w in session.windows))`; `not any((w.window_name == third_name for w in session.windows))`; when `window2`: execute `window2.kill()`; when `window`: execute `window.kill()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_random_py_test_get_test_window_name_loop_behavior_7941ff860d | 3.3a through 3.7c | planned parity | +| tests/test/test_random.py::test_get_test_window_name_null_prefix | Test that get_test_window_name with None prefix raises an assertion error; execute `t.cast('Session', object())`; `object()`; `get_test_window_name(mock_session, prefix=None)`; expect `pytest.raises(AssertionError)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_random_py_test_get_test_window_name_null_prefix_73135fa901 | 3.3a through 3.7c | planned parity | +| tests/test/test_random.py::test_get_test_window_name_requires_prefix | Test that get_test_window_name requires a prefix; execute `get_test_window_name(session=t.cast('Session', object()), prefix=None)`; `t.cast('Session', object())`; `object()`; expect `pytest.raises(AssertionError)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_random_py_test_get_test_window_name_requires_prefix_7be81373c7 | 3.3a through 3.7c | planned parity | | tests/test/test_random.py::test_import_typing_coverage | Test coverage for typing imports in random.py; execute `dir(random)`; assert `hasattr(t, 'TYPE_CHECKING')`; `'t' in dir(random)`. | unit test | PythonBehaviorParityTest#tests_test_test_random_py_test_import_typing_coverage_911f57b387 | not tmux-bound | planned parity | | tests/test/test_random.py::test_imports_coverage | Test coverage for import statements in random.py; assert `hasattr(random, 'random')`; `hasattr(random, 't')`; `hasattr(random, 'TEST_SESSION_PREFIX')`. | unit test | PythonBehaviorParityTest#tests_test_test_random_py_test_imports_coverage_10fccd6010 | not tmux-bound | planned parity | | tests/test/test_random.py::test_iterator_protocol | Test the complete iterator protocol of RandomStrSequence; execute `RandomStrSequence()`; assert `iterator is rng`; `isinstance(result, str)`; `len(result) == 8`. | unit test | PythonBehaviorParityTest#tests_test_test_random_py_test_iterator_protocol_f1f61f029a | not tmux-bound | planned parity | @@ -639,48 +639,48 @@ under [`tests/`](https://github.com/tmux-python/libtmux/blob/c4a980b/tests) and | tests/test/test_sparse_array.py::test_sparse_array_iter_values[consecutive] | Test SparseArray.iter_values method; case inputs `test_case=SparseArrayValuesTestCase(test_id='consecutive', adds=[(3, 'three'), (1, 'one'), (2, 'two')], expected_values=['one', 'two', 'three'])`; execute `SparseArray()`; `arr.iter_values()`; assert `list(arr.iter_values()) == test_case.expected_values`. | unit test | PythonBehaviorParityTest#ests_test_test_sparse_array_py_test_sparse_array_iter_values_consecutive_cc3924026a | not tmux-bound | planned parity | | tests/test/test_sparse_array.py::test_sparse_array_iter_values[empty] | Test SparseArray.iter_values method; case inputs `test_case=SparseArrayValuesTestCase(test_id='empty', adds=[], expected_values=[])`; execute `SparseArray()`; `arr.iter_values()`; assert `list(arr.iter_values()) == test_case.expected_values`. | unit test | PythonBehaviorParityTest#tests_test_test_sparse_array_py_test_sparse_array_iter_values_empty_f1bfa05fd9 | not tmux-bound | planned parity | | tests/test/test_sparse_array.py::test_sparse_array_iter_values[sorted_order] | Test SparseArray.iter_values method; case inputs `test_case=SparseArrayValuesTestCase(test_id='sorted_order', adds=[(10, 'ten'), (1, 'one'), (5, 'five')], expected_values=['one', 'five', 'ten'])`; execute `SparseArray()`; `arr.iter_values()`; assert `list(arr.iter_values()) == test_case.expected_values`. | unit test | PythonBehaviorParityTest#sts_test_test_sparse_array_py_test_sparse_array_iter_values_sorted_order_e0f2bfa205 | not tmux-bound | planned parity | -| tests/test/test_temporary.py::test_temp_session_cleanup_on_exception | Test that temp_session cleans up even when an exception occurs; fixtures `server`; execute `server.has_session(session_name)`; `RuntimeError()`; `temp_session(server)`; expect `pytest.raises(RuntimeError)`; assert `session_name is not None`; `server.has_session(session_name)`; `not server.has_session(session_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_temporary_py_test_temp_session_cleanup_on_exception_1333901d0f | 3.2a through 3.7b | planned parity | -| tests/test/test_temporary.py::test_temp_session_creates_and_destroys | Test that temp_session creates and destroys a session; fixtures `server`; execute `server.has_session(session_name)`; `temp_session(server)`; assert `session_name is not None`; `server.has_session(session_name)`; `not server.has_session(session_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_temporary_py_test_temp_session_creates_and_destroys_dc9a9b32eb | 3.2a through 3.7b | planned parity | -| tests/test/test_temporary.py::test_temp_session_outside_context | Test that temp_session's finally block handles a session already killed; fixtures `server`; execute `server.has_session(session_name)`; `session.kill()`; `temp_session(server)`; assert `session_name is not None`; `server.has_session(session_name)`; `not server.has_session(session_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_temporary_py_test_temp_session_outside_context_3beb5436cc | 3.2a through 3.7b | planned parity | -| tests/test/test_temporary.py::test_temp_session_with_name | Test temp_session with a provided session name; fixtures `server`; execute `server.has_session(session_name)`; `temp_session(server, session_name=session_name)`; assert `session.session_name == session_name`; `server.has_session(session_name)`; `not server.has_session(session_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_temporary_py_test_temp_session_with_name_66ee13409a | 3.2a through 3.7b | planned parity | -| tests/test/test_temporary.py::test_temp_window_cleanup_on_exception | Test that temp_window cleans up even when an exception occurs; fixtures `session`; execute `RuntimeError()`; `temp_window(session)`; `any((w.window_id == window_id for w in session.windows))`; expect `pytest.raises(RuntimeError)`; assert `window_id is not None`; `len(session.windows) == initial_windows + 1`; `any((w.window_id == window_id for w in session.windows))`; `len(session.windows) == initial_windows`; `not any((w.window_id == window_id for w in session.windows))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_temporary_py_test_temp_window_cleanup_on_exception_f789bddb7e | 3.2a through 3.7b | planned parity | -| tests/test/test_temporary.py::test_temp_window_creates_and_destroys | Test that temp_window creates and destroys a window; fixtures `session`; execute `temp_window(session)`; `any((w.window_id == window_id for w in session.windows))`; assert `window_id is not None`; `len(session.windows) == initial_windows + 1`; `any((w.window_id == window_id for w in session.windows))`; `len(session.windows) == initial_windows`; `not any((w.window_id == window_id for w in session.windows))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_temporary_py_test_temp_window_creates_and_destroys_66d8452734 | 3.2a through 3.7b | planned parity | -| tests/test/test_temporary.py::test_temp_window_outside_context | Test that temp_window's finally block handles a window already killed; fixtures `session`; execute `window.kill()`; `temp_window(session)`; `any((w.window_id == window_id for w in session.windows))`; assert `window_id is not None`; `len(session.windows) == initial_windows + 1`; `len(session.windows) == initial_windows`; `not any((w.window_id == window_id for w in session.windows))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_temporary_py_test_temp_window_outside_context_99af61c3cf | 3.2a through 3.7b | planned parity | -| tests/test/test_temporary.py::test_temp_window_with_name | Test temp_window with a provided window name; fixtures `session`; execute `temp_window(session, window_name=window_name)`; `any((w.window_name == window_name for w in session.windows))`; assert `window.window_name == window_name`; `len(session.windows) == initial_windows + 1`; `any((w.window_name == window_name for w in session.windows))`; `len(session.windows) == initial_windows`; `not any((w.window_name == window_name for w in session.windows))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_temporary_py_test_temp_window_with_name_fd6ff5e195 | 3.2a through 3.7b | planned parity | -| tests/test_client.py::test_client_attached_pane_tracks_active_pane | ``client.attached_pane`` reflects the active pane in the active window; fixtures `control_mode`, `server`, `session`; execute `control_mode()`; assert `client is not None`; `isinstance(attached, Pane)`; `attached.pane_id == session.active_window.active_pane.pane_id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_client_py_test_client_attached_pane_tracks_active_pane_856648b529 | 3.2a through 3.7b | planned parity | -| tests/test_client.py::test_client_attached_properties_return_none_after_detach | ``attached_*`` returns ``None`` after the client leaves ``list-clients``; fixtures `control_mode`, `server`; execute `control_mode()`; assert `client is not None`; `client.attached_session is not None`; `client.attached_window is not None`; `client.attached_pane is not None`; `client.attached_session is None`; `client.attached_window is None`; `client.attached_pane is None`. | real-tmux integration test | PythonBehaviorParityTest#_test_client_py_test_client_attached_properties_return_none_after_detach_a94d84646a | 3.2a through 3.7b | planned parity | -| tests/test_client.py::test_client_attached_session_returns_typed_session | ``client.attached_session`` resolves to the live :class:`Session`; fixtures `control_mode`, `server`, `session`; execute `control_mode()`; assert `client is not None`; `isinstance(attached, Session)`; `attached.session_id == session.session_id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_client_py_test_client_attached_session_returns_typed_session_2db32a30ab | 3.2a through 3.7b | planned parity | -| tests/test_client.py::test_client_attached_window_tracks_active_window | ``client.attached_window`` reflects the live active window; fixtures `control_mode`, `server`, `session`; execute `control_mode()`; `session.new_window(window_name='attached_window_probe')`; `session.select_window(new_window.window_index)`; assert `client is not None`; `new_window.window_index is not None`; `isinstance(attached, Window)`; `attached.window_id == new_window.window_id`; `attached.window_id != snapshot_window_id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_client_py_test_client_attached_window_tracks_active_window_acbde7102f | 3.2a through 3.7b | planned parity | -| tests/test_client.py::test_client_readonly_default_zero | A non-readonly attached client reports ``client_readonly == "0"``; fixtures `control_mode`, `server`; execute `control_mode()`; assert `client is not None`; `client.client_readonly == '0'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_client_py_test_client_readonly_default_zero_d3363faa60 | 3.2a through 3.7b | planned parity | -| tests/test_client.py::test_client_refresh_raises_when_client_name_is_none | ``Client.refresh()`` raises ``ValueError`` when ``client_name`` is unset; fixtures `server`; execute `client.refresh()`; `Client(server=server)`; expect `pytest.raises(ValueError, match='client_name')`; assert `client.client_name is None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_client_py_test_client_refresh_raises_when_client_name_is_none_06a9b387e2 | 3.2a through 3.7b | planned parity | -| tests/test_client.py::test_client_refresh_rehydrates_fields | ``Client.refresh()`` repopulates fields from tmux's live state; fixtures `control_mode`, `server`; execute `control_mode()`; `client.refresh()`; `Client.from_client_name(server=server, client_name=ctl.client_name)`; assert `client.client_name == ctl.client_name`; `client.client_pid == original_pid`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_client_py_test_client_refresh_rehydrates_fields_a64a728b4e | 3.2a through 3.7b | planned parity | -| tests/test_client.py::test_client_session_reports_attached_session | ``client.client_session`` reports the session this client is attached to; fixtures `control_mode`, `server`, `session`; execute `control_mode()`; assert `client is not None`; `client.client_session == session.session_name`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_client_py_test_client_session_reports_attached_session_e2d9e0864a | 3.2a through 3.7b | planned parity | -| tests/test_client.py::test_clients_property_hydrates_cross_scope | ``Server.clients`` hydrates the client's active session/window/pane; fixtures `control_mode`, `server`; execute `control_mode()`; assert `client is not None`; `client.session_id is not None`; `client.window_id is not None`; `client.pane_id is not None`; `attached_session is not None`; `active_pane is not None`; `client.window_id == attached_session.active_window.window_id`; `client.pane_id == active_pane.pane_id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_client_py_test_clients_property_hydrates_cross_scope_43198e59a0 | 3.2a through 3.7b | planned parity | -| tests/test_client.py::test_resolve_attached_catches_no_active_window | ``_resolve_attached`` returns ``(session, None, None)`` on NoActiveWindow; fixtures `control_mode`, `server`, `monkeypatch`; execute `control_mode()`; `client._resolve_attached()`; `property(raise_no_active_window)`; assert `client is not None`; `resolved_session is not None`; `resolved_window is None`; `resolved_pane is None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_client_py_test_resolve_attached_catches_no_active_window_18b4dc6044 | 3.2a through 3.7b | planned parity | -| tests/test_client.py::test_resolve_attached_returns_full_triple_for_live_client | ``_resolve_attached`` returns ``(session, window, pane)`` for a live client; fixtures `control_mode`, `server`, `session`; execute `control_mode()`; `client._resolve_attached()`; assert `client is not None`; `resolved_session is not None`; `resolved_session.session_id == session.session_id`; `resolved_window is not None`; `resolved_pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#test_client_py_test_resolve_attached_returns_full_triple_for_live_client_f620ce2cb1 | 3.2a through 3.7b | planned parity | -| tests/test_client.py::test_resolve_attached_returns_none_triple_after_detach | ``_resolve_attached`` returns ``(None, None, None)`` after detach; fixtures `control_mode`, `server`; execute `control_mode()`; `client._resolve_attached()`; assert `client is not None`; `resolved == (None, None, None)`. | real-tmux integration test | PythonBehaviorParityTest#ts_test_client_py_test_resolve_attached_returns_none_triple_after_detach_4759334406 | 3.2a through 3.7b | planned parity | -| tests/test_client.py::test_server_clients_returns_querylist | ``Server.clients`` lists every attached tmux client as a :class:`Client`; fixtures `control_mode`, `server`; execute `control_mode()`; assert `len(clients) >= 1`; `isinstance(client, Client)`; `client.client_name is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_client_py_test_server_clients_returns_querylist_c3651cf820 | 3.2a through 3.7b | planned parity | +| tests/test/test_temporary.py::test_temp_session_cleanup_on_exception | Test that temp_session cleans up even when an exception occurs; fixtures `server`; execute `server.has_session(session_name)`; `RuntimeError()`; `temp_session(server)`; expect `pytest.raises(RuntimeError)`; assert `session_name is not None`; `server.has_session(session_name)`; `not server.has_session(session_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_temporary_py_test_temp_session_cleanup_on_exception_1333901d0f | 3.3a through 3.7c | planned parity | +| tests/test/test_temporary.py::test_temp_session_creates_and_destroys | Test that temp_session creates and destroys a session; fixtures `server`; execute `server.has_session(session_name)`; `temp_session(server)`; assert `session_name is not None`; `server.has_session(session_name)`; `not server.has_session(session_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_temporary_py_test_temp_session_creates_and_destroys_dc9a9b32eb | 3.3a through 3.7c | planned parity | +| tests/test/test_temporary.py::test_temp_session_outside_context | Test that temp_session's finally block handles a session already killed; fixtures `server`; execute `server.has_session(session_name)`; `session.kill()`; `temp_session(server)`; assert `session_name is not None`; `server.has_session(session_name)`; `not server.has_session(session_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_temporary_py_test_temp_session_outside_context_3beb5436cc | 3.3a through 3.7c | planned parity | +| tests/test/test_temporary.py::test_temp_session_with_name | Test temp_session with a provided session name; fixtures `server`; execute `server.has_session(session_name)`; `temp_session(server, session_name=session_name)`; assert `session.session_name == session_name`; `server.has_session(session_name)`; `not server.has_session(session_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_temporary_py_test_temp_session_with_name_66ee13409a | 3.3a through 3.7c | planned parity | +| tests/test/test_temporary.py::test_temp_window_cleanup_on_exception | Test that temp_window cleans up even when an exception occurs; fixtures `session`; execute `RuntimeError()`; `temp_window(session)`; `any((w.window_id == window_id for w in session.windows))`; expect `pytest.raises(RuntimeError)`; assert `window_id is not None`; `len(session.windows) == initial_windows + 1`; `any((w.window_id == window_id for w in session.windows))`; `len(session.windows) == initial_windows`; `not any((w.window_id == window_id for w in session.windows))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_temporary_py_test_temp_window_cleanup_on_exception_f789bddb7e | 3.3a through 3.7c | planned parity | +| tests/test/test_temporary.py::test_temp_window_creates_and_destroys | Test that temp_window creates and destroys a window; fixtures `session`; execute `temp_window(session)`; `any((w.window_id == window_id for w in session.windows))`; assert `window_id is not None`; `len(session.windows) == initial_windows + 1`; `any((w.window_id == window_id for w in session.windows))`; `len(session.windows) == initial_windows`; `not any((w.window_id == window_id for w in session.windows))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_temporary_py_test_temp_window_creates_and_destroys_66d8452734 | 3.3a through 3.7c | planned parity | +| tests/test/test_temporary.py::test_temp_window_outside_context | Test that temp_window's finally block handles a window already killed; fixtures `session`; execute `window.kill()`; `temp_window(session)`; `any((w.window_id == window_id for w in session.windows))`; assert `window_id is not None`; `len(session.windows) == initial_windows + 1`; `len(session.windows) == initial_windows`; `not any((w.window_id == window_id for w in session.windows))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_temporary_py_test_temp_window_outside_context_99af61c3cf | 3.3a through 3.7c | planned parity | +| tests/test/test_temporary.py::test_temp_window_with_name | Test temp_window with a provided window name; fixtures `session`; execute `temp_window(session, window_name=window_name)`; `any((w.window_name == window_name for w in session.windows))`; assert `window.window_name == window_name`; `len(session.windows) == initial_windows + 1`; `any((w.window_name == window_name for w in session.windows))`; `len(session.windows) == initial_windows`; `not any((w.window_name == window_name for w in session.windows))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_test_temporary_py_test_temp_window_with_name_fd6ff5e195 | 3.3a through 3.7c | planned parity | +| tests/test_client.py::test_client_attached_pane_tracks_active_pane | ``client.attached_pane`` reflects the active pane in the active window; fixtures `control_mode`, `server`, `session`; execute `control_mode()`; assert `client is not None`; `isinstance(attached, Pane)`; `attached.pane_id == session.active_window.active_pane.pane_id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_client_py_test_client_attached_pane_tracks_active_pane_856648b529 | 3.3a through 3.7c | planned parity | +| tests/test_client.py::test_client_attached_properties_return_none_after_detach | ``attached_*`` returns ``None`` after the client leaves ``list-clients``; fixtures `control_mode`, `server`; execute `control_mode()`; assert `client is not None`; `client.attached_session is not None`; `client.attached_window is not None`; `client.attached_pane is not None`; `client.attached_session is None`; `client.attached_window is None`; `client.attached_pane is None`. | real-tmux integration test | PythonBehaviorParityTest#_test_client_py_test_client_attached_properties_return_none_after_detach_a94d84646a | 3.3a through 3.7c | planned parity | +| tests/test_client.py::test_client_attached_session_returns_typed_session | ``client.attached_session`` resolves to the live :class:`Session`; fixtures `control_mode`, `server`, `session`; execute `control_mode()`; assert `client is not None`; `isinstance(attached, Session)`; `attached.session_id == session.session_id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_client_py_test_client_attached_session_returns_typed_session_2db32a30ab | 3.3a through 3.7c | planned parity | +| tests/test_client.py::test_client_attached_window_tracks_active_window | ``client.attached_window`` reflects the live active window; fixtures `control_mode`, `server`, `session`; execute `control_mode()`; `session.new_window(window_name='attached_window_probe')`; `session.select_window(new_window.window_index)`; assert `client is not None`; `new_window.window_index is not None`; `isinstance(attached, Window)`; `attached.window_id == new_window.window_id`; `attached.window_id != snapshot_window_id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_client_py_test_client_attached_window_tracks_active_window_acbde7102f | 3.3a through 3.7c | planned parity | +| tests/test_client.py::test_client_readonly_default_zero | A non-readonly attached client reports ``client_readonly == "0"``; fixtures `control_mode`, `server`; execute `control_mode()`; assert `client is not None`; `client.client_readonly == '0'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_client_py_test_client_readonly_default_zero_d3363faa60 | 3.3a through 3.7c | planned parity | +| tests/test_client.py::test_client_refresh_raises_when_client_name_is_none | ``Client.refresh()`` raises ``ValueError`` when ``client_name`` is unset; fixtures `server`; execute `client.refresh()`; `Client(server=server)`; expect `pytest.raises(ValueError, match='client_name')`; assert `client.client_name is None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_client_py_test_client_refresh_raises_when_client_name_is_none_06a9b387e2 | 3.3a through 3.7c | planned parity | +| tests/test_client.py::test_client_refresh_rehydrates_fields | ``Client.refresh()`` repopulates fields from tmux's live state; fixtures `control_mode`, `server`; execute `control_mode()`; `client.refresh()`; `Client.from_client_name(server=server, client_name=ctl.client_name)`; assert `client.client_name == ctl.client_name`; `client.client_pid == original_pid`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_client_py_test_client_refresh_rehydrates_fields_a64a728b4e | 3.3a through 3.7c | planned parity | +| tests/test_client.py::test_client_session_reports_attached_session | ``client.client_session`` reports the session this client is attached to; fixtures `control_mode`, `server`, `session`; execute `control_mode()`; assert `client is not None`; `client.client_session == session.session_name`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_client_py_test_client_session_reports_attached_session_e2d9e0864a | 3.3a through 3.7c | planned parity | +| tests/test_client.py::test_clients_property_hydrates_cross_scope | ``Server.clients`` hydrates the client's active session/window/pane; fixtures `control_mode`, `server`; execute `control_mode()`; assert `client is not None`; `client.session_id is not None`; `client.window_id is not None`; `client.pane_id is not None`; `attached_session is not None`; `active_pane is not None`; `client.window_id == attached_session.active_window.window_id`; `client.pane_id == active_pane.pane_id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_client_py_test_clients_property_hydrates_cross_scope_43198e59a0 | 3.3a through 3.7c | planned parity | +| tests/test_client.py::test_resolve_attached_catches_no_active_window | ``_resolve_attached`` returns ``(session, None, None)`` on NoActiveWindow; fixtures `control_mode`, `server`, `monkeypatch`; execute `control_mode()`; `client._resolve_attached()`; `property(raise_no_active_window)`; assert `client is not None`; `resolved_session is not None`; `resolved_window is None`; `resolved_pane is None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_client_py_test_resolve_attached_catches_no_active_window_18b4dc6044 | 3.3a through 3.7c | planned parity | +| tests/test_client.py::test_resolve_attached_returns_full_triple_for_live_client | ``_resolve_attached`` returns ``(session, window, pane)`` for a live client; fixtures `control_mode`, `server`, `session`; execute `control_mode()`; `client._resolve_attached()`; assert `client is not None`; `resolved_session is not None`; `resolved_session.session_id == session.session_id`; `resolved_window is not None`; `resolved_pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#test_client_py_test_resolve_attached_returns_full_triple_for_live_client_f620ce2cb1 | 3.3a through 3.7c | planned parity | +| tests/test_client.py::test_resolve_attached_returns_none_triple_after_detach | ``_resolve_attached`` returns ``(None, None, None)`` after detach; fixtures `control_mode`, `server`; execute `control_mode()`; `client._resolve_attached()`; assert `client is not None`; `resolved == (None, None, None)`. | real-tmux integration test | PythonBehaviorParityTest#ts_test_client_py_test_resolve_attached_returns_none_triple_after_detach_4759334406 | 3.3a through 3.7c | planned parity | +| tests/test_client.py::test_server_clients_returns_querylist | ``Server.clients`` lists every attached tmux client as a :class:`Client`; fixtures `control_mode`, `server`; execute `control_mode()`; assert `len(clients) >= 1`; `isinstance(client, Client)`; `client.client_name is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_client_py_test_server_clients_returns_querylist_c3651cf820 | 3.3a through 3.7c | planned parity | | tests/test_common.py::test_get_libtmux_version | Verify get_libtmux_version(); execute `get_libtmux_version()`; `LooseVersion(__version__)`; assert `isinstance(version, LooseVersion)`; `LooseVersion(__version__) == version`. | unit test | PythonBehaviorParityTest#tests_test_common_py_test_get_libtmux_version_d2b919b70b | not tmux-bound | planned parity | | tests/test_common.py::test_get_version_binary_swap_requires_explicit_cache_clear | Documents the sticky-cache trap when tmux_bin=None and PATH changes; fixtures `monkeypatch`; execute `_MockProc(versions[call_count['n']])`; `get_version.cache_clear()`; `get_version()`; assert `str(first) == '3.2'`; `str(second) == '3.2'`; `call_count['n'] == 1`; `str(third) == '3.6'`; `call_count['n'] == 2`. | unit test | PythonBehaviorParityTest#est_common_py_test_get_version_binary_swap_requires_explicit_cache_clear_20650db80d | not tmux-bound | planned parity | | tests/test_common.py::test_get_version_cache_clear_invalidates | cache_clear() forces a fresh subprocess on the next call; fixtures `monkeypatch`; execute `_MockProc()`; `get_version.cache_clear()`; `get_version()`; assert `call_count['n'] == 1`; `call_count['n'] == 2`. | unit test | PythonBehaviorParityTest#tests_test_common_py_test_get_version_cache_clear_invalidates_349ba05954 | not tmux-bound | planned parity | | tests/test_common.py::test_get_version_cache_keyed_by_tmux_bin | Different tmux_bin args cache independently; same arg revisits hit; fixtures `monkeypatch`; execute `_MockProc(versions[kwargs['tmux_bin']])`; `get_version.cache_clear()`; `get_version(tmux_bin='/path/a/tmux')`; `get_version(tmux_bin='/path/b/tmux')`; assert `a1 != b1`; `a1 == a2`; `call_count['n'] == 2`. | unit test | PythonBehaviorParityTest#tests_test_common_py_test_get_version_cache_keyed_by_tmux_bin_fd49c906d2 | not tmux-bound | planned parity | | tests/test_common.py::test_get_version_is_memoized_for_same_tmux_bin | Two calls with the same tmux_bin fork tmux -V once; fixtures `monkeypatch`; execute `_MockProc()`; `get_version.cache_clear()`; `get_version()`; assert `v1 == v2`; `call_count['n'] == 1`. | unit test | PythonBehaviorParityTest#tests_test_common_py_test_get_version_is_memoized_for_same_tmux_bin_6247d1cd0c | not tmux-bound | planned parity | | tests/test_common.py::test_has_version | Test has_version(); execute `has_version(str(get_version()))`; `get_version()`; assert `has_version(str(get_version()))`. | unit test | PythonBehaviorParityTest#tests_test_common_py_test_has_version_b831b79403 | not tmux-bound | planned parity | -| tests/test_common.py::test_libtmux_exception_subcommand_default_none | Backward-compat: existing call sites (no kwarg) get subcommand=None; execute `exc.LibTmuxException(['no last window'])`; assert `err.subcommand is None`; `'no last window' in str(err)`; `not str(err).startswith(':')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_common_py_test_libtmux_exception_subcommand_default_none_617370776f | 3.2a through 3.7b | planned parity | -| tests/test_common.py::test_libtmux_exception_subcommand_tags_str | When ``subcommand`` is set, str(exc) prefixes ``"<subcommand>: …"``; execute `exc.LibTmuxException(['no last window'], subcommand='last-window')`; assert `err.subcommand == 'last-window'`; `str(err).startswith('last-window:')`; `'no last window' in str(err)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_common_py_test_libtmux_exception_subcommand_tags_str_7a2963a317 | 3.2a through 3.7b | planned parity | -| tests/test_common.py::test_raise_if_stderr_no_stderr_is_noop | ``raise_if_stderr`` returns silently when proc.stderr is empty; fixtures `session`; execute `session.cmd('display-message', '-p', '#{version}')`; `raise_if_stderr(proc, 'display-message')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_common_py_test_raise_if_stderr_no_stderr_is_noop_f24c83c29f | 3.2a through 3.7b | planned parity | -| tests/test_common.py::test_raise_if_stderr_raises_with_subcommand_tag | ``raise_if_stderr`` raises ``LibTmuxException`` tagged with subcommand; fixtures `session`; execute `session.server.cmd('list-clients', '-t', '$nonexistent_session_id_for_test')`; `raise_if_stderr(proc, 'list-clients')`; expect `pytest.raises(exc.LibTmuxException)`; assert `proc.stderr`; `excinfo.value.subcommand == 'list-clients'`; `str(excinfo.value).startswith('list-clients:')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_common_py_test_raise_if_stderr_raises_with_subcommand_tag_16c9b0d53a | 3.2a through 3.7b | planned parity | -| tests/test_common.py::test_raise_if_stderr_str_shape_exact | Lock down ``str(exc)`` and ``exc.args[0]`` against future drift; fixtures `session`; execute `session.cmd('last-window')`; `raise_if_stderr(proc, 'last-window')`; expect `pytest.raises(exc.LibTmuxException)`; assert `proc.stderr == ['no last window']`; `str(excinfo.value) == 'last-window: no last window'`; `excinfo.value.args == ('no last window',)`; `excinfo.value.subcommand == 'last-window'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_common_py_test_raise_if_stderr_str_shape_exact_b600864546 | 3.2a through 3.7b | planned parity | +| tests/test_common.py::test_libtmux_exception_subcommand_default_none | Backward-compat: existing call sites (no kwarg) get subcommand=None; execute `exc.LibTmuxException(['no last window'])`; assert `err.subcommand is None`; `'no last window' in str(err)`; `not str(err).startswith(':')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_common_py_test_libtmux_exception_subcommand_default_none_617370776f | 3.3a through 3.7c | planned parity | +| tests/test_common.py::test_libtmux_exception_subcommand_tags_str | When ``subcommand`` is set, str(exc) prefixes ``"<subcommand>: …"``; execute `exc.LibTmuxException(['no last window'], subcommand='last-window')`; assert `err.subcommand == 'last-window'`; `str(err).startswith('last-window:')`; `'no last window' in str(err)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_common_py_test_libtmux_exception_subcommand_tags_str_7a2963a317 | 3.3a through 3.7c | planned parity | +| tests/test_common.py::test_raise_if_stderr_no_stderr_is_noop | ``raise_if_stderr`` returns silently when proc.stderr is empty; fixtures `session`; execute `session.cmd('display-message', '-p', '#{version}')`; `raise_if_stderr(proc, 'display-message')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_common_py_test_raise_if_stderr_no_stderr_is_noop_f24c83c29f | 3.3a through 3.7c | planned parity | +| tests/test_common.py::test_raise_if_stderr_raises_with_subcommand_tag | ``raise_if_stderr`` raises ``LibTmuxException`` tagged with subcommand; fixtures `session`; execute `session.server.cmd('list-clients', '-t', '$nonexistent_session_id_for_test')`; `raise_if_stderr(proc, 'list-clients')`; expect `pytest.raises(exc.LibTmuxException)`; assert `proc.stderr`; `excinfo.value.subcommand == 'list-clients'`; `str(excinfo.value).startswith('list-clients:')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_common_py_test_raise_if_stderr_raises_with_subcommand_tag_16c9b0d53a | 3.3a through 3.7c | planned parity | +| tests/test_common.py::test_raise_if_stderr_str_shape_exact | Lock down ``str(exc)`` and ``exc.args[0]`` against future drift; fixtures `session`; execute `session.cmd('last-window')`; `raise_if_stderr(proc, 'last-window')`; expect `pytest.raises(exc.LibTmuxException)`; assert `proc.stderr == ['no last window']`; `str(excinfo.value) == 'last-window: no last window'`; `excinfo.value.args == ('no last window',)`; `excinfo.value.subcommand == 'last-window'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_common_py_test_raise_if_stderr_str_shape_exact_b600864546 | 3.3a through 3.7c | planned parity | | tests/test_common.py::test_session_check_name[contains_colon] | Verify session_check_name(); case inputs `exc_msg_regex='contains colons'`, `raises=True`, `session_name='name: great session'`, `test_id='contains_colon'`; when `raises`: execute `session_check_name(session_name)`; expect `pytest.raises(exc.BadSessionName)`; when `raises and exc_msg_regex is not None`: execute `exc_info.match(exc_msg_regex)`; assert `exc_info.match(exc_msg_regex)`; when `not (raises)`: execute `session_check_name(session_name)`. | unit test | PythonBehaviorParityTest#tests_test_common_py_test_session_check_name_contains_colon_4ac24afe6e | not tmux-bound | planned parity | | tests/test_common.py::test_session_check_name[contains_period] | Verify session_check_name(); case inputs `exc_msg_regex='contains periods'`, `raises=True`, `session_name='my great session.'`, `test_id='contains_period'`; when `raises`: execute `session_check_name(session_name)`; expect `pytest.raises(exc.BadSessionName)`; when `raises and exc_msg_regex is not None`: execute `exc_info.match(exc_msg_regex)`; assert `exc_info.match(exc_msg_regex)`; when `not (raises)`: execute `session_check_name(session_name)`. | unit test | PythonBehaviorParityTest#tests_test_common_py_test_session_check_name_contains_period_7e14ce5df4 | not tmux-bound | planned parity | | tests/test_common.py::test_session_check_name[empty_string] | Verify session_check_name(); case inputs `exc_msg_regex='empty'`, `raises=True`, `session_name=''`, `test_id='empty_string'`; when `raises`: execute `session_check_name(session_name)`; expect `pytest.raises(exc.BadSessionName)`; when `raises and exc_msg_regex is not None`: execute `exc_info.match(exc_msg_regex)`; assert `exc_info.match(exc_msg_regex)`; when `not (raises)`: execute `session_check_name(session_name)`. | unit test | PythonBehaviorParityTest#tests_test_common_py_test_session_check_name_empty_string_662679af52 | not tmux-bound | planned parity | | tests/test_common.py::test_session_check_name[none_value] | Verify session_check_name(); case inputs `exc_msg_regex='empty'`, `raises=True`, `session_name=None`, `test_id='none_value'`; when `raises`: execute `session_check_name(session_name)`; expect `pytest.raises(exc.BadSessionName)`; when `raises and exc_msg_regex is not None`: execute `exc_info.match(exc_msg_regex)`; assert `exc_info.match(exc_msg_regex)`; when `not (raises)`: execute `session_check_name(session_name)`. | unit test | PythonBehaviorParityTest#tests_test_common_py_test_session_check_name_none_value_68fd2ff3d8 | not tmux-bound | planned parity | | tests/test_common.py::test_session_check_name[valid_name] | Verify session_check_name(); case inputs `exc_msg_regex=None`, `raises=False`, `session_name='new great session'`, `test_id='valid_name'`; when `raises`: execute `session_check_name(session_name)`; expect `pytest.raises(exc.BadSessionName)`; when `raises and exc_msg_regex is not None`: execute `exc_info.match(exc_msg_regex)`; assert `exc_info.match(exc_msg_regex)`; when `not (raises)`: execute `session_check_name(session_name)`. | unit test | PythonBehaviorParityTest#tests_test_common_py_test_session_check_name_valid_name_63cf81f9ed | not tmux-bound | planned parity | | tests/test_common.py::test_session_check_name[valid_with_special_chars] | Verify session_check_name(); case inputs `exc_msg_regex=None`, `raises=False`, `session_name='ajf8a3fa83fads,,,a'`, `test_id='valid_with_special_chars'`; when `raises`: execute `session_check_name(session_name)`; expect `pytest.raises(exc.BadSessionName)`; when `raises and exc_msg_regex is not None`: execute `exc_info.match(exc_msg_regex)`; assert `exc_info.match(exc_msg_regex)`; when `not (raises)`: execute `session_check_name(session_name)`. | unit test | PythonBehaviorParityTest#tests_test_common_py_test_session_check_name_valid_with_special_chars_4ddab0bea8 | not tmux-bound | planned parity | -| tests/test_common.py::test_tmux_cmd_format_separator_survives_non_utf8_locale | FORMAT_SEPARATOR must survive a non-UTF-8 locale round-trip through tmux_cmd; fixtures `session`; execute `server.cmd('list-sessions', f'-F{fmt_str}')`; `get_version(tmux_bin=server.tmux_bin)`; `get_output_format('list-sessions', tmux_version)`; `locale.setlocale(locale.LC_CTYPE)`; `locale.setlocale(locale.LC_CTYPE, 'C')`; `locale.setlocale(locale.LC_CTYPE, old_lc_ctype)`; assert `proc.stdout`; `FORMAT_SEPARATOR in line; message=f'FORMAT_SEPARATOR U+241E not found in output; got {line[:80]!r}... (likely decoded with wrong encoding)'`; `isinstance(result, dict)`; `'session_id' in result`. | real-tmux integration test | PythonBehaviorParityTest#s_test_common_py_test_tmux_cmd_format_separator_survives_non_utf8_locale_c6f0b55d39 | 3.2a through 3.7b | planned parity | -| tests/test_common.py::test_tmux_cmd_pre_execution_logging | Verify tmux_cmd logs command before execution; fixtures `caplog`, `server`; execute `server.cmd('list-sessions')`; assert `len(running_records) > 0`; `'list-sessions' in running_records[0].tmux_cmd`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_common_py_test_tmux_cmd_pre_execution_logging_b0644f6e64 | 3.2a through 3.7b | planned parity | +| tests/test_common.py::test_tmux_cmd_format_separator_survives_non_utf8_locale | FORMAT_SEPARATOR must survive a non-UTF-8 locale round-trip through tmux_cmd; fixtures `session`; execute `server.cmd('list-sessions', f'-F{fmt_str}')`; `get_version(tmux_bin=server.tmux_bin)`; `get_output_format('list-sessions', tmux_version)`; `locale.setlocale(locale.LC_CTYPE)`; `locale.setlocale(locale.LC_CTYPE, 'C')`; `locale.setlocale(locale.LC_CTYPE, old_lc_ctype)`; assert `proc.stdout`; `FORMAT_SEPARATOR in line; message=f'FORMAT_SEPARATOR U+241E not found in output; got {line[:80]!r}... (likely decoded with wrong encoding)'`; `isinstance(result, dict)`; `'session_id' in result`. | real-tmux integration test | PythonBehaviorParityTest#s_test_common_py_test_tmux_cmd_format_separator_survives_non_utf8_locale_c6f0b55d39 | 3.3a through 3.7c | planned parity | +| tests/test_common.py::test_tmux_cmd_pre_execution_logging | Verify tmux_cmd logs command before execution; fixtures `caplog`, `server`; execute `server.cmd('list-sessions')`; assert `len(running_records) > 0`; `'list-sessions' in running_records[0].tmux_cmd`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_common_py_test_tmux_cmd_pre_execution_logging_b0644f6e64 | 3.3a through 3.7c | planned parity | | tests/test_common.py::test_tmux_cmd_raises_on_not_found | Verify raises if tmux command not found; fixtures `monkeypatch`; execute `tmux_cmd('-V')`; expect `pytest.raises(exc.TmuxCommandNotFound)`. | unit test | PythonBehaviorParityTest#tests_test_common_py_test_tmux_cmd_raises_on_not_found_5194c943e2 | not tmux-bound | planned parity | -| tests/test_common.py::test_tmux_cmd_unicode | Verify tmux commands with unicode; fixtures `session`; execute `session.cmd('new-window', '-n', 'юникод', '-F', 'Ελληνικά', target=3)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_common_py_test_tmux_cmd_unicode_50412fe23b | 3.2a through 3.7b | planned parity | +| tests/test_common.py::test_tmux_cmd_unicode | Verify tmux commands with unicode; fixtures `session`; execute `session.cmd('new-window', '-n', 'юникод', '-F', 'Ελληνικά', target=3)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_common_py_test_tmux_cmd_unicode_50412fe23b | 3.3a through 3.7c | planned parity | | tests/test_common.py::test_version_comparison[gt_newer_version] | Test version comparison functions; case inputs `comparison_type='gt'`, `expected=False`, `test_id='gt_newer_version'`, `version='4.0'`; execute `comparison_funcs[comparison_type](version)`; assert `comparison_funcs[comparison_type](version) == expected`. | unit test | PythonBehaviorParityTest#tests_test_common_py_test_version_comparison_gt_newer_version_c68e9dd794 | not tmux-bound | planned parity | | tests/test_common.py::test_version_comparison[gt_newer_version_with_letter] | Test version comparison functions; case inputs `comparison_type='gt'`, `expected=False`, `test_id='gt_newer_version_with_letter'`, `version='4.0b'`; execute `comparison_funcs[comparison_type](version)`; assert `comparison_funcs[comparison_type](version) == expected`. | unit test | PythonBehaviorParityTest#ests_test_common_py_test_version_comparison_gt_newer_version_with_letter_88aa5d1ecd | not tmux-bound | planned parity | | tests/test_common.py::test_version_comparison[gt_older_version] | Test version comparison functions; case inputs `comparison_type='gt'`, `expected=True`, `test_id='gt_older_version'`, `version='1.6'`; execute `comparison_funcs[comparison_type](version)`; assert `comparison_funcs[comparison_type](version) == expected`. | unit test | PythonBehaviorParityTest#tests_test_common_py_test_version_comparison_gt_older_version_dadf70bcdb | not tmux-bound | planned parity | @@ -714,34 +714,34 @@ under [`tests/`](https://github.com/tmux-python/libtmux/blob/c4a980b/tests) and | tests/test_common.py::test_version_validation[accepts_version_1_8a] | Test version validation; case inputs `check_type='type_check'`, `exc_msg_regex=None`, `mock_min_version=None`, `mock_version='1.8a'`, `raises=False`, `test_id='accepts_version_1_8a'`; fixtures `monkeypatch`; when `mock_version is not None`: execute `LooseVersion(mock_version)`; when `check_type == 'min_version' and raises`: execute `has_minimum_version()`; expect `pytest.raises(exc.LibTmuxException)`; when `check_type == 'min_version' and raises and exc_msg_regex is not None`: execute `exc_info.match(exc_msg_regex)`; when `check_type == 'min_version' and not (raises)`: execute `has_minimum_version()`; assert `has_minimum_version()`; when `not (check_type == 'min_version') and check_type == 'type_check'`: execute `has_version(mock_version)`; assert `mock_version is not None`; `isinstance(has_version(mock_version), bool)`. | unit test | PythonBehaviorParityTest#tests_test_common_py_test_version_validation_accepts_version_1_8a_25fd520174 | not tmux-bound | planned parity | | tests/test_common.py::test_version_validation[accepts_version_1_9a] | Test version validation; case inputs `check_type='type_check'`, `exc_msg_regex=None`, `mock_min_version=None`, `mock_version='1.9a'`, `raises=False`, `test_id='accepts_version_1_9a'`; fixtures `monkeypatch`; when `mock_version is not None`: execute `LooseVersion(mock_version)`; when `check_type == 'min_version' and raises`: execute `has_minimum_version()`; expect `pytest.raises(exc.LibTmuxException)`; when `check_type == 'min_version' and raises and exc_msg_regex is not None`: execute `exc_info.match(exc_msg_regex)`; when `check_type == 'min_version' and not (raises)`: execute `has_minimum_version()`; assert `has_minimum_version()`; when `not (check_type == 'min_version') and check_type == 'type_check'`: execute `has_version(mock_version)`; assert `mock_version is not None`; `isinstance(has_version(mock_version), bool)`. | unit test | PythonBehaviorParityTest#tests_test_common_py_test_version_validation_accepts_version_1_9a_81515d6c56 | not tmux-bound | planned parity | | tests/test_common.py::test_version_validation[rejects_version_1_7] | Test version validation; case inputs `check_type='min_version'`, `exc_msg_regex='libtmux only supports'`, `mock_min_version=None`, `mock_version='1.7'`, `raises=True`, `test_id='rejects_version_1_7'`; fixtures `monkeypatch`; when `mock_version is not None`: execute `LooseVersion(mock_version)`; when `check_type == 'min_version' and raises`: execute `has_minimum_version()`; expect `pytest.raises(exc.LibTmuxException)`; when `check_type == 'min_version' and raises and exc_msg_regex is not None`: execute `exc_info.match(exc_msg_regex)`; when `check_type == 'min_version' and not (raises)`: execute `has_minimum_version()`; assert `has_minimum_version()`; when `not (check_type == 'min_version') and check_type == 'type_check'`: execute `has_version(mock_version)`; assert `mock_version is not None`; `isinstance(has_version(mock_version), bool)`. | unit test | PythonBehaviorParityTest#tests_test_common_py_test_version_validation_rejects_version_1_7_7d72731841 | not tmux-bound | planned parity | -| tests/test_control_mode.py::test_control_mode_cleanup | Client is removed after ControlMode context exits; fixtures `control_mode`, `server`; execute `control_mode()`; `server.list_clients()`; assert `len(server.list_clients()) > 0`; `len(clients) == 0`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_control_mode_py_test_control_mode_cleanup_46681c6797 | 3.2a through 3.7b | planned parity | -| tests/test_control_mode.py::test_control_mode_client_name | ControlMode.client_name contains the tmux client identifier; fixtures `control_mode`; execute `control_mode()`; assert `'client-' in ctl.client_name`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_control_mode_py_test_control_mode_client_name_269d4a1f8d | 3.2a through 3.7b | planned parity | -| tests/test_control_mode.py::test_control_mode_client_name_matches_spawned_client | ControlMode records the client name for its own subprocess; fixtures `control_mode`, `server`; execute `control_mode()`; `server.cmd('list-clients', '-F', '#{client_pid}\t#{client_name}')`; `line.split('\t', 1)`; assert `first.client_name != second.client_name`; `(str(first._proc.pid), first.client_name) in clients`; `(str(second._proc.pid), second.client_name) in clients`. | real-tmux integration test | PythonBehaviorParityTest#est_control_mode_py_test_control_mode_client_name_matches_spawned_client_f8cc559c57 | 3.2a through 3.7b | planned parity | -| tests/test_control_mode.py::test_control_mode_creates_client | ControlMode creates a client visible in list-clients; fixtures `control_mode`, `server`; execute `control_mode()`; `server.list_clients()`; assert `len(clients) > 0`; `ctl.client_name != ''`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_control_mode_py_test_control_mode_creates_client_f9dd07a48d | 3.2a through 3.7b | planned parity | -| tests/test_control_mode.py::test_control_mode_stdout_preserves_non_ascii_output | Control-mode stdout must preserve non-ASCII tmux output; fixtures `control_mode`; execute `control_mode()`; `locale.setlocale(locale.LC_CTYPE)`; `locale.setlocale(locale.LC_CTYPE, 'C')`; `os.write(ctl._write_fd, f"display-message -p '{FORMAT_SEPARATOR}'\n".encode())`; `f"display-message -p '{FORMAT_SEPARATOR}'\n".encode()`; `range(20)`; assert `ready; message='timed out waiting for control-mode output'`. | real-tmux integration test | PythonBehaviorParityTest#test_control_mode_py_test_control_mode_stdout_preserves_non_ascii_output_60ae2d1c8c | 3.2a through 3.7b | planned parity | -| tests/test_dataclasses.py::test_pane | Verify Pane dataclass object; fixtures `monkeypatch`, `tmp_path`, `server`; execute `server.new_session()`; `pane.refresh()`; `pane.resize(adjustment_direction=ResizeAdjustmentDirection.Down, adjustment=25)`; `pane.resize(adjustment_direction=ResizeAdjustmentDirection.Right, adjustment=25)`; `session.new_window(window_name='test')`; `session.windows.filter(window_name=window.window_name)`; assert `session_ is not None`; `pane_ is not None`; `pane_.pane_id is not None`; `isinstance(pane_.pane_id, str)`; `isinstance(pane, Pane)`; `old_pane_size != pane.pane_height`; `pane.pane_current_command is not None`; `pane.window is not None`; `isinstance(pane.window, Window)`; `pane.window.session is not None`; `isinstance(pane.window.session, Session)`; `pane.session is not None`; `isinstance(pane.session, Session)`; `pane.session.windows`; `isinstance(pane.session.windows, list)`; `len(pane.session.windows) > 0`; `isinstance(w, Window)`; `isinstance(w.session, Session)`; `w.session_id == pane.session.session_id`; `len(pane.session.panes) > 0`; `isinstance(_p, Pane)`; `isinstance(_p.session, Session)`; `_p.session_id == pane.session.session_id`; `len(pane.window.panes) > 0`; `len(session.windows) > 1`; `isinstance(_w, Window)`; `isinstance(_w.session, Session)`; `_w.window_name == window.window_name`; `len(window.panes) > 0`; `_p.window_id == window.window_id`; `new_pane.pane_id != pane.pane_id`; `new_pane.window_id == window.window_id`; `new_pane_2.pane_id != new_pane.pane_id`; `new_pane_2.window_id == new_pane.window_id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_dataclasses_py_test_pane_f73141ec81 | 3.2a through 3.7b | planned parity | -| tests/test_dataclasses.py::test_querylist | Verify QueryList behavior with libtmux object; fixtures `monkeypatch`, `tmp_path`, `session`; execute `session.new_window(window_name='test_2')`; `session.new_window(window_name='test_3')`; `server.new_session('second session')`; `server.windows.filter(window_name='test_2')`; `QueryList(session.windows)`; `qs.count(session.windows[0])`; expect `pytest.raises(ObjectDoesNotExist)`; `pytest.raises(MultipleObjectsReturned)`; assert `qs.count(session.windows[0]) == 1`; `len(qs) == 3`; `isinstance(w, Window)`; `w.window_name == 'test_2'`; `isinstance(w_2, Window)`; `w_2.window_name == 'test_2'`; `result == 'default_value'`; `len(server.windows.filter(window_name='test_2')) == 2`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_dataclasses_py_test_querylist_676e64c22f | 3.2a through 3.7b | planned parity | -| tests/test_from_env.py::test_from_env_agrees_with_tmux_on_a_linked_window | A window linked into two sessions gets tmux's own canonical answer; fixtures `session`; execute `server.new_session(session_name='zzz-guest')`; `server.cmd('link-window', '-s', window.window_id, '-t', guest.session_name)`; `server.cmd('display-message', '-p', '-t', pane.pane_id, '#{session_id}')`; `env_for(server, pane.pane_id, session.session_id)`; `Session.from_env(env)`; `Window.from_env(env)`; assert `pane is not None`; `pane.pane_id is not None`; `window.window_id is not None`; `session.session_id is not None`; `guest.session_name is not None`; `resolved == {tmux_says}`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_from_env_py_test_from_env_agrees_with_tmux_on_a_linked_window_d485ece78e | 3.2a through 3.7b | planned parity | -| tests/test_from_env.py::test_from_env_dead_server[pane] | An unreachable server is a tmux failure, not a missing object; case inputs `from_env=<bound method Pane.from_env of <class 'libtmux.pane.Pane'>>`, `test_id='pane'`; fixtures `TestServer`; execute `TestServer()`; `server.new_session(session_name='doomed')`; `server.kill()`; `from_env(env)`; `env_for(server, pane.pane_id, session.session_id)`; expect `pytest.raises(exc.LibTmuxException)`; assert `pane is not None`; `pane.pane_id is not None`; `session.session_id is not None`; `not isinstance(excinfo.value, ObjectDoesNotExist)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_from_env_py_test_from_env_dead_server_pane_f66b174e30 | 3.2a through 3.7b | planned parity | -| tests/test_from_env.py::test_from_env_dead_server[session] | An unreachable server is a tmux failure, not a missing object; case inputs `from_env=<bound method Session.from_env of <class 'libtmux.session.Session'>>`, `test_id='session'`; fixtures `TestServer`; execute `TestServer()`; `server.new_session(session_name='doomed')`; `server.kill()`; `from_env(env)`; `env_for(server, pane.pane_id, session.session_id)`; expect `pytest.raises(exc.LibTmuxException)`; assert `pane is not None`; `pane.pane_id is not None`; `session.session_id is not None`; `not isinstance(excinfo.value, ObjectDoesNotExist)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_from_env_py_test_from_env_dead_server_session_e6fa5dcc19 | 3.2a through 3.7b | planned parity | -| tests/test_from_env.py::test_from_env_dead_server[window] | An unreachable server is a tmux failure, not a missing object; case inputs `from_env=<bound method Window.from_env of <class 'libtmux.window.Window'>>`, `test_id='window'`; fixtures `TestServer`; execute `TestServer()`; `server.new_session(session_name='doomed')`; `server.kill()`; `from_env(env)`; `env_for(server, pane.pane_id, session.session_id)`; expect `pytest.raises(exc.LibTmuxException)`; assert `pane is not None`; `pane.pane_id is not None`; `session.session_id is not None`; `not isinstance(excinfo.value, ObjectDoesNotExist)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_from_env_py_test_from_env_dead_server_window_b9732d8eb6 | 3.2a through 3.7b | planned parity | -| tests/test_from_env.py::test_from_env_defaults_to_os_environ | ``env=None`` reads the live process environment, as a real pane would; fixtures `session`, `monkeypatch`; execute `env_for(session.server, pane.pane_id, session.session_id).items()`; `env_for(session.server, pane.pane_id, session.session_id)`; `Pane.from_env()`; `Window.from_env()`; `Session.from_env()`; `Server.from_env().is_alive()`; assert `pane is not None`; `pane.pane_id is not None`; `session.session_id is not None`; `Pane.from_env().pane_id == pane.pane_id`; `Window.from_env().window_id == session.active_window.window_id`; `Session.from_env().session_id == session.session_id`; `Server.from_env().is_alive()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_from_env_py_test_from_env_defaults_to_os_environ_9336b344e2 | 3.2a through 3.7b | planned parity | -| tests/test_from_env.py::test_from_env_follows_pane_after_move_window | A moved window resolves to its new session, not the one in ``$TMUX``; fixtures `session`; execute `server.new_session(session_name='destination')`; `session.new_window()`; `window.move_window(destination='99', session=destination.session_id)`; `env_for(server, pane.pane_id, session.session_id)`; `Session.from_env(env)`; `Window.from_env(env)`; assert `pane is not None`; `pane.pane_id is not None`; `session.session_id is not None`; `destination.session_id is not None`; `Session.from_env(env).session_id == destination.session_id`; `Window.from_env(env).session.session_id == destination.session_id`; `Pane.from_env(env).session.session_id == destination.session_id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_from_env_py_test_from_env_follows_pane_after_move_window_7906d90561 | 3.2a through 3.7b | planned parity | -| tests/test_from_env.py::test_from_env_ignores_stale_session_id | The session id inside ``$TMUX`` is never read, however wrong it is; fixtures `session`; execute `env_for(session.server, pane.pane_id, '$99999')`; `Session.from_env(env)`; assert `pane is not None`; `pane.pane_id is not None`; `session.session_id is not None`; `Session.from_env(env).session_id == session.session_id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_from_env_py_test_from_env_ignores_stale_session_id_e89ba6b076 | 3.2a through 3.7b | planned parity | -| tests/test_from_env.py::test_from_env_missing_pane_on_live_server[pane] | A vanished pane on a reachable server is an object-lookup failure; case inputs `from_env=<bound method Pane.from_env of <class 'libtmux.pane.Pane'>>`, `test_id='pane'`; fixtures `session`; execute `from_env(env)`; `env_for(session.server, pane.pane_id, session.session_id)`; expect `pytest.raises(exc.TmuxObjectDoesNotExist)`; assert `pane is not None`; `pane.pane_id is not None`; `session.session_id is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_from_env_py_test_from_env_missing_pane_on_live_server_pane_7e4a382775 | 3.2a through 3.7b | planned parity | -| tests/test_from_env.py::test_from_env_missing_pane_on_live_server[session] | A vanished pane on a reachable server is an object-lookup failure; case inputs `from_env=<bound method Session.from_env of <class 'libtmux.session.Session'>>`, `test_id='session'`; fixtures `session`; execute `from_env(env)`; `env_for(session.server, pane.pane_id, session.session_id)`; expect `pytest.raises(exc.TmuxObjectDoesNotExist)`; assert `pane is not None`; `pane.pane_id is not None`; `session.session_id is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_from_env_py_test_from_env_missing_pane_on_live_server_session_fd3e981d60 | 3.2a through 3.7b | planned parity | -| tests/test_from_env.py::test_from_env_missing_pane_on_live_server[window] | A vanished pane on a reachable server is an object-lookup failure; case inputs `from_env=<bound method Window.from_env of <class 'libtmux.window.Window'>>`, `test_id='window'`; fixtures `session`; execute `from_env(env)`; `env_for(session.server, pane.pane_id, session.session_id)`; expect `pytest.raises(exc.TmuxObjectDoesNotExist)`; assert `pane is not None`; `pane.pane_id is not None`; `session.session_id is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_from_env_py_test_from_env_missing_pane_on_live_server_window_eb72897570 | 3.2a through 3.7b | planned parity | +| tests/test_control_mode.py::test_control_mode_cleanup | Client is removed after ControlMode context exits; fixtures `control_mode`, `server`; execute `control_mode()`; `server.list_clients()`; assert `len(server.list_clients()) > 0`; `len(clients) == 0`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_control_mode_py_test_control_mode_cleanup_46681c6797 | 3.3a through 3.7c | planned parity | +| tests/test_control_mode.py::test_control_mode_client_name | ControlMode.client_name contains the tmux client identifier; fixtures `control_mode`; execute `control_mode()`; assert `'client-' in ctl.client_name`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_control_mode_py_test_control_mode_client_name_269d4a1f8d | 3.3a through 3.7c | planned parity | +| tests/test_control_mode.py::test_control_mode_client_name_matches_spawned_client | ControlMode records the client name for its own subprocess; fixtures `control_mode`, `server`; execute `control_mode()`; `server.cmd('list-clients', '-F', '#{client_pid}\t#{client_name}')`; `line.split('\t', 1)`; assert `first.client_name != second.client_name`; `(str(first._proc.pid), first.client_name) in clients`; `(str(second._proc.pid), second.client_name) in clients`. | real-tmux integration test | PythonBehaviorParityTest#est_control_mode_py_test_control_mode_client_name_matches_spawned_client_f8cc559c57 | 3.3a through 3.7c | planned parity | +| tests/test_control_mode.py::test_control_mode_creates_client | ControlMode creates a client visible in list-clients; fixtures `control_mode`, `server`; execute `control_mode()`; `server.list_clients()`; assert `len(clients) > 0`; `ctl.client_name != ''`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_control_mode_py_test_control_mode_creates_client_f9dd07a48d | 3.3a through 3.7c | planned parity | +| tests/test_control_mode.py::test_control_mode_stdout_preserves_non_ascii_output | Control-mode stdout must preserve non-ASCII tmux output; fixtures `control_mode`; execute `control_mode()`; `locale.setlocale(locale.LC_CTYPE)`; `locale.setlocale(locale.LC_CTYPE, 'C')`; `os.write(ctl._write_fd, f"display-message -p '{FORMAT_SEPARATOR}'\n".encode())`; `f"display-message -p '{FORMAT_SEPARATOR}'\n".encode()`; `range(20)`; assert `ready; message='timed out waiting for control-mode output'`. | real-tmux integration test | PythonBehaviorParityTest#test_control_mode_py_test_control_mode_stdout_preserves_non_ascii_output_60ae2d1c8c | 3.3a through 3.7c | planned parity | +| tests/test_dataclasses.py::test_pane | Verify Pane dataclass object; fixtures `monkeypatch`, `tmp_path`, `server`; execute `server.new_session()`; `pane.refresh()`; `pane.resize(adjustment_direction=ResizeAdjustmentDirection.Down, adjustment=25)`; `pane.resize(adjustment_direction=ResizeAdjustmentDirection.Right, adjustment=25)`; `session.new_window(window_name='test')`; `session.windows.filter(window_name=window.window_name)`; assert `session_ is not None`; `pane_ is not None`; `pane_.pane_id is not None`; `isinstance(pane_.pane_id, str)`; `isinstance(pane, Pane)`; `old_pane_size != pane.pane_height`; `pane.pane_current_command is not None`; `pane.window is not None`; `isinstance(pane.window, Window)`; `pane.window.session is not None`; `isinstance(pane.window.session, Session)`; `pane.session is not None`; `isinstance(pane.session, Session)`; `pane.session.windows`; `isinstance(pane.session.windows, list)`; `len(pane.session.windows) > 0`; `isinstance(w, Window)`; `isinstance(w.session, Session)`; `w.session_id == pane.session.session_id`; `len(pane.session.panes) > 0`; `isinstance(_p, Pane)`; `isinstance(_p.session, Session)`; `_p.session_id == pane.session.session_id`; `len(pane.window.panes) > 0`; `len(session.windows) > 1`; `isinstance(_w, Window)`; `isinstance(_w.session, Session)`; `_w.window_name == window.window_name`; `len(window.panes) > 0`; `_p.window_id == window.window_id`; `new_pane.pane_id != pane.pane_id`; `new_pane.window_id == window.window_id`; `new_pane_2.pane_id != new_pane.pane_id`; `new_pane_2.window_id == new_pane.window_id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_dataclasses_py_test_pane_f73141ec81 | 3.3a through 3.7c | planned parity | +| tests/test_dataclasses.py::test_querylist | Verify QueryList behavior with libtmux object; fixtures `monkeypatch`, `tmp_path`, `session`; execute `session.new_window(window_name='test_2')`; `session.new_window(window_name='test_3')`; `server.new_session('second session')`; `server.windows.filter(window_name='test_2')`; `QueryList(session.windows)`; `qs.count(session.windows[0])`; expect `pytest.raises(ObjectDoesNotExist)`; `pytest.raises(MultipleObjectsReturned)`; assert `qs.count(session.windows[0]) == 1`; `len(qs) == 3`; `isinstance(w, Window)`; `w.window_name == 'test_2'`; `isinstance(w_2, Window)`; `w_2.window_name == 'test_2'`; `result == 'default_value'`; `len(server.windows.filter(window_name='test_2')) == 2`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_dataclasses_py_test_querylist_676e64c22f | 3.3a through 3.7c | planned parity | +| tests/test_from_env.py::test_from_env_agrees_with_tmux_on_a_linked_window | A window linked into two sessions gets tmux's own canonical answer; fixtures `session`; execute `server.new_session(session_name='zzz-guest')`; `server.cmd('link-window', '-s', window.window_id, '-t', guest.session_name)`; `server.cmd('display-message', '-p', '-t', pane.pane_id, '#{session_id}')`; `env_for(server, pane.pane_id, session.session_id)`; `Session.from_env(env)`; `Window.from_env(env)`; assert `pane is not None`; `pane.pane_id is not None`; `window.window_id is not None`; `session.session_id is not None`; `guest.session_name is not None`; `resolved == {tmux_says}`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_from_env_py_test_from_env_agrees_with_tmux_on_a_linked_window_d485ece78e | 3.3a through 3.7c | planned parity | +| tests/test_from_env.py::test_from_env_dead_server[pane] | An unreachable server is a tmux failure, not a missing object; case inputs `from_env=<bound method Pane.from_env of <class 'libtmux.pane.Pane'>>`, `test_id='pane'`; fixtures `TestServer`; execute `TestServer()`; `server.new_session(session_name='doomed')`; `server.kill()`; `from_env(env)`; `env_for(server, pane.pane_id, session.session_id)`; expect `pytest.raises(exc.LibTmuxException)`; assert `pane is not None`; `pane.pane_id is not None`; `session.session_id is not None`; `not isinstance(excinfo.value, ObjectDoesNotExist)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_from_env_py_test_from_env_dead_server_pane_f66b174e30 | 3.3a through 3.7c | planned parity | +| tests/test_from_env.py::test_from_env_dead_server[session] | An unreachable server is a tmux failure, not a missing object; case inputs `from_env=<bound method Session.from_env of <class 'libtmux.session.Session'>>`, `test_id='session'`; fixtures `TestServer`; execute `TestServer()`; `server.new_session(session_name='doomed')`; `server.kill()`; `from_env(env)`; `env_for(server, pane.pane_id, session.session_id)`; expect `pytest.raises(exc.LibTmuxException)`; assert `pane is not None`; `pane.pane_id is not None`; `session.session_id is not None`; `not isinstance(excinfo.value, ObjectDoesNotExist)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_from_env_py_test_from_env_dead_server_session_e6fa5dcc19 | 3.3a through 3.7c | planned parity | +| tests/test_from_env.py::test_from_env_dead_server[window] | An unreachable server is a tmux failure, not a missing object; case inputs `from_env=<bound method Window.from_env of <class 'libtmux.window.Window'>>`, `test_id='window'`; fixtures `TestServer`; execute `TestServer()`; `server.new_session(session_name='doomed')`; `server.kill()`; `from_env(env)`; `env_for(server, pane.pane_id, session.session_id)`; expect `pytest.raises(exc.LibTmuxException)`; assert `pane is not None`; `pane.pane_id is not None`; `session.session_id is not None`; `not isinstance(excinfo.value, ObjectDoesNotExist)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_from_env_py_test_from_env_dead_server_window_b9732d8eb6 | 3.3a through 3.7c | planned parity | +| tests/test_from_env.py::test_from_env_defaults_to_os_environ | ``env=None`` reads the live process environment, as a real pane would; fixtures `session`, `monkeypatch`; execute `env_for(session.server, pane.pane_id, session.session_id).items()`; `env_for(session.server, pane.pane_id, session.session_id)`; `Pane.from_env()`; `Window.from_env()`; `Session.from_env()`; `Server.from_env().is_alive()`; assert `pane is not None`; `pane.pane_id is not None`; `session.session_id is not None`; `Pane.from_env().pane_id == pane.pane_id`; `Window.from_env().window_id == session.active_window.window_id`; `Session.from_env().session_id == session.session_id`; `Server.from_env().is_alive()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_from_env_py_test_from_env_defaults_to_os_environ_9336b344e2 | 3.3a through 3.7c | planned parity | +| tests/test_from_env.py::test_from_env_follows_pane_after_move_window | A moved window resolves to its new session, not the one in ``$TMUX``; fixtures `session`; execute `server.new_session(session_name='destination')`; `session.new_window()`; `window.move_window(destination='99', session=destination.session_id)`; `env_for(server, pane.pane_id, session.session_id)`; `Session.from_env(env)`; `Window.from_env(env)`; assert `pane is not None`; `pane.pane_id is not None`; `session.session_id is not None`; `destination.session_id is not None`; `Session.from_env(env).session_id == destination.session_id`; `Window.from_env(env).session.session_id == destination.session_id`; `Pane.from_env(env).session.session_id == destination.session_id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_from_env_py_test_from_env_follows_pane_after_move_window_7906d90561 | 3.3a through 3.7c | planned parity | +| tests/test_from_env.py::test_from_env_ignores_stale_session_id | The session id inside ``$TMUX`` is never read, however wrong it is; fixtures `session`; execute `env_for(session.server, pane.pane_id, '$99999')`; `Session.from_env(env)`; assert `pane is not None`; `pane.pane_id is not None`; `session.session_id is not None`; `Session.from_env(env).session_id == session.session_id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_from_env_py_test_from_env_ignores_stale_session_id_e89ba6b076 | 3.3a through 3.7c | planned parity | +| tests/test_from_env.py::test_from_env_missing_pane_on_live_server[pane] | A vanished pane on a reachable server is an object-lookup failure; case inputs `from_env=<bound method Pane.from_env of <class 'libtmux.pane.Pane'>>`, `test_id='pane'`; fixtures `session`; execute `from_env(env)`; `env_for(session.server, pane.pane_id, session.session_id)`; expect `pytest.raises(exc.TmuxObjectDoesNotExist)`; assert `pane is not None`; `pane.pane_id is not None`; `session.session_id is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_from_env_py_test_from_env_missing_pane_on_live_server_pane_7e4a382775 | 3.3a through 3.7c | planned parity | +| tests/test_from_env.py::test_from_env_missing_pane_on_live_server[session] | A vanished pane on a reachable server is an object-lookup failure; case inputs `from_env=<bound method Session.from_env of <class 'libtmux.session.Session'>>`, `test_id='session'`; fixtures `session`; execute `from_env(env)`; `env_for(session.server, pane.pane_id, session.session_id)`; expect `pytest.raises(exc.TmuxObjectDoesNotExist)`; assert `pane is not None`; `pane.pane_id is not None`; `session.session_id is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_from_env_py_test_from_env_missing_pane_on_live_server_session_fd3e981d60 | 3.3a through 3.7c | planned parity | +| tests/test_from_env.py::test_from_env_missing_pane_on_live_server[window] | A vanished pane on a reachable server is an object-lookup failure; case inputs `from_env=<bound method Window.from_env of <class 'libtmux.window.Window'>>`, `test_id='window'`; fixtures `session`; execute `from_env(env)`; `env_for(session.server, pane.pane_id, session.session_id)`; expect `pytest.raises(exc.TmuxObjectDoesNotExist)`; assert `pane is not None`; `pane.pane_id is not None`; `session.session_id is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_from_env_py_test_from_env_missing_pane_on_live_server_window_eb72897570 | 3.3a through 3.7c | planned parity | | tests/test_from_env.py::test_from_env_outside_tmux[pane] | Every constructor rejects an environment with no tmux in it; case inputs `from_env=<bound method Pane.from_env of <class 'libtmux.pane.Pane'>>`, `test_id='pane'`; execute `from_env({})`; expect `pytest.raises(exc.NotInsideTmux)`. | unit test | PythonBehaviorParityTest#tests_test_from_env_py_test_from_env_outside_tmux_pane_cd8de04a8f | not tmux-bound | planned parity | | tests/test_from_env.py::test_from_env_outside_tmux[server] | Every constructor rejects an environment with no tmux in it; case inputs `from_env=<bound method Server.from_env of <class 'libtmux.server.Server'>>`, `test_id='server'`; execute `from_env({})`; expect `pytest.raises(exc.NotInsideTmux)`. | unit test | PythonBehaviorParityTest#tests_test_from_env_py_test_from_env_outside_tmux_server_84aef7c630 | not tmux-bound | planned parity | | tests/test_from_env.py::test_from_env_outside_tmux[session] | Every constructor rejects an environment with no tmux in it; case inputs `from_env=<bound method Session.from_env of <class 'libtmux.session.Session'>>`, `test_id='session'`; execute `from_env({})`; expect `pytest.raises(exc.NotInsideTmux)`. | unit test | PythonBehaviorParityTest#tests_test_from_env_py_test_from_env_outside_tmux_session_dd04f5eb25 | not tmux-bound | planned parity | | tests/test_from_env.py::test_from_env_outside_tmux[window] | Every constructor rejects an environment with no tmux in it; case inputs `from_env=<bound method Window.from_env of <class 'libtmux.window.Window'>>`, `test_id='window'`; execute `from_env({})`; expect `pytest.raises(exc.NotInsideTmux)`. | unit test | PythonBehaviorParityTest#tests_test_from_env_py_test_from_env_outside_tmux_window_b080771883 | not tmux-bound | planned parity | -| tests/test_from_env.py::test_from_env_pane_without_parent_id[session] | A pane that names no parent is a ``ValueError`` on both constructors; case inputs `from_env=<bound method Session.from_env of <class 'libtmux.session.Session'>>`, `message='Pane must have a session_id to resolve its session'`, `test_id='session'`; fixtures `server`, `monkeypatch`; execute `from_env({})`; `Pane(server=server, pane_id='%0')`; `classmethod(unidentified_pane)`; `re.escape(message)`; expect `pytest.raises(ValueError, match=re.escape(message))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_from_env_py_test_from_env_pane_without_parent_id_session_3d07657187 | 3.2a through 3.7b | planned parity | -| tests/test_from_env.py::test_from_env_pane_without_parent_id[window] | A pane that names no parent is a ``ValueError`` on both constructors; case inputs `from_env=<bound method Window.from_env of <class 'libtmux.window.Window'>>`, `message='Pane must have a window_id to resolve its window'`, `test_id='window'`; fixtures `server`, `monkeypatch`; execute `from_env({})`; `Pane(server=server, pane_id='%0')`; `classmethod(unidentified_pane)`; `re.escape(message)`; expect `pytest.raises(ValueError, match=re.escape(message))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_from_env_py_test_from_env_pane_without_parent_id_window_b75c580ec3 | 3.2a through 3.7b | planned parity | -| tests/test_from_env.py::test_from_env_resolves_the_calling_pane | The whole family agrees on the pane, window, and session; fixtures `session`; execute `env_for(session.server, pane.pane_id, session.session_id)`; `Pane.from_env(env)`; `Window.from_env(env)`; `Session.from_env(env)`; assert `pane is not None`; `pane.pane_id is not None`; `session.session_id is not None`; `Pane.from_env(env).pane_id == pane.pane_id`; `Window.from_env(env).window_id == window.window_id`; `Session.from_env(env).session_id == session.session_id`; `Pane.from_env(env).window.window_id == window.window_id`; `Pane.from_env(env).session.session_id == session.session_id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_from_env_py_test_from_env_resolves_the_calling_pane_153bf84a14 | 3.2a through 3.7b | planned parity | -| tests/test_from_env.py::test_from_env_returns_the_class_it_was_called_on[pane] | ``from_env`` honours ``cls``, so subclasses get their own type back; case inputs `subclass=<class 'tests.test_from_env.MyPane'>`, `test_id='pane'`; fixtures `session`; execute `subclass.from_env(env)`; `env_for(server, pane.pane_id, session.session_id)`; assert `pane is not None`; `pane.pane_id is not None`; `session.session_id is not None`; `type(subclass.from_env(env)) is subclass`. | real-tmux integration test | PythonBehaviorParityTest#s_test_from_env_py_test_from_env_returns_the_class_it_was_called_on_pane_0b13051b18 | 3.2a through 3.7b | planned parity | -| tests/test_from_env.py::test_from_env_returns_the_class_it_was_called_on[server] | ``from_env`` honours ``cls``, so subclasses get their own type back; case inputs `subclass=<class 'tests.test_from_env.MyServer'>`, `test_id='server'`; fixtures `session`; execute `subclass.from_env(env)`; `env_for(server, pane.pane_id, session.session_id)`; assert `pane is not None`; `pane.pane_id is not None`; `session.session_id is not None`; `type(subclass.from_env(env)) is subclass`. | real-tmux integration test | PythonBehaviorParityTest#test_from_env_py_test_from_env_returns_the_class_it_was_called_on_server_a32ffbdf10 | 3.2a through 3.7b | planned parity | -| tests/test_from_env.py::test_from_env_returns_the_class_it_was_called_on[session] | ``from_env`` honours ``cls``, so subclasses get their own type back; case inputs `subclass=<class 'tests.test_from_env.MySession'>`, `test_id='session'`; fixtures `session`; execute `subclass.from_env(env)`; `env_for(server, pane.pane_id, session.session_id)`; assert `pane is not None`; `pane.pane_id is not None`; `session.session_id is not None`; `type(subclass.from_env(env)) is subclass`. | real-tmux integration test | PythonBehaviorParityTest#est_from_env_py_test_from_env_returns_the_class_it_was_called_on_session_15125a3b92 | 3.2a through 3.7b | planned parity | -| tests/test_from_env.py::test_from_env_returns_the_class_it_was_called_on[window] | ``from_env`` honours ``cls``, so subclasses get their own type back; case inputs `subclass=<class 'tests.test_from_env.MyWindow'>`, `test_id='window'`; fixtures `session`; execute `subclass.from_env(env)`; `env_for(server, pane.pane_id, session.session_id)`; assert `pane is not None`; `pane.pane_id is not None`; `session.session_id is not None`; `type(subclass.from_env(env)) is subclass`. | real-tmux integration test | PythonBehaviorParityTest#test_from_env_py_test_from_env_returns_the_class_it_was_called_on_window_242d69a16a | 3.2a through 3.7b | planned parity | +| tests/test_from_env.py::test_from_env_pane_without_parent_id[session] | A pane that names no parent is a ``ValueError`` on both constructors; case inputs `from_env=<bound method Session.from_env of <class 'libtmux.session.Session'>>`, `message='Pane must have a session_id to resolve its session'`, `test_id='session'`; fixtures `server`, `monkeypatch`; execute `from_env({})`; `Pane(server=server, pane_id='%0')`; `classmethod(unidentified_pane)`; `re.escape(message)`; expect `pytest.raises(ValueError, match=re.escape(message))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_from_env_py_test_from_env_pane_without_parent_id_session_3d07657187 | 3.3a through 3.7c | planned parity | +| tests/test_from_env.py::test_from_env_pane_without_parent_id[window] | A pane that names no parent is a ``ValueError`` on both constructors; case inputs `from_env=<bound method Window.from_env of <class 'libtmux.window.Window'>>`, `message='Pane must have a window_id to resolve its window'`, `test_id='window'`; fixtures `server`, `monkeypatch`; execute `from_env({})`; `Pane(server=server, pane_id='%0')`; `classmethod(unidentified_pane)`; `re.escape(message)`; expect `pytest.raises(ValueError, match=re.escape(message))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_from_env_py_test_from_env_pane_without_parent_id_window_b75c580ec3 | 3.3a through 3.7c | planned parity | +| tests/test_from_env.py::test_from_env_resolves_the_calling_pane | The whole family agrees on the pane, window, and session; fixtures `session`; execute `env_for(session.server, pane.pane_id, session.session_id)`; `Pane.from_env(env)`; `Window.from_env(env)`; `Session.from_env(env)`; assert `pane is not None`; `pane.pane_id is not None`; `session.session_id is not None`; `Pane.from_env(env).pane_id == pane.pane_id`; `Window.from_env(env).window_id == window.window_id`; `Session.from_env(env).session_id == session.session_id`; `Pane.from_env(env).window.window_id == window.window_id`; `Pane.from_env(env).session.session_id == session.session_id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_from_env_py_test_from_env_resolves_the_calling_pane_153bf84a14 | 3.3a through 3.7c | planned parity | +| tests/test_from_env.py::test_from_env_returns_the_class_it_was_called_on[pane] | ``from_env`` honours ``cls``, so subclasses get their own type back; case inputs `subclass=<class 'tests.test_from_env.MyPane'>`, `test_id='pane'`; fixtures `session`; execute `subclass.from_env(env)`; `env_for(server, pane.pane_id, session.session_id)`; assert `pane is not None`; `pane.pane_id is not None`; `session.session_id is not None`; `type(subclass.from_env(env)) is subclass`. | real-tmux integration test | PythonBehaviorParityTest#s_test_from_env_py_test_from_env_returns_the_class_it_was_called_on_pane_0b13051b18 | 3.3a through 3.7c | planned parity | +| tests/test_from_env.py::test_from_env_returns_the_class_it_was_called_on[server] | ``from_env`` honours ``cls``, so subclasses get their own type back; case inputs `subclass=<class 'tests.test_from_env.MyServer'>`, `test_id='server'`; fixtures `session`; execute `subclass.from_env(env)`; `env_for(server, pane.pane_id, session.session_id)`; assert `pane is not None`; `pane.pane_id is not None`; `session.session_id is not None`; `type(subclass.from_env(env)) is subclass`. | real-tmux integration test | PythonBehaviorParityTest#test_from_env_py_test_from_env_returns_the_class_it_was_called_on_server_a32ffbdf10 | 3.3a through 3.7c | planned parity | +| tests/test_from_env.py::test_from_env_returns_the_class_it_was_called_on[session] | ``from_env`` honours ``cls``, so subclasses get their own type back; case inputs `subclass=<class 'tests.test_from_env.MySession'>`, `test_id='session'`; fixtures `session`; execute `subclass.from_env(env)`; `env_for(server, pane.pane_id, session.session_id)`; assert `pane is not None`; `pane.pane_id is not None`; `session.session_id is not None`; `type(subclass.from_env(env)) is subclass`. | real-tmux integration test | PythonBehaviorParityTest#est_from_env_py_test_from_env_returns_the_class_it_was_called_on_session_15125a3b92 | 3.3a through 3.7c | planned parity | +| tests/test_from_env.py::test_from_env_returns_the_class_it_was_called_on[window] | ``from_env`` honours ``cls``, so subclasses get their own type back; case inputs `subclass=<class 'tests.test_from_env.MyWindow'>`, `test_id='window'`; fixtures `session`; execute `subclass.from_env(env)`; `env_for(server, pane.pane_id, session.session_id)`; assert `pane is not None`; `pane.pane_id is not None`; `session.session_id is not None`; `type(subclass.from_env(env)) is subclass`. | real-tmux integration test | PythonBehaviorParityTest#test_from_env_py_test_from_env_returns_the_class_it_was_called_on_window_242d69a16a | 3.3a through 3.7c | planned parity | | tests/test_from_env.py::test_pane_from_env_bad_pane_id[empty] | ``$TMUX_PANE`` must carry the ``%`` sigil tmux routes targets by; case inputs `message='$TMUX_PANE is unset or empty'`, `test_id='empty'`, `tmux_pane=''`; execute `re.escape(message)`; `Pane.from_env(env)`; expect `pytest.raises(exc.NotInsideTmux, match=re.escape(message))`. | unit test | PythonBehaviorParityTest#tests_test_from_env_py_test_pane_from_env_bad_pane_id_empty_6ae52c8f18 | not tmux-bound | planned parity | | tests/test_from_env.py::test_pane_from_env_bad_pane_id[session_sigil] | ``$TMUX_PANE`` must carry the ``%`` sigil tmux routes targets by; case inputs `message='$TMUX_PANE is not a pane id'`, `test_id='session_sigil'`, `tmux_pane='$1'`; execute `re.escape(message)`; `Pane.from_env(env)`; expect `pytest.raises(exc.NotInsideTmux, match=re.escape(message))`. | unit test | PythonBehaviorParityTest#tests_test_from_env_py_test_pane_from_env_bad_pane_id_session_sigil_9ab9824b53 | not tmux-bound | planned parity | | tests/test_from_env.py::test_pane_from_env_bad_pane_id[sigil_less] | ``$TMUX_PANE`` must carry the ``%`` sigil tmux routes targets by; case inputs `message='$TMUX_PANE is not a pane id'`, `test_id='sigil_less'`, `tmux_pane='0'`; execute `re.escape(message)`; `Pane.from_env(env)`; expect `pytest.raises(exc.NotInsideTmux, match=re.escape(message))`. | unit test | PythonBehaviorParityTest#tests_test_from_env_py_test_pane_from_env_bad_pane_id_sigil_less_0d668f5d24 | not tmux-bound | planned parity | @@ -754,114 +754,114 @@ under [`tests/`](https://github.com/tmux-python/libtmux/blob/c4a980b/tests) and | tests/test_from_env.py::test_server_from_env_socket_path[default_socket] | ``Server.from_env()`` splits ``$TMUX`` from the right; case inputs `expected_socket_path='/tmp/tmux-1000/default'`, `test_id='default_socket'`, `tmux='/tmp/tmux-1000/default,84215,0'`; execute `Server.from_env({'TMUX': tmux})`; assert `Server.from_env({'TMUX': tmux}).socket_path == expected_socket_path`. | unit test | PythonBehaviorParityTest#tests_test_from_env_py_test_server_from_env_socket_path_default_socket_36d69567ca | not tmux-bound | planned parity | | tests/test_from_env.py::test_server_from_env_socket_path[named_socket] | ``Server.from_env()`` splits ``$TMUX`` from the right; case inputs `expected_socket_path='/tmp/tmux-1000/libtmux_test0'`, `test_id='named_socket'`, `tmux='/tmp/tmux-1000/libtmux_test0,1,47'`; execute `Server.from_env({'TMUX': tmux})`; assert `Server.from_env({'TMUX': tmux}).socket_path == expected_socket_path`. | unit test | PythonBehaviorParityTest#tests_test_from_env_py_test_server_from_env_socket_path_named_socket_606800eb25 | not tmux-bound | planned parity | | tests/test_from_env.py::test_server_from_env_socket_path[no_session] | ``Server.from_env()`` splits ``$TMUX`` from the right; case inputs `expected_socket_path='/tmp/tmux-1000/default'`, `test_id='no_session'`, `tmux='/tmp/tmux-1000/default,84215,-1'`; execute `Server.from_env({'TMUX': tmux})`; assert `Server.from_env({'TMUX': tmux}).socket_path == expected_socket_path`. | unit test | PythonBehaviorParityTest#tests_test_from_env_py_test_server_from_env_socket_path_no_session_b66bbdeb91 | not tmux-bound | planned parity | -| tests/test_from_env.py::test_window_from_env_is_the_containing_window | The caller's window is reported, not the session's active window; fixtures `session`; execute `session.new_window(window_name='foreground', attach=True)`; `env_for(session.server, caller_pane.pane_id, session.session_id)`; `Window.from_env(env)`; assert `caller_pane is not None`; `caller_pane.pane_id is not None`; `session.session_id is not None`; `session.active_window.window_id == foreground.window_id`; `Window.from_env(env).window_id == caller_window.window_id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_from_env_py_test_window_from_env_is_the_containing_window_300cefa54e | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_scope_handling[pane_scope] | Test hooks at different scopes (session, window, pane); case inputs `test_case=HookScopeTestCase(test_id='pane_scope', scope='pane', scope_flag='-p', min_version='3.2')`; fixtures `server`; execute `server.new_session(session_name='test_scope')`; assert `window is not None`; `pane is not None`; `result is not None`; `len(result) > 0`; `'display-message' in result[0]`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `test_case.scope == 'session'`: execute `session.set_hook(f'{hook_name}[0]', hook_cmd)`; `session._show_hook(f'{hook_name}[0]')`; when `not (test_case.scope == 'session') and test_case.scope == 'window'`: execute `window.set_hook(f'{hook_name}[0]', hook_cmd)`; `window._show_hook(f'{hook_name}[0]')`; when `not (test_case.scope == 'session') and not (test_case.scope == 'window')`: execute `pane.set_hook(f'{hook_name}[0]', hook_cmd)`; `pane._show_hook(f'{hook_name}[0]')`; when `test_case.scope == 'session'`: execute `session.unset_hook(hook_name)`; when `not (test_case.scope == 'session') and test_case.scope == 'window'`: execute `window.unset_hook(hook_name)`; when `not (test_case.scope == 'session') and not (test_case.scope == 'window')`: execute `pane.unset_hook(hook_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_scope_handling_pane_scope_0ae0362c90 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_scope_handling[session_scope] | Test hooks at different scopes (session, window, pane); case inputs `test_case=HookScopeTestCase(test_id='session_scope', scope='session', scope_flag='', min_version='3.2')`; fixtures `server`; execute `server.new_session(session_name='test_scope')`; assert `window is not None`; `pane is not None`; `result is not None`; `len(result) > 0`; `'display-message' in result[0]`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `test_case.scope == 'session'`: execute `session.set_hook(f'{hook_name}[0]', hook_cmd)`; `session._show_hook(f'{hook_name}[0]')`; when `not (test_case.scope == 'session') and test_case.scope == 'window'`: execute `window.set_hook(f'{hook_name}[0]', hook_cmd)`; `window._show_hook(f'{hook_name}[0]')`; when `not (test_case.scope == 'session') and not (test_case.scope == 'window')`: execute `pane.set_hook(f'{hook_name}[0]', hook_cmd)`; `pane._show_hook(f'{hook_name}[0]')`; when `test_case.scope == 'session'`: execute `session.unset_hook(hook_name)`; when `not (test_case.scope == 'session') and test_case.scope == 'window'`: execute `window.unset_hook(hook_name)`; when `not (test_case.scope == 'session') and not (test_case.scope == 'window')`: execute `pane.unset_hook(hook_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_scope_handling_session_scope_797011f03e | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_scope_handling[window_scope] | Test hooks at different scopes (session, window, pane); case inputs `test_case=HookScopeTestCase(test_id='window_scope', scope='window', scope_flag='-w', min_version='3.2')`; fixtures `server`; execute `server.new_session(session_name='test_scope')`; assert `window is not None`; `pane is not None`; `result is not None`; `len(result) > 0`; `'display-message' in result[0]`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `test_case.scope == 'session'`: execute `session.set_hook(f'{hook_name}[0]', hook_cmd)`; `session._show_hook(f'{hook_name}[0]')`; when `not (test_case.scope == 'session') and test_case.scope == 'window'`: execute `window.set_hook(f'{hook_name}[0]', hook_cmd)`; `window._show_hook(f'{hook_name}[0]')`; when `not (test_case.scope == 'session') and not (test_case.scope == 'window')`: execute `pane.set_hook(f'{hook_name}[0]', hook_cmd)`; `pane._show_hook(f'{hook_name}[0]')`; when `test_case.scope == 'session'`: execute `session.unset_hook(hook_name)`; when `not (test_case.scope == 'session') and test_case.scope == 'window'`: execute `window.unset_hook(hook_name)`; when `not (test_case.scope == 'session') and not (test_case.scope == 'window')`: execute `pane.unset_hook(hook_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_scope_handling_window_scope_30bdffa08e | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_bind_key] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_bind_key', hook='after-bind-key', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_bind_key_2c1491243a | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_capture_pane] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_capture_pane', hook='after-capture-pane', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_capture_pane_1c20d0ded1 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_copy_mode] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_copy_mode', hook='after-copy-mode', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_copy_mode_7c66a39a9a | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_display_message] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_display_message', hook='after-display-message', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_display_message_66930d7867 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_display_panes] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_display_panes', hook='after-display-panes', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_display_panes_44d5e873cf | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_kill_pane] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_kill_pane', hook='after-kill-pane', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_kill_pane_1ee798d263 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_list_buffers] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_list_buffers', hook='after-list-buffers', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_list_buffers_6aeca01463 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_list_clients] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_list_clients', hook='after-list-clients', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_list_clients_d37ac3b45c | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_list_keys] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_list_keys', hook='after-list-keys', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_list_keys_574aa657f2 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_list_panes] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_list_panes', hook='after-list-panes', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_list_panes_9a3c42a812 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_list_sessions] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_list_sessions', hook='after-list-sessions', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_list_sessions_5b3a1af1ea | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_list_windows] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_list_windows', hook='after-list-windows', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_list_windows_e5b8040590 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_load_buffer] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_load_buffer', hook='after-load-buffer', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_load_buffer_abdf518ce9 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_lock_server] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_lock_server', hook='after-lock-server', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_lock_server_b00af3984b | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_new_session] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_new_session', hook='after-new-session', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_new_session_120d8eccad | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_new_window] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_new_window', hook='after-new-window', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_new_window_4dd3b3215a | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_paste_buffer] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_paste_buffer', hook='after-paste-buffer', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_paste_buffer_3b2a414e79 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_pipe_pane] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_pipe_pane', hook='after-pipe-pane', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_pipe_pane_9d8a00a513 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_queue] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_queue', hook='after-queue', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_queue_1b30f9cba2 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_refresh_client] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_refresh_client', hook='after-refresh-client', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_refresh_client_1c630dd3d2 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_rename_session] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_rename_session', hook='after-rename-session', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_rename_session_5a54759861 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_rename_window] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_rename_window', hook='after-rename-window', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_rename_window_1f06b456a5 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_resize_pane] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_resize_pane', hook='after-resize-pane', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_resize_pane_10b70feee1 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_resize_window] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_resize_window', hook='after-resize-window', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_resize_window_4d67871b80 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_save_buffer] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_save_buffer', hook='after-save-buffer', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_save_buffer_31e068490b | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_select_layout] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_select_layout', hook='after-select-layout', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_select_layout_74a1f70c57 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_select_pane] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_select_pane', hook='after-select-pane', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_select_pane_5874f76b06 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_select_window] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_select_window', hook='after-select-window', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_select_window_1efbcd9248 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_send_keys] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_send_keys', hook='after-send-keys', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_send_keys_103443f062 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_set_buffer] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_set_buffer', hook='after-set-buffer', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_set_buffer_76205dc9c9 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_set_environment] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_set_environment', hook='after-set-environment', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_set_environment_189cf1b62a | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_set_hook] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_set_hook', hook='after-set-hook', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_set_hook_0203929ead | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_set_option] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_set_option', hook='after-set-option', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_set_option_5cda0169a3 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_show_environment] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_show_environment', hook='after-show-environment', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_hooks_py_test_hook_set_show_unset_cycle_after_show_environment_1320ed1a70 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_show_messages] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_show_messages', hook='after-show-messages', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_show_messages_e4c7ec5e33 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_show_options] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_show_options', hook='after-show-options', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_show_options_2d663149f8 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_split_window] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_split_window', hook='after-split-window', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_split_window_da6e2cb721 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_unbind_key] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_unbind_key', hook='after-unbind-key', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_unbind_key_30ec6b9acf | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[alert_activity] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='alert_activity', hook='alert-activity', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_alert_activity_9729ffdbec | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[alert_bell] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='alert_bell', hook='alert-bell', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_alert_bell_14e21a960a | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[alert_silence] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='alert_silence', hook='alert-silence', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_alert_silence_afe03fe070 | 3.2a through 3.7b | planned parity | +| tests/test_from_env.py::test_window_from_env_is_the_containing_window | The caller's window is reported, not the session's active window; fixtures `session`; execute `session.new_window(window_name='foreground', attach=True)`; `env_for(session.server, caller_pane.pane_id, session.session_id)`; `Window.from_env(env)`; assert `caller_pane is not None`; `caller_pane.pane_id is not None`; `session.session_id is not None`; `session.active_window.window_id == foreground.window_id`; `Window.from_env(env).window_id == caller_window.window_id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_from_env_py_test_window_from_env_is_the_containing_window_300cefa54e | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_scope_handling[pane_scope] | Test hooks at different scopes (session, window, pane); case inputs `test_case=HookScopeTestCase(test_id='pane_scope', scope='pane', scope_flag='-p', min_version='3.2')`; fixtures `server`; execute `server.new_session(session_name='test_scope')`; assert `window is not None`; `pane is not None`; `result is not None`; `len(result) > 0`; `'display-message' in result[0]`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `test_case.scope == 'session'`: execute `session.set_hook(f'{hook_name}[0]', hook_cmd)`; `session._show_hook(f'{hook_name}[0]')`; when `not (test_case.scope == 'session') and test_case.scope == 'window'`: execute `window.set_hook(f'{hook_name}[0]', hook_cmd)`; `window._show_hook(f'{hook_name}[0]')`; when `not (test_case.scope == 'session') and not (test_case.scope == 'window')`: execute `pane.set_hook(f'{hook_name}[0]', hook_cmd)`; `pane._show_hook(f'{hook_name}[0]')`; when `test_case.scope == 'session'`: execute `session.unset_hook(hook_name)`; when `not (test_case.scope == 'session') and test_case.scope == 'window'`: execute `window.unset_hook(hook_name)`; when `not (test_case.scope == 'session') and not (test_case.scope == 'window')`: execute `pane.unset_hook(hook_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_scope_handling_pane_scope_0ae0362c90 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_scope_handling[session_scope] | Test hooks at different scopes (session, window, pane); case inputs `test_case=HookScopeTestCase(test_id='session_scope', scope='session', scope_flag='', min_version='3.2')`; fixtures `server`; execute `server.new_session(session_name='test_scope')`; assert `window is not None`; `pane is not None`; `result is not None`; `len(result) > 0`; `'display-message' in result[0]`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `test_case.scope == 'session'`: execute `session.set_hook(f'{hook_name}[0]', hook_cmd)`; `session._show_hook(f'{hook_name}[0]')`; when `not (test_case.scope == 'session') and test_case.scope == 'window'`: execute `window.set_hook(f'{hook_name}[0]', hook_cmd)`; `window._show_hook(f'{hook_name}[0]')`; when `not (test_case.scope == 'session') and not (test_case.scope == 'window')`: execute `pane.set_hook(f'{hook_name}[0]', hook_cmd)`; `pane._show_hook(f'{hook_name}[0]')`; when `test_case.scope == 'session'`: execute `session.unset_hook(hook_name)`; when `not (test_case.scope == 'session') and test_case.scope == 'window'`: execute `window.unset_hook(hook_name)`; when `not (test_case.scope == 'session') and not (test_case.scope == 'window')`: execute `pane.unset_hook(hook_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_scope_handling_session_scope_797011f03e | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_scope_handling[window_scope] | Test hooks at different scopes (session, window, pane); case inputs `test_case=HookScopeTestCase(test_id='window_scope', scope='window', scope_flag='-w', min_version='3.2')`; fixtures `server`; execute `server.new_session(session_name='test_scope')`; assert `window is not None`; `pane is not None`; `result is not None`; `len(result) > 0`; `'display-message' in result[0]`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `test_case.scope == 'session'`: execute `session.set_hook(f'{hook_name}[0]', hook_cmd)`; `session._show_hook(f'{hook_name}[0]')`; when `not (test_case.scope == 'session') and test_case.scope == 'window'`: execute `window.set_hook(f'{hook_name}[0]', hook_cmd)`; `window._show_hook(f'{hook_name}[0]')`; when `not (test_case.scope == 'session') and not (test_case.scope == 'window')`: execute `pane.set_hook(f'{hook_name}[0]', hook_cmd)`; `pane._show_hook(f'{hook_name}[0]')`; when `test_case.scope == 'session'`: execute `session.unset_hook(hook_name)`; when `not (test_case.scope == 'session') and test_case.scope == 'window'`: execute `window.unset_hook(hook_name)`; when `not (test_case.scope == 'session') and not (test_case.scope == 'window')`: execute `pane.unset_hook(hook_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_scope_handling_window_scope_30bdffa08e | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_bind_key] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_bind_key', hook='after-bind-key', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_bind_key_2c1491243a | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_capture_pane] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_capture_pane', hook='after-capture-pane', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_capture_pane_1c20d0ded1 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_copy_mode] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_copy_mode', hook='after-copy-mode', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_copy_mode_7c66a39a9a | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_display_message] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_display_message', hook='after-display-message', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_display_message_66930d7867 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_display_panes] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_display_panes', hook='after-display-panes', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_display_panes_44d5e873cf | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_kill_pane] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_kill_pane', hook='after-kill-pane', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_kill_pane_1ee798d263 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_list_buffers] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_list_buffers', hook='after-list-buffers', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_list_buffers_6aeca01463 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_list_clients] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_list_clients', hook='after-list-clients', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_list_clients_d37ac3b45c | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_list_keys] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_list_keys', hook='after-list-keys', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_list_keys_574aa657f2 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_list_panes] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_list_panes', hook='after-list-panes', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_list_panes_9a3c42a812 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_list_sessions] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_list_sessions', hook='after-list-sessions', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_list_sessions_5b3a1af1ea | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_list_windows] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_list_windows', hook='after-list-windows', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_list_windows_e5b8040590 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_load_buffer] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_load_buffer', hook='after-load-buffer', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_load_buffer_abdf518ce9 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_lock_server] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_lock_server', hook='after-lock-server', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_lock_server_b00af3984b | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_new_session] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_new_session', hook='after-new-session', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_new_session_120d8eccad | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_new_window] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_new_window', hook='after-new-window', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_new_window_4dd3b3215a | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_paste_buffer] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_paste_buffer', hook='after-paste-buffer', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_paste_buffer_3b2a414e79 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_pipe_pane] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_pipe_pane', hook='after-pipe-pane', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_pipe_pane_9d8a00a513 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_queue] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_queue', hook='after-queue', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_queue_1b30f9cba2 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_refresh_client] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_refresh_client', hook='after-refresh-client', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_refresh_client_1c630dd3d2 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_rename_session] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_rename_session', hook='after-rename-session', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_rename_session_5a54759861 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_rename_window] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_rename_window', hook='after-rename-window', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_rename_window_1f06b456a5 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_resize_pane] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_resize_pane', hook='after-resize-pane', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_resize_pane_10b70feee1 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_resize_window] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_resize_window', hook='after-resize-window', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_resize_window_4d67871b80 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_save_buffer] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_save_buffer', hook='after-save-buffer', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_save_buffer_31e068490b | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_select_layout] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_select_layout', hook='after-select-layout', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_select_layout_74a1f70c57 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_select_pane] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_select_pane', hook='after-select-pane', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_select_pane_5874f76b06 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_select_window] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_select_window', hook='after-select-window', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_select_window_1efbcd9248 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_send_keys] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_send_keys', hook='after-send-keys', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_send_keys_103443f062 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_set_buffer] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_set_buffer', hook='after-set-buffer', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_set_buffer_76205dc9c9 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_set_environment] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_set_environment', hook='after-set-environment', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_set_environment_189cf1b62a | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_set_hook] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_set_hook', hook='after-set-hook', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_set_hook_0203929ead | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_set_option] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_set_option', hook='after-set-option', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_set_option_5cda0169a3 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_show_environment] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_show_environment', hook='after-show-environment', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_hooks_py_test_hook_set_show_unset_cycle_after_show_environment_1320ed1a70 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_show_messages] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_show_messages', hook='after-show-messages', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_show_messages_e4c7ec5e33 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_show_options] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_show_options', hook='after-show-options', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_show_options_2d663149f8 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_split_window] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_split_window', hook='after-split-window', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_split_window_da6e2cb721 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[after_unbind_key] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='after_unbind_key', hook='after-unbind-key', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_after_unbind_key_30ec6b9acf | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[alert_activity] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='alert_activity', hook='alert-activity', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_alert_activity_9729ffdbec | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[alert_bell] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='alert_bell', hook='alert-bell', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_alert_bell_14e21a960a | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[alert_silence] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='alert_silence', hook='alert-silence', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_alert_silence_afe03fe070 | 3.3a through 3.7c | planned parity | | tests/test_hooks.py::test_hook_set_show_unset_cycle[client_active] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='client_active', hook='client-active', min_version='3.3', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_client_active_3b7874f832 | 3.3 through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[client_attached] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='client_attached', hook='client-attached', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_client_attached_0f29c4f5f0 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[client_detached] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='client_detached', hook='client-detached', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_client_detached_c681d32818 | 3.2a through 3.7b | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[client_attached] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='client_attached', hook='client-attached', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_client_attached_0f29c4f5f0 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[client_detached] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='client_detached', hook='client-detached', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_client_detached_c681d32818 | 3.3a through 3.7c | planned parity | | tests/test_hooks.py::test_hook_set_show_unset_cycle[client_focus_in] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='client_focus_in', hook='client-focus-in', min_version='3.3', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_client_focus_in_3f3ecd3b15 | 3.3 through 3.7b | planned parity | | tests/test_hooks.py::test_hook_set_show_unset_cycle[client_focus_out] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='client_focus_out', hook='client-focus-out', min_version='3.3', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_client_focus_out_41ac8543aa | 3.3 through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[client_resized] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='client_resized', hook='client-resized', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_client_resized_5e79f6adfd | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[client_session_changed] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='client_session_changed', hook='client-session-changed', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_hooks_py_test_hook_set_show_unset_cycle_client_session_changed_0a3b14cc14 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[pane_died] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='pane_died', hook='pane-died', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_pane_died_721b0c9b7e | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[pane_exited] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='pane_exited', hook='pane-exited', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_pane_exited_c0c96cf7af | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[pane_focus_in] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='pane_focus_in', hook='pane-focus-in', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_pane_focus_in_71117656f3 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[pane_focus_out] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='pane_focus_out', hook='pane-focus-out', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_pane_focus_out_e69f0137c3 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[pane_mode_changed] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='pane_mode_changed', hook='pane-mode-changed', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_pane_mode_changed_cca7b40d6f | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[pane_set_clipboard] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='pane_set_clipboard', hook='pane-set-clipboard', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_pane_set_clipboard_76f45b0002 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[session_closed] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='session_closed', hook='session-closed', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_session_closed_803af103b5 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[session_created] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='session_created', hook='session-created', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_session_created_418947221e | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[session_renamed] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='session_renamed', hook='session-renamed', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_session_renamed_876e23b9d2 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[session_window_changed] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='session_window_changed', hook='session-window-changed', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_hooks_py_test_hook_set_show_unset_cycle_session_window_changed_cd7ff74abf | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[window_linked] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='window_linked', hook='window-linked', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_window_linked_65f09a3c87 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[window_renamed] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='window_renamed', hook='window-renamed', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_window_renamed_0b579984c6 | 3.2a through 3.7b | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[client_resized] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='client_resized', hook='client-resized', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_client_resized_5e79f6adfd | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[client_session_changed] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='client_session_changed', hook='client-session-changed', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_hooks_py_test_hook_set_show_unset_cycle_client_session_changed_0a3b14cc14 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[pane_died] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='pane_died', hook='pane-died', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_pane_died_721b0c9b7e | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[pane_exited] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='pane_exited', hook='pane-exited', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_pane_exited_c0c96cf7af | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[pane_focus_in] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='pane_focus_in', hook='pane-focus-in', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_pane_focus_in_71117656f3 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[pane_focus_out] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='pane_focus_out', hook='pane-focus-out', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_pane_focus_out_e69f0137c3 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[pane_mode_changed] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='pane_mode_changed', hook='pane-mode-changed', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_pane_mode_changed_cca7b40d6f | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[pane_set_clipboard] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='pane_set_clipboard', hook='pane-set-clipboard', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_pane_set_clipboard_76f45b0002 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[session_closed] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='session_closed', hook='session-closed', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_session_closed_803af103b5 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[session_created] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='session_created', hook='session-created', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_session_created_418947221e | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[session_renamed] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='session_renamed', hook='session-renamed', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_session_renamed_876e23b9d2 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[session_window_changed] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='session_window_changed', hook='session-window-changed', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_hooks_py_test_hook_set_show_unset_cycle_session_window_changed_cd7ff74abf | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[window_linked] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='window_linked', hook='window-linked', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_window_linked_65f09a3c87 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[window_renamed] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='window_renamed', hook='window-renamed', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_window_renamed_0b579984c6 | 3.3a through 3.7c | planned parity | | tests/test_hooks.py::test_hook_set_show_unset_cycle[window_resized] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='window_resized', hook='window-resized', min_version='3.3', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_window_resized_bf9b817452 | 3.3 through 3.7b | planned parity | -| tests/test_hooks.py::test_hook_set_show_unset_cycle[window_unlinked] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='window_unlinked', hook='window-unlinked', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_window_unlinked_8ca5dda260 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hooks_dataclass | Tests for hooks dataclass; fixtures `server`; execute `server.new_session(session_name='test hooks')`; `session.cmd('set-hook', 'session-renamed[0]', 'set -g status-left-style bg=red')`; `server.cmd('show-hooks', 'session-renamed[0]')`; `session.cmd('set-hook', '-p', 'session-renamed[0]', 'set -g status-left-style bg=red')`; `session.cmd('set-hook', 'session-renamed[1]', 'set -g status-left-style bg=white')`; `session.cmd('set-hook', 'session-renamed[2]', 'set -g status-left-style bg=blue')`; assert `pane is not None`; `not set_hook_proc.stdout`; `not set_hook_proc.stderr`; `hooks.session_renamed.as_list() == ['set-option -g status-left-style bg=red']`; `session_out_processed.session_renamed is not None`; `session_out_processed.session_renamed[2] == 'set-option -g status-left-style bg=blue'`; `hooks.session_renamed.as_list() == ['set-option -g status-left-style bg=red', 'set-option -g status-left-style bg=white', 'set-option -g status-left-style bg=blue']`; `hooks.after_set_buffer.as_list() == ['set-option -g status-left-style bg=pink']`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hooks_dataclass_46244b6202 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hooks_mixin | Tests for hooks; fixtures `server`; execute `server.new_session(session_name='test hooks')`; `pane.set_hook('session-renamed[0]', 'set -g status-left-style bg=red')`; `pane.show_hooks()`; `pane._show_hook('session-renamed[0]')`; `Hooks.from_stdout(show_hooks_raw)`; `SparseArray({0: 'set-option -g status-left-style bg=red'})`; assert `pane is not None`; `not pane.show_hooks()`; `show_hooks_raw is not None`; `hooks.session_renamed == SparseArray({0: 'set-option -g status-left-style bg=red'})`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hooks_mixin_e114642bf6 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_hooks_raw_cmd | Raw hook set, show, unset via cmd; fixtures `server`; execute `server.new_session(session_name='test hooks')`; `server.cmd('set-hook', '-g', 'session-renamed[0]', 'set -g status-left-style bg=red')`; `server.cmd('show-hooks')`; `server.cmd('show-hooks', '-g', 'session-renamed[0]')`; `server.cmd('set-hook', '-g', '-u', 'session-renamed[0]')`; `server.cmd('set-hook', '-g', '-u', 'session-renamed')`; assert `pane is not None`; `not set_hook_proc.stdout`; `not set_hook_proc.stderr`; `not server.cmd('show-hooks').stdout`; `'set-option -g status-left-style bg=red' in show_hooks_proc.stdout[0]`; `server.cmd('show-hooks', '-g', 'session-renamed[0]').stdout == ['session-renamed[0] ']`; `not session.cmd('show-hooks', '-s').stdout`; `session.cmd('show-hooks', '-s', 'session-renamed[0]').stdout == []`; when `has_gte_version('3.2')`: execute `window.cmd('set-hook', '-w', 'session-renamed[0]', 'set -g status-left-style bg=red')`; `server.cmd('show-hooks', 'session-renamed[0]')`; `window.cmd('set-hook', '-w', '-u', 'session-renamed[0]')`; `window.cmd('show-hooks', '-w', 'session-renamed[0]')`; `window.cmd('set-hook', '-w', '-u', 'session-renamed')`; `pane.cmd('set-hook', '-p', 'session-renamed[0]', 'set -g status-left-style bg=red')`; assert `not set_hook_proc.stdout`; `not set_hook_proc.stderr`; `'set-option -g status-left-style bg=red' in show_hooks_proc.stdout[0]`; `window.cmd('show-hooks', '-w', 'session-renamed[0]').stdout == ['session-renamed[0] ']`; `window.cmd('show-hooks', '-w', 'session-renamed[0]').stdout == []`; `pane.cmd('show-hooks', '-p', 'session-renamed[0]').stdout == ['session-renamed[0] ']`; `pane.cmd('show-hooks', '-p', 'session-renamed[0]').stdout == []`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hooks_raw_cmd_4fc114cddd | 3.2a through 3.7b; conditional boundaries: 3.2 | planned parity | +| tests/test_hooks.py::test_hook_set_show_unset_cycle[window_unlinked] | Test set/show/unset cycle for each hook; case inputs `test_case=HookTestCase(test_id='window_unlinked', hook='window-unlinked', min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_hook_cycle')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `test_case.hook.replace('-', '_')`; `session.unset_hook(f'{test_case.hook}[0]')`; `Hooks.from_stdout(result)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected hook {test_case.hook} to be set'`; `hook_value is not None; message=f'Hook attribute {hook_attr} not found in Hooks'`; `len(hook_value) > 0; message=f'Expected hook {test_case.hook} to have values'`; `'display-message' in hook_value[0]; message=f"Expected 'display-message' in hook value, got: {hook_value[0]}"`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `result_after_unset`: execute `Hooks.from_stdout(result_after_unset)`; when `result_after_unset and hook_value_after`: execute `hook_value_after.as_list()`; assert `len(hook_value_after.as_list()) == 0 or hook_value_after[0] == ''; message=f'Expected hook {test_case.hook} to be unset'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hook_set_show_unset_cycle_window_unlinked_8ca5dda260 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hooks_dataclass | Tests for hooks dataclass; fixtures `server`; execute `server.new_session(session_name='test hooks')`; `session.cmd('set-hook', 'session-renamed[0]', 'set -g status-left-style bg=red')`; `server.cmd('show-hooks', 'session-renamed[0]')`; `session.cmd('set-hook', '-p', 'session-renamed[0]', 'set -g status-left-style bg=red')`; `session.cmd('set-hook', 'session-renamed[1]', 'set -g status-left-style bg=white')`; `session.cmd('set-hook', 'session-renamed[2]', 'set -g status-left-style bg=blue')`; assert `pane is not None`; `not set_hook_proc.stdout`; `not set_hook_proc.stderr`; `hooks.session_renamed.as_list() == ['set-option -g status-left-style bg=red']`; `session_out_processed.session_renamed is not None`; `session_out_processed.session_renamed[2] == 'set-option -g status-left-style bg=blue'`; `hooks.session_renamed.as_list() == ['set-option -g status-left-style bg=red', 'set-option -g status-left-style bg=white', 'set-option -g status-left-style bg=blue']`; `hooks.after_set_buffer.as_list() == ['set-option -g status-left-style bg=pink']`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hooks_dataclass_46244b6202 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hooks_mixin | Tests for hooks; fixtures `server`; execute `server.new_session(session_name='test hooks')`; `pane.set_hook('session-renamed[0]', 'set -g status-left-style bg=red')`; `pane.show_hooks()`; `pane._show_hook('session-renamed[0]')`; `Hooks.from_stdout(show_hooks_raw)`; `SparseArray({0: 'set-option -g status-left-style bg=red'})`; assert `pane is not None`; `not pane.show_hooks()`; `show_hooks_raw is not None`; `hooks.session_renamed == SparseArray({0: 'set-option -g status-left-style bg=red'})`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hooks_mixin_e114642bf6 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_hooks_raw_cmd | Raw hook set, show, unset via cmd; fixtures `server`; execute `server.new_session(session_name='test hooks')`; `server.cmd('set-hook', '-g', 'session-renamed[0]', 'set -g status-left-style bg=red')`; `server.cmd('show-hooks')`; `server.cmd('show-hooks', '-g', 'session-renamed[0]')`; `server.cmd('set-hook', '-g', '-u', 'session-renamed[0]')`; `server.cmd('set-hook', '-g', '-u', 'session-renamed')`; assert `pane is not None`; `not set_hook_proc.stdout`; `not set_hook_proc.stderr`; `not server.cmd('show-hooks').stdout`; `'set-option -g status-left-style bg=red' in show_hooks_proc.stdout[0]`; `server.cmd('show-hooks', '-g', 'session-renamed[0]').stdout == ['session-renamed[0] ']`; `not session.cmd('show-hooks', '-s').stdout`; `session.cmd('show-hooks', '-s', 'session-renamed[0]').stdout == []`; when `has_gte_version('3.2')`: execute `window.cmd('set-hook', '-w', 'session-renamed[0]', 'set -g status-left-style bg=red')`; `server.cmd('show-hooks', 'session-renamed[0]')`; `window.cmd('set-hook', '-w', '-u', 'session-renamed[0]')`; `window.cmd('show-hooks', '-w', 'session-renamed[0]')`; `window.cmd('set-hook', '-w', '-u', 'session-renamed')`; `pane.cmd('set-hook', '-p', 'session-renamed[0]', 'set -g status-left-style bg=red')`; assert `not set_hook_proc.stdout`; `not set_hook_proc.stderr`; `'set-option -g status-left-style bg=red' in show_hooks_proc.stdout[0]`; `window.cmd('show-hooks', '-w', 'session-renamed[0]').stdout == ['session-renamed[0] ']`; `window.cmd('show-hooks', '-w', 'session-renamed[0]').stdout == []`; `pane.cmd('show-hooks', '-p', 'session-renamed[0]').stdout == ['session-renamed[0] ']`; `pane.cmd('show-hooks', '-p', 'session-renamed[0]').stdout == []`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_hooks_raw_cmd_4fc114cddd | 3.3a through 3.7c; conditional boundaries: 3.2 | planned parity | | tests/test_hooks.py::test_new_hooks_version_gated[pane_title_changed] | Test new hooks that require tmux 3.5+; case inputs `test_case=HookTestCase(test_id='pane_title_changed', hook='pane-title-changed', min_version='3.5', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_new_hooks')`; `session.set_hook(f'{test_case.hook}[0]', hook_cmd)`; `session._show_hook(f'{test_case.hook}[0]')`; `session.unset_hook(f'{test_case.hook}[0]')`; assert `result is not None; message=f'Expected hook {test_case.hook} to be set'`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_new_hooks_version_gated_pane_title_changed_69b5502353 | 3.5 through 3.7b | planned parity | -| tests/test_hooks.py::test_run_hook_basic | Test run_hook() method exists and can be called without error; fixtures `server`; execute `server.new_session(session_name='test_run_hook')`; `session.set_hook('session-renamed[0]', 'display-message "test"')`; `session.run_hook('session-renamed[0]')`; `session.unset_hook('session-renamed')`; assert `result is session`; when `not has_gte_version('3.2')`: skip via `pytest.skip('Requires tmux >= 3.2')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_run_hook_basic_1e7330eab3 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_set_hook_append_flag | Test set_hook with append=True uses tmux's -a flag; fixtures `server`; execute `server.new_session(session_name='test_append_flag')`; `session.set_hook('session-renamed[0]', "display-message 'initial'")`; `session.set_hook('session-renamed', "display-message 'appended'", append=True)`; `session.show_hook('session-renamed')`; `session.unset_hook('session-renamed')`; assert `hooks is not None`; `isinstance(hooks, SparseArray)`; `len(hooks) == 2`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_set_hook_append_flag_916689e76b | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_set_hook_flag_combinations[append_and_global] | Test set_hook with various flag combinations; case inputs `test_case=SetHookFlagTestCase(test_id='append_and_global', flag_kwargs={'append': True, 'global_': True}, expected_behavior='appends', min_version='3.2')`; fixtures `server`; execute `server.new_session(session_name='test_flags')`; `session.unset_hook(hook_name)`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `test_case.expected_behavior == 'appends'`: execute `session.set_hook(f'{hook_name}[0]', "display-message 'initial'")`; `session.set_hook(hook_name, hook_cmd, **test_case.flag_kwargs)`; `parsed.session_renamed.as_list()`; assert `len(hook_values) >= 1`; when `test_case.expected_behavior == 'appends' and test_case.flag_kwargs.get('global_')`: execute `server.cmd('show-hooks', '-g', hook_name)`; `Hooks.from_stdout(result.stdout)`; assert `result.stdout is not None`; when `test_case.expected_behavior == 'appends' and not (test_case.flag_kwargs.get('global_'))`: execute `session._show_hook(hook_name)`; `Hooks.from_stdout(hooks)`; assert `hooks is not None`; when `not (test_case.expected_behavior == 'appends') and test_case.expected_behavior == 'global'`: execute `session.set_hook(f'{hook_name}[0]', hook_cmd, **test_case.flag_kwargs)`; `server.cmd('show-hooks', '-g', f'{hook_name}[0]')`; assert `result.stdout is not None`; `len(result.stdout) > 0`; `'display-message' in result.stdout[0]`; when `not (test_case.expected_behavior == 'appends') and not (test_case.expected_behavior == 'global') and test_case.expected_behavior == 'runs_immediately'`: execute `session.set_hook(hook_name, hook_cmd, **test_case.flag_kwargs)`; `session._show_hook(hook_name)`; when `not (test_case.expected_behavior == 'appends') and not (test_case.expected_behavior == 'global') and test_case.expected_behavior == 'runs_immediately' and hooks`: execute `Hooks.from_stdout(hooks)`; assert `parsed.session_renamed is None or len(parsed.session_renamed) == 0`; when `test_case.flag_kwargs.get('global_')`: execute `server.cmd('set-hook', '-gu', hook_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_set_hook_flag_combinations_append_and_global_be3d8a5031 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_set_hook_flag_combinations[append_to_existing] | Test set_hook with various flag combinations; case inputs `test_case=SetHookFlagTestCase(test_id='append_to_existing', flag_kwargs={'append': True}, expected_behavior='appends', min_version='3.2')`; fixtures `server`; execute `server.new_session(session_name='test_flags')`; `session.unset_hook(hook_name)`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `test_case.expected_behavior == 'appends'`: execute `session.set_hook(f'{hook_name}[0]', "display-message 'initial'")`; `session.set_hook(hook_name, hook_cmd, **test_case.flag_kwargs)`; `parsed.session_renamed.as_list()`; assert `len(hook_values) >= 1`; when `test_case.expected_behavior == 'appends' and test_case.flag_kwargs.get('global_')`: execute `server.cmd('show-hooks', '-g', hook_name)`; `Hooks.from_stdout(result.stdout)`; assert `result.stdout is not None`; when `test_case.expected_behavior == 'appends' and not (test_case.flag_kwargs.get('global_'))`: execute `session._show_hook(hook_name)`; `Hooks.from_stdout(hooks)`; assert `hooks is not None`; when `not (test_case.expected_behavior == 'appends') and test_case.expected_behavior == 'global'`: execute `session.set_hook(f'{hook_name}[0]', hook_cmd, **test_case.flag_kwargs)`; `server.cmd('show-hooks', '-g', f'{hook_name}[0]')`; assert `result.stdout is not None`; `len(result.stdout) > 0`; `'display-message' in result.stdout[0]`; when `not (test_case.expected_behavior == 'appends') and not (test_case.expected_behavior == 'global') and test_case.expected_behavior == 'runs_immediately'`: execute `session.set_hook(hook_name, hook_cmd, **test_case.flag_kwargs)`; `session._show_hook(hook_name)`; when `not (test_case.expected_behavior == 'appends') and not (test_case.expected_behavior == 'global') and test_case.expected_behavior == 'runs_immediately' and hooks`: execute `Hooks.from_stdout(hooks)`; assert `parsed.session_renamed is None or len(parsed.session_renamed) == 0`; when `test_case.flag_kwargs.get('global_')`: execute `server.cmd('set-hook', '-gu', hook_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_set_hook_flag_combinations_append_to_existing_5f0e92ba33 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_set_hook_flag_combinations[global_hook] | Test set_hook with various flag combinations; case inputs `test_case=SetHookFlagTestCase(test_id='global_hook', flag_kwargs={'global_': True}, expected_behavior='global', min_version='3.2')`; fixtures `server`; execute `server.new_session(session_name='test_flags')`; `session.unset_hook(hook_name)`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `test_case.expected_behavior == 'appends'`: execute `session.set_hook(f'{hook_name}[0]', "display-message 'initial'")`; `session.set_hook(hook_name, hook_cmd, **test_case.flag_kwargs)`; `parsed.session_renamed.as_list()`; assert `len(hook_values) >= 1`; when `test_case.expected_behavior == 'appends' and test_case.flag_kwargs.get('global_')`: execute `server.cmd('show-hooks', '-g', hook_name)`; `Hooks.from_stdout(result.stdout)`; assert `result.stdout is not None`; when `test_case.expected_behavior == 'appends' and not (test_case.flag_kwargs.get('global_'))`: execute `session._show_hook(hook_name)`; `Hooks.from_stdout(hooks)`; assert `hooks is not None`; when `not (test_case.expected_behavior == 'appends') and test_case.expected_behavior == 'global'`: execute `session.set_hook(f'{hook_name}[0]', hook_cmd, **test_case.flag_kwargs)`; `server.cmd('show-hooks', '-g', f'{hook_name}[0]')`; assert `result.stdout is not None`; `len(result.stdout) > 0`; `'display-message' in result.stdout[0]`; when `not (test_case.expected_behavior == 'appends') and not (test_case.expected_behavior == 'global') and test_case.expected_behavior == 'runs_immediately'`: execute `session.set_hook(hook_name, hook_cmd, **test_case.flag_kwargs)`; `session._show_hook(hook_name)`; when `not (test_case.expected_behavior == 'appends') and not (test_case.expected_behavior == 'global') and test_case.expected_behavior == 'runs_immediately' and hooks`: execute `Hooks.from_stdout(hooks)`; assert `parsed.session_renamed is None or len(parsed.session_renamed) == 0`; when `test_case.flag_kwargs.get('global_')`: execute `server.cmd('set-hook', '-gu', hook_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_set_hook_flag_combinations_global_hook_8eb90c674e | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_set_hook_flag_combinations[run_immediately] | Test set_hook with various flag combinations; case inputs `test_case=SetHookFlagTestCase(test_id='run_immediately', flag_kwargs={'run': True}, expected_behavior='runs_immediately', min_version='3.2')`; fixtures `server`; execute `server.new_session(session_name='test_flags')`; `session.unset_hook(hook_name)`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `test_case.expected_behavior == 'appends'`: execute `session.set_hook(f'{hook_name}[0]', "display-message 'initial'")`; `session.set_hook(hook_name, hook_cmd, **test_case.flag_kwargs)`; `parsed.session_renamed.as_list()`; assert `len(hook_values) >= 1`; when `test_case.expected_behavior == 'appends' and test_case.flag_kwargs.get('global_')`: execute `server.cmd('show-hooks', '-g', hook_name)`; `Hooks.from_stdout(result.stdout)`; assert `result.stdout is not None`; when `test_case.expected_behavior == 'appends' and not (test_case.flag_kwargs.get('global_'))`: execute `session._show_hook(hook_name)`; `Hooks.from_stdout(hooks)`; assert `hooks is not None`; when `not (test_case.expected_behavior == 'appends') and test_case.expected_behavior == 'global'`: execute `session.set_hook(f'{hook_name}[0]', hook_cmd, **test_case.flag_kwargs)`; `server.cmd('show-hooks', '-g', f'{hook_name}[0]')`; assert `result.stdout is not None`; `len(result.stdout) > 0`; `'display-message' in result.stdout[0]`; when `not (test_case.expected_behavior == 'appends') and not (test_case.expected_behavior == 'global') and test_case.expected_behavior == 'runs_immediately'`: execute `session.set_hook(hook_name, hook_cmd, **test_case.flag_kwargs)`; `session._show_hook(hook_name)`; when `not (test_case.expected_behavior == 'appends') and not (test_case.expected_behavior == 'global') and test_case.expected_behavior == 'runs_immediately' and hooks`: execute `Hooks.from_stdout(hooks)`; assert `parsed.session_renamed is None or len(parsed.session_renamed) == 0`; when `test_case.flag_kwargs.get('global_')`: execute `server.cmd('set-hook', '-gu', hook_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_set_hook_flag_combinations_run_immediately_874cdba8df | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_set_hooks[set_hooks_clear_existing] | Test set_hooks operations; case inputs `test_case=SetHooksTestCase(test_id='set_hooks_clear_existing', hook='session-renamed', setup_hooks={0: "display-message 'old 0'", 1: "display-message 'old 1'"}, operation_args={'values': {0: "display-message 'new 0'"}, 'clear_existing': True}, expected_indices=[0], expected_contains=['new 0'])`; fixtures `server`; execute `server.new_session(session_name='test_set_hooks')`; `session.set_hook(f'{test_case.hook}[{idx}]', val)`; `session.set_hooks(test_case.hook, **test_case.operation_args)`; `session.show_hook(test_case.hook)`; `session.unset_hook(test_case.hook)`; assert `hooks is not None`; `isinstance(hooks, SparseArray)`; `sorted(hooks.keys()) == test_case.expected_indices`; when `test_case.expected_contains`: execute `any((expected_str in v for v in hooks.values()))`; assert `any((expected_str in v for v in hooks.values()))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_set_hooks_set_hooks_clear_existing_5256a7be82 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_set_hooks[set_hooks_with_dict] | Test set_hooks operations; case inputs `test_case=SetHooksTestCase(test_id='set_hooks_with_dict', hook='session-renamed', setup_hooks={}, operation_args={'values': {0: "display-message 'hook 0'", 1: "display-message 'hook 1'", 5: "display-message 'hook 5'"}}, expected_indices=[0, 1, 5], expected_contains=['hook 0', 'hook 1', 'hook 5'])`; fixtures `server`; execute `server.new_session(session_name='test_set_hooks')`; `session.set_hook(f'{test_case.hook}[{idx}]', val)`; `session.set_hooks(test_case.hook, **test_case.operation_args)`; `session.show_hook(test_case.hook)`; `session.unset_hook(test_case.hook)`; assert `hooks is not None`; `isinstance(hooks, SparseArray)`; `sorted(hooks.keys()) == test_case.expected_indices`; when `test_case.expected_contains`: execute `any((expected_str in v for v in hooks.values()))`; assert `any((expected_str in v for v in hooks.values()))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_set_hooks_set_hooks_with_dict_1898803e0c | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_set_hooks[set_hooks_with_list] | Test set_hooks operations; case inputs `test_case=SetHooksTestCase(test_id='set_hooks_with_list', hook='session-renamed', setup_hooks={}, operation_args={'values': ["display-message 'hook 0'", "display-message 'hook 1'", "display-message 'hook 2'"]}, expected_indices=[0, 1, 2], expected_contains=None)`; fixtures `server`; execute `server.new_session(session_name='test_set_hooks')`; `session.set_hook(f'{test_case.hook}[{idx}]', val)`; `session.set_hooks(test_case.hook, **test_case.operation_args)`; `session.show_hook(test_case.hook)`; `session.unset_hook(test_case.hook)`; assert `hooks is not None`; `isinstance(hooks, SparseArray)`; `sorted(hooks.keys()) == test_case.expected_indices`; when `test_case.expected_contains`: execute `any((expected_str in v for v in hooks.values()))`; assert `any((expected_str in v for v in hooks.values()))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_set_hooks_set_hooks_with_list_6f6c753f39 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_set_hooks_method_chaining | Test that set_hooks supports method chaining; fixtures `server`; execute `server.new_session(session_name='test_set_hooks_chain')`; `session.set_hooks('session-renamed', ["display-message 'hook 0'"]).set_hook('session-renamed', "display-message 'hook 1'", append=True).set_hook('session-renamed', "display-message 'hook 2'", append=True)`; `session.set_hooks('session-renamed', ["display-message 'hook 0'"]).set_hook('session-renamed', "display-message 'hook 1'", append=True)`; `session.set_hooks('session-renamed', ["display-message 'hook 0'"])`; `session.show_hook('session-renamed')`; `session.unset_hook('session-renamed')`; assert `result is session`; `hooks is not None`; `isinstance(hooks, SparseArray)`; `sorted(hooks.keys()) == [0, 1, 2]`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_set_hooks_method_chaining_d4a4a6d605 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_set_hooks_with_sparse_array | Test set_hooks with SparseArray input; fixtures `server`; execute `server.new_session(session_name='test_set_hooks_sparse')`; `session.set_hooks('session-renamed', sparse)`; `session.show_hook('session-renamed')`; `session.unset_hook('session-renamed')`; `SparseArray()`; assert `hooks is not None`; `isinstance(hooks, SparseArray)`; `sorted(hooks.keys()) == [0, 10]`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_set_hooks_with_sparse_array_a69f549411 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_show_hook_indexed_lookup[index_five-session-renamed-5-display-message 'test five'] | Test that show_hook with indexed hook name returns the specific value; case inputs `hook_index=5`, `hook_name='session-renamed'`, `hook_value="display-message 'test five'"`, `test_id='index_five'`; fixtures `server`; execute `server.new_session(session_name='test_indexed_lookup')`; `session.set_hook(indexed_hook, hook_value)`; `session.show_hook(indexed_hook)`; `session.show_hook(hook_name)`; `session.unset_hook(hook_name)`; assert `result is not None`; `isinstance(result, str); message=f'Expected str, got {type(result)}'`; `'display-message' in result`; `isinstance(base_result, SparseArray)`; `hook_index in base_result`. | real-tmux integration test | PythonBehaviorParityTest#ok_indexed_lookup_index_five_session_renamed_5_display_message_test_five_6496fdb088 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_show_hook_indexed_lookup[index_zero-session-renamed-0-display-message 'test zero'] | Test that show_hook with indexed hook name returns the specific value; case inputs `hook_index=0`, `hook_name='session-renamed'`, `hook_value="display-message 'test zero'"`, `test_id='index_zero'`; fixtures `server`; execute `server.new_session(session_name='test_indexed_lookup')`; `session.set_hook(indexed_hook, hook_value)`; `session.show_hook(indexed_hook)`; `session.show_hook(hook_name)`; `session.unset_hook(hook_name)`; assert `result is not None`; `isinstance(result, str); message=f'Expected str, got {type(result)}'`; `'display-message' in result`; `isinstance(base_result, SparseArray)`; `hook_index in base_result`. | real-tmux integration test | PythonBehaviorParityTest#ok_indexed_lookup_index_zero_session_renamed_0_display_message_test_zero_953d5e7188 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_show_hook_indexed_lookup[window_hook-window-renamed-0-display-message 'window test'] | Test that show_hook with indexed hook name returns the specific value; case inputs `hook_index=0`, `hook_name='window-renamed'`, `hook_value="display-message 'window test'"`, `test_id='window_hook'`; fixtures `server`; execute `server.new_session(session_name='test_indexed_lookup')`; `session.set_hook(indexed_hook, hook_value)`; `session.show_hook(indexed_hook)`; `session.show_hook(hook_name)`; `session.unset_hook(hook_name)`; assert `result is not None`; `isinstance(result, str); message=f'Expected str, got {type(result)}'`; `'display-message' in result`; `isinstance(base_result, SparseArray)`; `hook_index in base_result`. | real-tmux integration test | PythonBehaviorParityTest#_indexed_lookup_window_hook_window_renamed_0_display_message_window_test_58f9ead4cd | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_show_hook_returns_sparse_array | Test that show_hook returns SparseArray for array hooks; fixtures `server`; execute `server.new_session(session_name='test_show_hook')`; `session.set_hook('session-renamed[5]', "display-message 'fifth'")`; `session.set_hook('session-renamed[0]', "display-message 'zeroth'")`; `session.set_hook('session-renamed[2]', "display-message 'second'")`; `session.show_hook('session-renamed')`; `session.unset_hook('session-renamed')`; assert `hooks is not None`; `isinstance(hooks, SparseArray)`; `sorted(hooks.keys()) == [0, 2, 5]`; `len(value_list) == 3`; `'zeroth' in value_list[0]`; `'second' in value_list[1]`; `'fifth' in value_list[2]`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_show_hook_returns_sparse_array_6e3931a6cb | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_show_hooks_empty_result | Test show_hooks returns empty dict when no hooks are set; fixtures `server`; execute `server.new_session(session_name='test_empty_hooks')`; `session.show_hooks()`; assert `isinstance(hooks, dict)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_show_hooks_empty_result_b6cbde6abd | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_show_hooks_parsing_edge_cases[multiple_different_hooks] | Test show_hooks parses various output formats correctly; case inputs `test_case=ShowHooksParseTestCase(test_id='multiple_different_hooks', setup_commands=[('session-renamed[0]', "display-message 'renamed'"), ('after-split-window[0]', "display-message 'split'")], expected_keys=['session-renamed[0]', 'after-split-window[0]'], check_values=True)`; fixtures `server`; execute `server.new_session(session_name='test_parsing')`; `session.set_hook(hook, value)`; `session.show_hooks()`; `session.unset_hook(base_hook)`; `hook.split('[')`; assert `key in hooks; message=f'Expected key {key} not found in {list(hooks.keys())}'`; when `test_case.check_values`: assert `hooks[key] is not None; message=f'Expected value for {key} to be non-None'`. | real-tmux integration test | PythonBehaviorParityTest#est_hooks_py_test_show_hooks_parsing_edge_cases_multiple_different_hooks_d989b9d220 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_show_hooks_parsing_edge_cases[multiple_indexed_hooks] | Test show_hooks parses various output formats correctly; case inputs `test_case=ShowHooksParseTestCase(test_id='multiple_indexed_hooks', setup_commands=[('session-renamed[0]', "display-message 'first'"), ('session-renamed[1]', "display-message 'second'"), ('session-renamed[5]', "display-message 'fifth'")], expected_keys=['session-renamed[0]', 'session-renamed[1]', 'session-renamed[5]'], check_values=True)`; fixtures `server`; execute `server.new_session(session_name='test_parsing')`; `session.set_hook(hook, value)`; `session.show_hooks()`; `session.unset_hook(base_hook)`; `hook.split('[')`; assert `key in hooks; message=f'Expected key {key} not found in {list(hooks.keys())}'`; when `test_case.check_values`: assert `hooks[key] is not None; message=f'Expected value for {key} to be non-None'`. | real-tmux integration test | PythonBehaviorParityTest#_test_hooks_py_test_show_hooks_parsing_edge_cases_multiple_indexed_hooks_58da3ecfdc | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_show_hooks_parsing_edge_cases[normal_hook_value] | Test show_hooks parses various output formats correctly; case inputs `test_case=ShowHooksParseTestCase(test_id='normal_hook_value', setup_commands=[('session-renamed[0]', "display-message 'test'")], expected_keys=['session-renamed[0]'], check_values=True)`; fixtures `server`; execute `server.new_session(session_name='test_parsing')`; `session.set_hook(hook, value)`; `session.show_hooks()`; `session.unset_hook(base_hook)`; `hook.split('[')`; assert `key in hooks; message=f'Expected key {key} not found in {list(hooks.keys())}'`; when `test_case.check_values`: assert `hooks[key] is not None; message=f'Expected value for {key} to be non-None'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_show_hooks_parsing_edge_cases_normal_hook_value_412b6d8628 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_show_hooks_stores_string_values[multiple_hooks] | Test that show_hooks() correctly stores string hook values; case inputs `test_case=ShowHooksTestCase(test_id='multiple_hooks', hook='session-renamed[1]', value='display-message "another"', expected_value='display-message another', expected_type=<class 'str'>, min_version='3.2')`; fixtures `server`; execute `server.new_session(session_name='test_show_hooks')`; `session.set_hook(test_case.hook, test_case.value)`; `session.show_hooks()`; `session.unset_hook(test_case.hook.split('[')[0])`; `test_case.hook.split('[')`; assert `test_case.hook in hooks`; `isinstance(hooks[test_case.hook], test_case.expected_type)`; `hooks[test_case.hook] == test_case.expected_value`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux >= {test_case.min_version}')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_show_hooks_stores_string_values_multiple_hooks_c283082221 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_show_hooks_stores_string_values[string_hook_value] | Test that show_hooks() correctly stores string hook values; case inputs `test_case=ShowHooksTestCase(test_id='string_hook_value', hook='session-renamed[0]', value='display-message "test"', expected_value='display-message test', expected_type=<class 'str'>, min_version='3.2')`; fixtures `server`; execute `server.new_session(session_name='test_show_hooks')`; `session.set_hook(test_case.hook, test_case.value)`; `session.show_hooks()`; `session.unset_hook(test_case.hook.split('[')[0])`; `test_case.hook.split('[')`; assert `test_case.hook in hooks`; `isinstance(hooks[test_case.hook], test_case.expected_type)`; `hooks[test_case.hook] == test_case.expected_value`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux >= {test_case.min_version}')`. | real-tmux integration test | PythonBehaviorParityTest#sts_test_hooks_py_test_show_hooks_stores_string_values_string_hook_value_dc86738eb8 | 3.2a through 3.7b | planned parity | -| tests/test_hooks.py::test_unset_hook_clears_all_indices | Test that unset_hook without index clears all hook indices; fixtures `server`; execute `server.new_session(session_name='test_unset_hook')`; `session.set_hook('session-renamed[0]', "display-message 'hook 0'")`; `session.set_hook('session-renamed[5]', "display-message 'hook 5'")`; `session.set_hook('session-renamed[10]', "display-message 'hook 10'")`; `session.show_hook('session-renamed')`; `session.unset_hook('session-renamed')`; assert `hooks is not None`; `isinstance(hooks, SparseArray)`; `sorted(hooks.keys()) == [0, 5, 10]`; `hooks_after is None or (isinstance(hooks_after, SparseArray) and len(hooks_after) == 0)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_unset_hook_clears_all_indices_782777b361 | 3.2a through 3.7b | planned parity | -| tests/test_logging.py::test_lifecycle_info_logging_schema | Test that lifecycle operations produce INFO records with str-typed extra; fixtures `session`, `caplog`; execute `session.new_window(window_name='log_test')`; `window.kill()`; `t.cast(t.Any, record)`; assert `len(records) >= 1; message='expected at least one INFO lifecycle record'`; when `val is not None`: assert `isinstance(val, str); message=f'extra key {key!r} should be str, got {type(val).__name__}'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_logging_py_test_lifecycle_info_logging_schema_8796636f6b | 3.2a through 3.7b | planned parity | +| tests/test_hooks.py::test_run_hook_basic | Test run_hook() method exists and can be called without error; fixtures `server`; execute `server.new_session(session_name='test_run_hook')`; `session.set_hook('session-renamed[0]', 'display-message "test"')`; `session.run_hook('session-renamed[0]')`; `session.unset_hook('session-renamed')`; assert `result is session`; when `not has_gte_version('3.2')`: skip via `pytest.skip('Requires tmux >= 3.2')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_run_hook_basic_1e7330eab3 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_set_hook_append_flag | Test set_hook with append=True uses tmux's -a flag; fixtures `server`; execute `server.new_session(session_name='test_append_flag')`; `session.set_hook('session-renamed[0]', "display-message 'initial'")`; `session.set_hook('session-renamed', "display-message 'appended'", append=True)`; `session.show_hook('session-renamed')`; `session.unset_hook('session-renamed')`; assert `hooks is not None`; `isinstance(hooks, SparseArray)`; `len(hooks) == 2`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_set_hook_append_flag_916689e76b | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_set_hook_flag_combinations[append_and_global] | Test set_hook with various flag combinations; case inputs `test_case=SetHookFlagTestCase(test_id='append_and_global', flag_kwargs={'append': True, 'global_': True}, expected_behavior='appends', min_version='3.2')`; fixtures `server`; execute `server.new_session(session_name='test_flags')`; `session.unset_hook(hook_name)`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `test_case.expected_behavior == 'appends'`: execute `session.set_hook(f'{hook_name}[0]', "display-message 'initial'")`; `session.set_hook(hook_name, hook_cmd, **test_case.flag_kwargs)`; `parsed.session_renamed.as_list()`; assert `len(hook_values) >= 1`; when `test_case.expected_behavior == 'appends' and test_case.flag_kwargs.get('global_')`: execute `server.cmd('show-hooks', '-g', hook_name)`; `Hooks.from_stdout(result.stdout)`; assert `result.stdout is not None`; when `test_case.expected_behavior == 'appends' and not (test_case.flag_kwargs.get('global_'))`: execute `session._show_hook(hook_name)`; `Hooks.from_stdout(hooks)`; assert `hooks is not None`; when `not (test_case.expected_behavior == 'appends') and test_case.expected_behavior == 'global'`: execute `session.set_hook(f'{hook_name}[0]', hook_cmd, **test_case.flag_kwargs)`; `server.cmd('show-hooks', '-g', f'{hook_name}[0]')`; assert `result.stdout is not None`; `len(result.stdout) > 0`; `'display-message' in result.stdout[0]`; when `not (test_case.expected_behavior == 'appends') and not (test_case.expected_behavior == 'global') and test_case.expected_behavior == 'runs_immediately'`: execute `session.set_hook(hook_name, hook_cmd, **test_case.flag_kwargs)`; `session._show_hook(hook_name)`; when `not (test_case.expected_behavior == 'appends') and not (test_case.expected_behavior == 'global') and test_case.expected_behavior == 'runs_immediately' and hooks`: execute `Hooks.from_stdout(hooks)`; assert `parsed.session_renamed is None or len(parsed.session_renamed) == 0`; when `test_case.flag_kwargs.get('global_')`: execute `server.cmd('set-hook', '-gu', hook_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_set_hook_flag_combinations_append_and_global_be3d8a5031 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_set_hook_flag_combinations[append_to_existing] | Test set_hook with various flag combinations; case inputs `test_case=SetHookFlagTestCase(test_id='append_to_existing', flag_kwargs={'append': True}, expected_behavior='appends', min_version='3.2')`; fixtures `server`; execute `server.new_session(session_name='test_flags')`; `session.unset_hook(hook_name)`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `test_case.expected_behavior == 'appends'`: execute `session.set_hook(f'{hook_name}[0]', "display-message 'initial'")`; `session.set_hook(hook_name, hook_cmd, **test_case.flag_kwargs)`; `parsed.session_renamed.as_list()`; assert `len(hook_values) >= 1`; when `test_case.expected_behavior == 'appends' and test_case.flag_kwargs.get('global_')`: execute `server.cmd('show-hooks', '-g', hook_name)`; `Hooks.from_stdout(result.stdout)`; assert `result.stdout is not None`; when `test_case.expected_behavior == 'appends' and not (test_case.flag_kwargs.get('global_'))`: execute `session._show_hook(hook_name)`; `Hooks.from_stdout(hooks)`; assert `hooks is not None`; when `not (test_case.expected_behavior == 'appends') and test_case.expected_behavior == 'global'`: execute `session.set_hook(f'{hook_name}[0]', hook_cmd, **test_case.flag_kwargs)`; `server.cmd('show-hooks', '-g', f'{hook_name}[0]')`; assert `result.stdout is not None`; `len(result.stdout) > 0`; `'display-message' in result.stdout[0]`; when `not (test_case.expected_behavior == 'appends') and not (test_case.expected_behavior == 'global') and test_case.expected_behavior == 'runs_immediately'`: execute `session.set_hook(hook_name, hook_cmd, **test_case.flag_kwargs)`; `session._show_hook(hook_name)`; when `not (test_case.expected_behavior == 'appends') and not (test_case.expected_behavior == 'global') and test_case.expected_behavior == 'runs_immediately' and hooks`: execute `Hooks.from_stdout(hooks)`; assert `parsed.session_renamed is None or len(parsed.session_renamed) == 0`; when `test_case.flag_kwargs.get('global_')`: execute `server.cmd('set-hook', '-gu', hook_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_set_hook_flag_combinations_append_to_existing_5f0e92ba33 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_set_hook_flag_combinations[global_hook] | Test set_hook with various flag combinations; case inputs `test_case=SetHookFlagTestCase(test_id='global_hook', flag_kwargs={'global_': True}, expected_behavior='global', min_version='3.2')`; fixtures `server`; execute `server.new_session(session_name='test_flags')`; `session.unset_hook(hook_name)`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `test_case.expected_behavior == 'appends'`: execute `session.set_hook(f'{hook_name}[0]', "display-message 'initial'")`; `session.set_hook(hook_name, hook_cmd, **test_case.flag_kwargs)`; `parsed.session_renamed.as_list()`; assert `len(hook_values) >= 1`; when `test_case.expected_behavior == 'appends' and test_case.flag_kwargs.get('global_')`: execute `server.cmd('show-hooks', '-g', hook_name)`; `Hooks.from_stdout(result.stdout)`; assert `result.stdout is not None`; when `test_case.expected_behavior == 'appends' and not (test_case.flag_kwargs.get('global_'))`: execute `session._show_hook(hook_name)`; `Hooks.from_stdout(hooks)`; assert `hooks is not None`; when `not (test_case.expected_behavior == 'appends') and test_case.expected_behavior == 'global'`: execute `session.set_hook(f'{hook_name}[0]', hook_cmd, **test_case.flag_kwargs)`; `server.cmd('show-hooks', '-g', f'{hook_name}[0]')`; assert `result.stdout is not None`; `len(result.stdout) > 0`; `'display-message' in result.stdout[0]`; when `not (test_case.expected_behavior == 'appends') and not (test_case.expected_behavior == 'global') and test_case.expected_behavior == 'runs_immediately'`: execute `session.set_hook(hook_name, hook_cmd, **test_case.flag_kwargs)`; `session._show_hook(hook_name)`; when `not (test_case.expected_behavior == 'appends') and not (test_case.expected_behavior == 'global') and test_case.expected_behavior == 'runs_immediately' and hooks`: execute `Hooks.from_stdout(hooks)`; assert `parsed.session_renamed is None or len(parsed.session_renamed) == 0`; when `test_case.flag_kwargs.get('global_')`: execute `server.cmd('set-hook', '-gu', hook_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_set_hook_flag_combinations_global_hook_8eb90c674e | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_set_hook_flag_combinations[run_immediately] | Test set_hook with various flag combinations; case inputs `test_case=SetHookFlagTestCase(test_id='run_immediately', flag_kwargs={'run': True}, expected_behavior='runs_immediately', min_version='3.2')`; fixtures `server`; execute `server.new_session(session_name='test_flags')`; `session.unset_hook(hook_name)`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`; when `test_case.expected_behavior == 'appends'`: execute `session.set_hook(f'{hook_name}[0]', "display-message 'initial'")`; `session.set_hook(hook_name, hook_cmd, **test_case.flag_kwargs)`; `parsed.session_renamed.as_list()`; assert `len(hook_values) >= 1`; when `test_case.expected_behavior == 'appends' and test_case.flag_kwargs.get('global_')`: execute `server.cmd('show-hooks', '-g', hook_name)`; `Hooks.from_stdout(result.stdout)`; assert `result.stdout is not None`; when `test_case.expected_behavior == 'appends' and not (test_case.flag_kwargs.get('global_'))`: execute `session._show_hook(hook_name)`; `Hooks.from_stdout(hooks)`; assert `hooks is not None`; when `not (test_case.expected_behavior == 'appends') and test_case.expected_behavior == 'global'`: execute `session.set_hook(f'{hook_name}[0]', hook_cmd, **test_case.flag_kwargs)`; `server.cmd('show-hooks', '-g', f'{hook_name}[0]')`; assert `result.stdout is not None`; `len(result.stdout) > 0`; `'display-message' in result.stdout[0]`; when `not (test_case.expected_behavior == 'appends') and not (test_case.expected_behavior == 'global') and test_case.expected_behavior == 'runs_immediately'`: execute `session.set_hook(hook_name, hook_cmd, **test_case.flag_kwargs)`; `session._show_hook(hook_name)`; when `not (test_case.expected_behavior == 'appends') and not (test_case.expected_behavior == 'global') and test_case.expected_behavior == 'runs_immediately' and hooks`: execute `Hooks.from_stdout(hooks)`; assert `parsed.session_renamed is None or len(parsed.session_renamed) == 0`; when `test_case.flag_kwargs.get('global_')`: execute `server.cmd('set-hook', '-gu', hook_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_set_hook_flag_combinations_run_immediately_874cdba8df | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_set_hooks[set_hooks_clear_existing] | Test set_hooks operations; case inputs `test_case=SetHooksTestCase(test_id='set_hooks_clear_existing', hook='session-renamed', setup_hooks={0: "display-message 'old 0'", 1: "display-message 'old 1'"}, operation_args={'values': {0: "display-message 'new 0'"}, 'clear_existing': True}, expected_indices=[0], expected_contains=['new 0'])`; fixtures `server`; execute `server.new_session(session_name='test_set_hooks')`; `session.set_hook(f'{test_case.hook}[{idx}]', val)`; `session.set_hooks(test_case.hook, **test_case.operation_args)`; `session.show_hook(test_case.hook)`; `session.unset_hook(test_case.hook)`; assert `hooks is not None`; `isinstance(hooks, SparseArray)`; `sorted(hooks.keys()) == test_case.expected_indices`; when `test_case.expected_contains`: execute `any((expected_str in v for v in hooks.values()))`; assert `any((expected_str in v for v in hooks.values()))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_set_hooks_set_hooks_clear_existing_5256a7be82 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_set_hooks[set_hooks_with_dict] | Test set_hooks operations; case inputs `test_case=SetHooksTestCase(test_id='set_hooks_with_dict', hook='session-renamed', setup_hooks={}, operation_args={'values': {0: "display-message 'hook 0'", 1: "display-message 'hook 1'", 5: "display-message 'hook 5'"}}, expected_indices=[0, 1, 5], expected_contains=['hook 0', 'hook 1', 'hook 5'])`; fixtures `server`; execute `server.new_session(session_name='test_set_hooks')`; `session.set_hook(f'{test_case.hook}[{idx}]', val)`; `session.set_hooks(test_case.hook, **test_case.operation_args)`; `session.show_hook(test_case.hook)`; `session.unset_hook(test_case.hook)`; assert `hooks is not None`; `isinstance(hooks, SparseArray)`; `sorted(hooks.keys()) == test_case.expected_indices`; when `test_case.expected_contains`: execute `any((expected_str in v for v in hooks.values()))`; assert `any((expected_str in v for v in hooks.values()))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_set_hooks_set_hooks_with_dict_1898803e0c | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_set_hooks[set_hooks_with_list] | Test set_hooks operations; case inputs `test_case=SetHooksTestCase(test_id='set_hooks_with_list', hook='session-renamed', setup_hooks={}, operation_args={'values': ["display-message 'hook 0'", "display-message 'hook 1'", "display-message 'hook 2'"]}, expected_indices=[0, 1, 2], expected_contains=None)`; fixtures `server`; execute `server.new_session(session_name='test_set_hooks')`; `session.set_hook(f'{test_case.hook}[{idx}]', val)`; `session.set_hooks(test_case.hook, **test_case.operation_args)`; `session.show_hook(test_case.hook)`; `session.unset_hook(test_case.hook)`; assert `hooks is not None`; `isinstance(hooks, SparseArray)`; `sorted(hooks.keys()) == test_case.expected_indices`; when `test_case.expected_contains`: execute `any((expected_str in v for v in hooks.values()))`; assert `any((expected_str in v for v in hooks.values()))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_set_hooks_set_hooks_with_list_6f6c753f39 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_set_hooks_method_chaining | Test that set_hooks supports method chaining; fixtures `server`; execute `server.new_session(session_name='test_set_hooks_chain')`; `session.set_hooks('session-renamed', ["display-message 'hook 0'"]).set_hook('session-renamed', "display-message 'hook 1'", append=True).set_hook('session-renamed', "display-message 'hook 2'", append=True)`; `session.set_hooks('session-renamed', ["display-message 'hook 0'"]).set_hook('session-renamed', "display-message 'hook 1'", append=True)`; `session.set_hooks('session-renamed', ["display-message 'hook 0'"])`; `session.show_hook('session-renamed')`; `session.unset_hook('session-renamed')`; assert `result is session`; `hooks is not None`; `isinstance(hooks, SparseArray)`; `sorted(hooks.keys()) == [0, 1, 2]`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_set_hooks_method_chaining_d4a4a6d605 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_set_hooks_with_sparse_array | Test set_hooks with SparseArray input; fixtures `server`; execute `server.new_session(session_name='test_set_hooks_sparse')`; `session.set_hooks('session-renamed', sparse)`; `session.show_hook('session-renamed')`; `session.unset_hook('session-renamed')`; `SparseArray()`; assert `hooks is not None`; `isinstance(hooks, SparseArray)`; `sorted(hooks.keys()) == [0, 10]`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_set_hooks_with_sparse_array_a69f549411 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_show_hook_indexed_lookup[index_five-session-renamed-5-display-message 'test five'] | Test that show_hook with indexed hook name returns the specific value; case inputs `hook_index=5`, `hook_name='session-renamed'`, `hook_value="display-message 'test five'"`, `test_id='index_five'`; fixtures `server`; execute `server.new_session(session_name='test_indexed_lookup')`; `session.set_hook(indexed_hook, hook_value)`; `session.show_hook(indexed_hook)`; `session.show_hook(hook_name)`; `session.unset_hook(hook_name)`; assert `result is not None`; `isinstance(result, str); message=f'Expected str, got {type(result)}'`; `'display-message' in result`; `isinstance(base_result, SparseArray)`; `hook_index in base_result`. | real-tmux integration test | PythonBehaviorParityTest#ok_indexed_lookup_index_five_session_renamed_5_display_message_test_five_6496fdb088 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_show_hook_indexed_lookup[index_zero-session-renamed-0-display-message 'test zero'] | Test that show_hook with indexed hook name returns the specific value; case inputs `hook_index=0`, `hook_name='session-renamed'`, `hook_value="display-message 'test zero'"`, `test_id='index_zero'`; fixtures `server`; execute `server.new_session(session_name='test_indexed_lookup')`; `session.set_hook(indexed_hook, hook_value)`; `session.show_hook(indexed_hook)`; `session.show_hook(hook_name)`; `session.unset_hook(hook_name)`; assert `result is not None`; `isinstance(result, str); message=f'Expected str, got {type(result)}'`; `'display-message' in result`; `isinstance(base_result, SparseArray)`; `hook_index in base_result`. | real-tmux integration test | PythonBehaviorParityTest#ok_indexed_lookup_index_zero_session_renamed_0_display_message_test_zero_953d5e7188 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_show_hook_indexed_lookup[window_hook-window-renamed-0-display-message 'window test'] | Test that show_hook with indexed hook name returns the specific value; case inputs `hook_index=0`, `hook_name='window-renamed'`, `hook_value="display-message 'window test'"`, `test_id='window_hook'`; fixtures `server`; execute `server.new_session(session_name='test_indexed_lookup')`; `session.set_hook(indexed_hook, hook_value)`; `session.show_hook(indexed_hook)`; `session.show_hook(hook_name)`; `session.unset_hook(hook_name)`; assert `result is not None`; `isinstance(result, str); message=f'Expected str, got {type(result)}'`; `'display-message' in result`; `isinstance(base_result, SparseArray)`; `hook_index in base_result`. | real-tmux integration test | PythonBehaviorParityTest#_indexed_lookup_window_hook_window_renamed_0_display_message_window_test_58f9ead4cd | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_show_hook_returns_sparse_array | Test that show_hook returns SparseArray for array hooks; fixtures `server`; execute `server.new_session(session_name='test_show_hook')`; `session.set_hook('session-renamed[5]', "display-message 'fifth'")`; `session.set_hook('session-renamed[0]', "display-message 'zeroth'")`; `session.set_hook('session-renamed[2]', "display-message 'second'")`; `session.show_hook('session-renamed')`; `session.unset_hook('session-renamed')`; assert `hooks is not None`; `isinstance(hooks, SparseArray)`; `sorted(hooks.keys()) == [0, 2, 5]`; `len(value_list) == 3`; `'zeroth' in value_list[0]`; `'second' in value_list[1]`; `'fifth' in value_list[2]`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_show_hook_returns_sparse_array_6e3931a6cb | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_show_hooks_empty_result | Test show_hooks returns empty dict when no hooks are set; fixtures `server`; execute `server.new_session(session_name='test_empty_hooks')`; `session.show_hooks()`; assert `isinstance(hooks, dict)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_show_hooks_empty_result_b6cbde6abd | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_show_hooks_parsing_edge_cases[multiple_different_hooks] | Test show_hooks parses various output formats correctly; case inputs `test_case=ShowHooksParseTestCase(test_id='multiple_different_hooks', setup_commands=[('session-renamed[0]', "display-message 'renamed'"), ('after-split-window[0]', "display-message 'split'")], expected_keys=['session-renamed[0]', 'after-split-window[0]'], check_values=True)`; fixtures `server`; execute `server.new_session(session_name='test_parsing')`; `session.set_hook(hook, value)`; `session.show_hooks()`; `session.unset_hook(base_hook)`; `hook.split('[')`; assert `key in hooks; message=f'Expected key {key} not found in {list(hooks.keys())}'`; when `test_case.check_values`: assert `hooks[key] is not None; message=f'Expected value for {key} to be non-None'`. | real-tmux integration test | PythonBehaviorParityTest#est_hooks_py_test_show_hooks_parsing_edge_cases_multiple_different_hooks_d989b9d220 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_show_hooks_parsing_edge_cases[multiple_indexed_hooks] | Test show_hooks parses various output formats correctly; case inputs `test_case=ShowHooksParseTestCase(test_id='multiple_indexed_hooks', setup_commands=[('session-renamed[0]', "display-message 'first'"), ('session-renamed[1]', "display-message 'second'"), ('session-renamed[5]', "display-message 'fifth'")], expected_keys=['session-renamed[0]', 'session-renamed[1]', 'session-renamed[5]'], check_values=True)`; fixtures `server`; execute `server.new_session(session_name='test_parsing')`; `session.set_hook(hook, value)`; `session.show_hooks()`; `session.unset_hook(base_hook)`; `hook.split('[')`; assert `key in hooks; message=f'Expected key {key} not found in {list(hooks.keys())}'`; when `test_case.check_values`: assert `hooks[key] is not None; message=f'Expected value for {key} to be non-None'`. | real-tmux integration test | PythonBehaviorParityTest#_test_hooks_py_test_show_hooks_parsing_edge_cases_multiple_indexed_hooks_58da3ecfdc | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_show_hooks_parsing_edge_cases[normal_hook_value] | Test show_hooks parses various output formats correctly; case inputs `test_case=ShowHooksParseTestCase(test_id='normal_hook_value', setup_commands=[('session-renamed[0]', "display-message 'test'")], expected_keys=['session-renamed[0]'], check_values=True)`; fixtures `server`; execute `server.new_session(session_name='test_parsing')`; `session.set_hook(hook, value)`; `session.show_hooks()`; `session.unset_hook(base_hook)`; `hook.split('[')`; assert `key in hooks; message=f'Expected key {key} not found in {list(hooks.keys())}'`; when `test_case.check_values`: assert `hooks[key] is not None; message=f'Expected value for {key} to be non-None'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_show_hooks_parsing_edge_cases_normal_hook_value_412b6d8628 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_show_hooks_stores_string_values[multiple_hooks] | Test that show_hooks() correctly stores string hook values; case inputs `test_case=ShowHooksTestCase(test_id='multiple_hooks', hook='session-renamed[1]', value='display-message "another"', expected_value='display-message another', expected_type=<class 'str'>, min_version='3.2')`; fixtures `server`; execute `server.new_session(session_name='test_show_hooks')`; `session.set_hook(test_case.hook, test_case.value)`; `session.show_hooks()`; `session.unset_hook(test_case.hook.split('[')[0])`; `test_case.hook.split('[')`; assert `test_case.hook in hooks`; `isinstance(hooks[test_case.hook], test_case.expected_type)`; `hooks[test_case.hook] == test_case.expected_value`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux >= {test_case.min_version}')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_show_hooks_stores_string_values_multiple_hooks_c283082221 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_show_hooks_stores_string_values[string_hook_value] | Test that show_hooks() correctly stores string hook values; case inputs `test_case=ShowHooksTestCase(test_id='string_hook_value', hook='session-renamed[0]', value='display-message "test"', expected_value='display-message test', expected_type=<class 'str'>, min_version='3.2')`; fixtures `server`; execute `server.new_session(session_name='test_show_hooks')`; `session.set_hook(test_case.hook, test_case.value)`; `session.show_hooks()`; `session.unset_hook(test_case.hook.split('[')[0])`; `test_case.hook.split('[')`; assert `test_case.hook in hooks`; `isinstance(hooks[test_case.hook], test_case.expected_type)`; `hooks[test_case.hook] == test_case.expected_value`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux >= {test_case.min_version}')`. | real-tmux integration test | PythonBehaviorParityTest#sts_test_hooks_py_test_show_hooks_stores_string_values_string_hook_value_dc86738eb8 | 3.3a through 3.7c | planned parity | +| tests/test_hooks.py::test_unset_hook_clears_all_indices | Test that unset_hook without index clears all hook indices; fixtures `server`; execute `server.new_session(session_name='test_unset_hook')`; `session.set_hook('session-renamed[0]', "display-message 'hook 0'")`; `session.set_hook('session-renamed[5]', "display-message 'hook 5'")`; `session.set_hook('session-renamed[10]', "display-message 'hook 10'")`; `session.show_hook('session-renamed')`; `session.unset_hook('session-renamed')`; assert `hooks is not None`; `isinstance(hooks, SparseArray)`; `sorted(hooks.keys()) == [0, 5, 10]`; `hooks_after is None or (isinstance(hooks_after, SparseArray) and len(hooks_after) == 0)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_hooks_py_test_unset_hook_clears_all_indices_782777b361 | 3.3a through 3.7c | planned parity | +| tests/test_logging.py::test_lifecycle_info_logging_schema | Test that lifecycle operations produce INFO records with str-typed extra; fixtures `session`, `caplog`; execute `session.new_window(window_name='log_test')`; `window.kill()`; `t.cast(t.Any, record)`; assert `len(records) >= 1; message='expected at least one INFO lifecycle record'`; when `val is not None`: assert `isinstance(val, str); message=f'extra key {key!r} should be str, got {type(val).__name__}'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_logging_py_test_lifecycle_info_logging_schema_8796636f6b | 3.3a through 3.7c | planned parity | | tests/test_logging.py::test_options_warning_logging_schema | Test that options parse warnings produce records with tmux_option_key; fixtures `caplog`; execute `SparseArray()`; `explode_complex({'terminal-features': bad_features})`; `t.cast(t.Any, records[0])`; assert `len(records) >= 1; message='expected WARNING record for option parse failure'`; `isinstance(rec.tmux_option_key, str)`; `rec.exc_info is None`. | unit test | PythonBehaviorParityTest#tests_test_logging_py_test_options_warning_logging_schema_358e8402b3 | not tmux-bound | planned parity | -| tests/test_logging.py::test_pane_kill_all_except_logging | Test that pane.kill(all_except=True) identifies the surviving pane; fixtures `session`, `caplog`; execute `window.resize(height=100, width=100)`; `window.split()`; `survivor.kill(all_except=True)`; `t.cast(t.Any, records[0])`; `rec.getMessage()`; `all((p.pane_id not in remaining_pane_ids for p in other_panes))`; assert `window is not None`; `len(records) >= 1; message='expected INFO record for all-except pane kill'`; `rec.getMessage() == 'other panes killed'`; `rec.tmux_pane == survivor.pane_id`; `rec.tmux_target == survivor.pane_id`; `survivor.pane_id in remaining_pane_ids`; `all((p.pane_id not in remaining_pane_ids for p in other_panes))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_logging_py_test_pane_kill_all_except_logging_63ecf7040d | 3.2a through 3.7b | planned parity | -| tests/test_logging.py::test_pane_split_info_logging | Test that pane.split() produces INFO record with str-typed extra; fixtures `session`, `caplog`; execute `pane.split()`; `t.cast(t.Any, records[0])`; `new_pane.kill()`; assert `window is not None`; `pane is not None`; `len(records) >= 1; message='expected INFO record for pane split'`; `isinstance(rec.tmux_subcommand, str)`; `isinstance(rec.tmux_pane, str)`; when `val is not None`: assert `isinstance(val, str); message=f'extra key {key!r} should be str, got {type(val).__name__}'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_logging_py_test_pane_split_info_logging_e11a0d5da2 | 3.2a through 3.7b | planned parity | -| tests/test_logging.py::test_server_kill_info_logging | Test that server.kill() emits a lifecycle INFO record; fixtures `caplog`; execute `Server(socket_name=f'libtmux_log_{next(namer)}')`; `temp_server.new_session(session_name=f'log_session_{next(namer)}')`; `temp_server.kill()`; `t.cast(t.Any, records[0])`; `rec.getMessage()`; assert `len(records) >= 1; message='expected INFO record for server kill'`; `rec.getMessage() == 'server killed'`; `isinstance(rec.tmux_subcommand, str)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_logging_py_test_server_kill_info_logging_638284f100 | 3.2a through 3.7b | planned parity | -| tests/test_logging.py::test_server_new_session_info_logging | Test that server.new_session() produces INFO record with str-typed extra; fixtures `server`, `caplog`; execute `server.new_session(session_name='log_test_session')`; `t.cast(t.Any, records[0])`; `new_session.kill()`; assert `len(records) >= 1; message='expected INFO record for session creation'`; `isinstance(rec.tmux_subcommand, str)`; `isinstance(rec.tmux_session, str)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_logging_py_test_server_new_session_info_logging_c2eb50abfa | 3.2a through 3.7b | planned parity | +| tests/test_logging.py::test_pane_kill_all_except_logging | Test that pane.kill(all_except=True) identifies the surviving pane; fixtures `session`, `caplog`; execute `window.resize(height=100, width=100)`; `window.split()`; `survivor.kill(all_except=True)`; `t.cast(t.Any, records[0])`; `rec.getMessage()`; `all((p.pane_id not in remaining_pane_ids for p in other_panes))`; assert `window is not None`; `len(records) >= 1; message='expected INFO record for all-except pane kill'`; `rec.getMessage() == 'other panes killed'`; `rec.tmux_pane == survivor.pane_id`; `rec.tmux_target == survivor.pane_id`; `survivor.pane_id in remaining_pane_ids`; `all((p.pane_id not in remaining_pane_ids for p in other_panes))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_logging_py_test_pane_kill_all_except_logging_63ecf7040d | 3.3a through 3.7c | planned parity | +| tests/test_logging.py::test_pane_split_info_logging | Test that pane.split() produces INFO record with str-typed extra; fixtures `session`, `caplog`; execute `pane.split()`; `t.cast(t.Any, records[0])`; `new_pane.kill()`; assert `window is not None`; `pane is not None`; `len(records) >= 1; message='expected INFO record for pane split'`; `isinstance(rec.tmux_subcommand, str)`; `isinstance(rec.tmux_pane, str)`; when `val is not None`: assert `isinstance(val, str); message=f'extra key {key!r} should be str, got {type(val).__name__}'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_logging_py_test_pane_split_info_logging_e11a0d5da2 | 3.3a through 3.7c | planned parity | +| tests/test_logging.py::test_server_kill_info_logging | Test that server.kill() emits a lifecycle INFO record; fixtures `caplog`; execute `Server(socket_name=f'libtmux_log_{next(namer)}')`; `temp_server.new_session(session_name=f'log_session_{next(namer)}')`; `temp_server.kill()`; `t.cast(t.Any, records[0])`; `rec.getMessage()`; assert `len(records) >= 1; message='expected INFO record for server kill'`; `rec.getMessage() == 'server killed'`; `isinstance(rec.tmux_subcommand, str)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_logging_py_test_server_kill_info_logging_638284f100 | 3.3a through 3.7c | planned parity | +| tests/test_logging.py::test_server_new_session_info_logging | Test that server.new_session() produces INFO record with str-typed extra; fixtures `server`, `caplog`; execute `server.new_session(session_name='log_test_session')`; `t.cast(t.Any, records[0])`; `new_session.kill()`; assert `len(records) >= 1; message='expected INFO record for session creation'`; `isinstance(rec.tmux_subcommand, str)`; `isinstance(rec.tmux_session, str)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_logging_py_test_server_new_session_info_logging_c2eb50abfa | 3.3a through 3.7c | planned parity | | tests/test_logging.py::test_server_new_session_surfaces_kill_session_stderr | Test kill-session stderr propagation using monkeypatch for the failure path; fixtures `monkeypatch`; execute `server.new_session(session_name='existing_session', kill_session=True)`; `Server(socket_name=f'libtmux_log_{next(namer)}')`; `types.SimpleNamespace(stderr=['kill failed'])`; expect `pytest.raises(exc.LibTmuxException, match='kill failed')`. | unit test | PythonBehaviorParityTest#sts_test_logging_py_test_server_new_session_surfaces_kill_session_stderr_fce6fd3b89 | not tmux-bound | planned parity | -| tests/test_logging.py::test_session_kill_all_except_logging | Test that session.kill(all_except=True) identifies the surviving session; fixtures `server`, `caplog`; execute `server.new_session(session_name=f'log_survivor_{next(namer)}')`; `server.new_session(session_name=f'log_other_{next(namer)}')`; `survivor.kill(all_except=True)`; `t.cast(t.Any, records[0])`; `rec.getMessage()`; `all((session.session_id not in remaining_session_ids for session in other_sessions))`; assert `len(records) >= 1; message='expected INFO record for all-except session kill'`; `rec.getMessage() == 'other sessions killed'`; `rec.tmux_session == survivor.session_name`; `rec.tmux_target == survivor.session_id`; `survivor.session_id in remaining_session_ids`; `all((session.session_id not in remaining_session_ids for session in other_sessions))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_logging_py_test_session_kill_all_except_logging_2a4929ce65 | 3.2a through 3.7b | planned parity | -| tests/test_logging.py::test_tmux_cmd_debug_logging_schema | Test that tmux_cmd produces structured log records per AGENTS.md; fixtures `server`, `caplog`; execute `server.cmd('list-sessions')`; `t.cast(t.Any, records[0])`; assert `len(records) >= 1`; `isinstance(record.tmux_cmd, str)`; `isinstance(record.tmux_exit_code, int)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_logging_py_test_tmux_cmd_debug_logging_schema_ee20f7841a | 3.2a through 3.7b | planned parity | -| tests/test_logging.py::test_window_kill_all_except_logging | Test that window.kill(all_except=True) identifies the surviving window; fixtures `session`, `caplog`; execute `session.new_window(window_name=f'log_survivor_{next(namer)}')`; `session.new_window(window_name=f'log_other_{next(namer)}')`; `survivor.kill(all_except=True)`; `t.cast(t.Any, records[0])`; `rec.getMessage()`; `all((window.window_id not in remaining_window_ids for window in other_windows))`; assert `len(records) >= 1; message='expected INFO record for all-except window kill'`; `rec.getMessage() == 'other windows killed'`; `rec.tmux_window == survivor.window_name`; `rec.tmux_target == survivor.window_id`; `survivor.window_id in remaining_window_ids`; `all((window.window_id not in remaining_window_ids for window in other_windows))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_logging_py_test_window_kill_all_except_logging_2b0be2bf8e | 3.2a through 3.7b | planned parity | -| tests/test_logging.py::test_window_rename_info_logging | Test that window.rename_window() produces INFO record with str-typed extra; fixtures `session`, `caplog`; execute `window.rename_window('log_renamed')`; `t.cast(t.Any, records[0])`; assert `window is not None`; `len(records) >= 1; message='expected INFO record for window rename'`; `isinstance(rec.tmux_subcommand, str)`; when `val is not None`: assert `isinstance(val, str); message=f'extra key {key!r} should be str, got {type(val).__name__}'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_logging_py_test_window_rename_info_logging_3dd94c3763 | 3.2a through 3.7b | planned parity | -| tests/test_neo.py::test_context_only_token_scope[command_list_alias] | Tokens registered outside ``format.c`` route to the ``context`` scope; case inputs `token='command_list_alias'`; execute `_token_scope(token)`; assert `_token_scope(token) == 'context'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_neo_py_test_context_only_token_scope_command_list_alias_977650294c | 3.2a through 3.7b | planned parity | -| tests/test_neo.py::test_context_only_token_scope[command_list_name] | Tokens registered outside ``format.c`` route to the ``context`` scope; case inputs `token='command_list_name'`; execute `_token_scope(token)`; assert `_token_scope(token) == 'context'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_neo_py_test_context_only_token_scope_command_list_name_5a0bc1f3f5 | 3.2a through 3.7b | planned parity | -| tests/test_neo.py::test_context_only_token_scope[command_list_usage] | Tokens registered outside ``format.c`` route to the ``context`` scope; case inputs `token='command_list_usage'`; execute `_token_scope(token)`; assert `_token_scope(token) == 'context'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_neo_py_test_context_only_token_scope_command_list_usage_f1efc2b591 | 3.2a through 3.7b | planned parity | -| tests/test_neo.py::test_context_only_token_scope[current_file] | Tokens registered outside ``format.c`` route to the ``context`` scope; case inputs `token='current_file'`; execute `_token_scope(token)`; assert `_token_scope(token) == 'context'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_neo_py_test_context_only_token_scope_current_file_665eda3081 | 3.2a through 3.7b | planned parity | -| tests/test_neo.py::test_context_only_token_scope[search_match] | Tokens registered outside ``format.c`` route to the ``context`` scope; case inputs `token='search_match'`; execute `_token_scope(token)`; assert `_token_scope(token) == 'context'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_neo_py_test_context_only_token_scope_search_match_6f9972a6e4 | 3.2a through 3.7b | planned parity | +| tests/test_logging.py::test_session_kill_all_except_logging | Test that session.kill(all_except=True) identifies the surviving session; fixtures `server`, `caplog`; execute `server.new_session(session_name=f'log_survivor_{next(namer)}')`; `server.new_session(session_name=f'log_other_{next(namer)}')`; `survivor.kill(all_except=True)`; `t.cast(t.Any, records[0])`; `rec.getMessage()`; `all((session.session_id not in remaining_session_ids for session in other_sessions))`; assert `len(records) >= 1; message='expected INFO record for all-except session kill'`; `rec.getMessage() == 'other sessions killed'`; `rec.tmux_session == survivor.session_name`; `rec.tmux_target == survivor.session_id`; `survivor.session_id in remaining_session_ids`; `all((session.session_id not in remaining_session_ids for session in other_sessions))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_logging_py_test_session_kill_all_except_logging_2a4929ce65 | 3.3a through 3.7c | planned parity | +| tests/test_logging.py::test_tmux_cmd_debug_logging_schema | Test that tmux_cmd produces structured log records per AGENTS.md; fixtures `server`, `caplog`; execute `server.cmd('list-sessions')`; `t.cast(t.Any, records[0])`; assert `len(records) >= 1`; `isinstance(record.tmux_cmd, str)`; `isinstance(record.tmux_exit_code, int)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_logging_py_test_tmux_cmd_debug_logging_schema_ee20f7841a | 3.3a through 3.7c | planned parity | +| tests/test_logging.py::test_window_kill_all_except_logging | Test that window.kill(all_except=True) identifies the surviving window; fixtures `session`, `caplog`; execute `session.new_window(window_name=f'log_survivor_{next(namer)}')`; `session.new_window(window_name=f'log_other_{next(namer)}')`; `survivor.kill(all_except=True)`; `t.cast(t.Any, records[0])`; `rec.getMessage()`; `all((window.window_id not in remaining_window_ids for window in other_windows))`; assert `len(records) >= 1; message='expected INFO record for all-except window kill'`; `rec.getMessage() == 'other windows killed'`; `rec.tmux_window == survivor.window_name`; `rec.tmux_target == survivor.window_id`; `survivor.window_id in remaining_window_ids`; `all((window.window_id not in remaining_window_ids for window in other_windows))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_logging_py_test_window_kill_all_except_logging_2b0be2bf8e | 3.3a through 3.7c | planned parity | +| tests/test_logging.py::test_window_rename_info_logging | Test that window.rename_window() produces INFO record with str-typed extra; fixtures `session`, `caplog`; execute `window.rename_window('log_renamed')`; `t.cast(t.Any, records[0])`; assert `window is not None`; `len(records) >= 1; message='expected INFO record for window rename'`; `isinstance(rec.tmux_subcommand, str)`; when `val is not None`: assert `isinstance(val, str); message=f'extra key {key!r} should be str, got {type(val).__name__}'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_logging_py_test_window_rename_info_logging_3dd94c3763 | 3.3a through 3.7c | planned parity | +| tests/test_neo.py::test_context_only_token_scope[command_list_alias] | Tokens registered outside ``format.c`` route to the ``context`` scope; case inputs `token='command_list_alias'`; execute `_token_scope(token)`; assert `_token_scope(token) == 'context'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_neo_py_test_context_only_token_scope_command_list_alias_977650294c | 3.3a through 3.7c | planned parity | +| tests/test_neo.py::test_context_only_token_scope[command_list_name] | Tokens registered outside ``format.c`` route to the ``context`` scope; case inputs `token='command_list_name'`; execute `_token_scope(token)`; assert `_token_scope(token) == 'context'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_neo_py_test_context_only_token_scope_command_list_name_5a0bc1f3f5 | 3.3a through 3.7c | planned parity | +| tests/test_neo.py::test_context_only_token_scope[command_list_usage] | Tokens registered outside ``format.c`` route to the ``context`` scope; case inputs `token='command_list_usage'`; execute `_token_scope(token)`; assert `_token_scope(token) == 'context'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_neo_py_test_context_only_token_scope_command_list_usage_f1efc2b591 | 3.3a through 3.7c | planned parity | +| tests/test_neo.py::test_context_only_token_scope[current_file] | Tokens registered outside ``format.c`` route to the ``context`` scope; case inputs `token='current_file'`; execute `_token_scope(token)`; assert `_token_scope(token) == 'context'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_neo_py_test_context_only_token_scope_current_file_665eda3081 | 3.3a through 3.7c | planned parity | +| tests/test_neo.py::test_context_only_token_scope[search_match] | Tokens registered outside ``format.c`` route to the ``context`` scope; case inputs `token='search_match'`; execute `_token_scope(token)`; assert `_token_scope(token) == 'context'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_neo_py_test_context_only_token_scope_search_match_6f9972a6e4 | 3.3a through 3.7c | planned parity | | tests/test_neo.py::test_context_scope_excluded_from_every_list_cmd[list-clients] | ``"context"`` is excluded from every ``SCOPES_BY_LIST_CMD`` entry; case inputs `list_cmd='list-clients'`; assert `'context' not in SCOPES_BY_LIST_CMD[list_cmd]`. | unit test | PythonBehaviorParityTest#test_neo_py_test_context_scope_excluded_from_every_list_cmd_list_clients_6b8176815c | not tmux-bound | planned parity | | tests/test_neo.py::test_context_scope_excluded_from_every_list_cmd[list-panes] | ``"context"`` is excluded from every ``SCOPES_BY_LIST_CMD`` entry; case inputs `list_cmd='list-panes'`; assert `'context' not in SCOPES_BY_LIST_CMD[list_cmd]`. | unit test | PythonBehaviorParityTest#s_test_neo_py_test_context_scope_excluded_from_every_list_cmd_list_panes_9c7b349b83 | not tmux-bound | planned parity | | tests/test_neo.py::test_context_scope_excluded_from_every_list_cmd[list-sessions] | ``"context"`` is excluded from every ``SCOPES_BY_LIST_CMD`` entry; case inputs `list_cmd='list-sessions'`; assert `'context' not in SCOPES_BY_LIST_CMD[list_cmd]`. | unit test | PythonBehaviorParityTest#est_neo_py_test_context_scope_excluded_from_every_list_cmd_list_sessions_fdad81f033 | not tmux-bound | planned parity | @@ -871,19 +871,19 @@ under [`tests/`](https://github.com/tmux-python/libtmux/blob/c4a980b/tests) and | tests/test_neo.py::test_context_tokens_absent_from_every_list_cmd_template[command_list_usage] | Context-only tokens never appear in any ``list-*`` ``-F`` template; case inputs `token='command_list_usage'`; execute `get_output_format(list_cmd, '3.6a')`; assert `token not in fields; message=f'{token!r} (context-only) leaked into {list_cmd} template'`. | unit test | PythonBehaviorParityTest#st_context_tokens_absent_from_every_list_cmd_template_command_list_usage_103322c2d8 | not tmux-bound | planned parity | | tests/test_neo.py::test_context_tokens_absent_from_every_list_cmd_template[current_file] | Context-only tokens never appear in any ``list-*`` ``-F`` template; case inputs `token='current_file'`; execute `get_output_format(list_cmd, '3.6a')`; assert `token not in fields; message=f'{token!r} (context-only) leaked into {list_cmd} template'`. | unit test | PythonBehaviorParityTest#_py_test_context_tokens_absent_from_every_list_cmd_template_current_file_c13dc9f27f | not tmux-bound | planned parity | | tests/test_neo.py::test_context_tokens_absent_from_every_list_cmd_template[search_match] | Context-only tokens never appear in any ``list-*`` ``-F`` template; case inputs `token='search_match'`; execute `get_output_format(list_cmd, '3.6a')`; assert `token not in fields; message=f'{token!r} (context-only) leaked into {list_cmd} template'`. | unit test | PythonBehaviorParityTest#_py_test_context_tokens_absent_from_every_list_cmd_template_search_match_0c631dffc4 | not tmux-bound | planned parity | -| tests/test_neo.py::test_every_obj_field_classifies_to_known_scope | Every declared ``Obj`` field must classify to a known scope; assert `not unclassified; message=f'Obj fields with no scope classification (add them to _SCOPE_OVERRIDES, _SCOPE_PREFIXES, _UNIVERSAL_TOKENS, or _CONTEXT_ONLY_TOKENS): {unclassified}'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_neo_py_test_every_obj_field_classifies_to_known_scope_c39ac6288c | 3.2a through 3.7b | planned parity | +| tests/test_neo.py::test_every_obj_field_classifies_to_known_scope | Every declared ``Obj`` field must classify to a known scope; assert `not unclassified; message=f'Obj fields with no scope classification (add them to _SCOPE_OVERRIDES, _SCOPE_PREFIXES, _UNIVERSAL_TOKENS, or _CONTEXT_ONLY_TOKENS): {unclassified}'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_neo_py_test_every_obj_field_classifies_to_known_scope_c39ac6288c | 3.3a through 3.7c | planned parity | | tests/test_neo.py::test_field_version_keys_are_obj_fields | Every gated field name must exist on :class:`libtmux.neo.Obj`; assert `token in obj_fields; message=f'{token!r} in FIELD_VERSION but not declared on Obj'`. | unit test | PythonBehaviorParityTest#tests_test_neo_py_test_field_version_keys_are_obj_fields_6ffa97bc75 | not tmux-bound | planned parity | -| tests/test_neo.py::test_format_token_gated_to_3_7[bracket_paste_flag] | Tmux 3.7 pane format tokens must not leak into older -F templates; case inputs `token='bracket_paste_flag'`; execute `get_output_format('list-panes', '3.6')`; `get_output_format('list-panes', '3.7')`; assert `FIELD_VERSION[token] == '3.7'`; `token not in fields_old`; `token in fields_new`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_neo_py_test_format_token_gated_to_3_7_bracket_paste_flag_227b867afb | 3.2a through 3.7b | planned parity | -| tests/test_neo.py::test_format_token_gated_to_3_7[pane_flags] | Tmux 3.7 pane format tokens must not leak into older -F templates; case inputs `token='pane_flags'`; execute `get_output_format('list-panes', '3.6')`; `get_output_format('list-panes', '3.7')`; assert `FIELD_VERSION[token] == '3.7'`; `token not in fields_old`; `token in fields_new`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_neo_py_test_format_token_gated_to_3_7_pane_flags_bda1b12178 | 3.2a through 3.7b | planned parity | -| tests/test_neo.py::test_format_token_gated_to_3_7[pane_floating_flag] | Tmux 3.7 pane format tokens must not leak into older -F templates; case inputs `token='pane_floating_flag'`; execute `get_output_format('list-panes', '3.6')`; `get_output_format('list-panes', '3.7')`; assert `FIELD_VERSION[token] == '3.7'`; `token not in fields_old`; `token in fields_new`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_neo_py_test_format_token_gated_to_3_7_pane_floating_flag_a2d37a85c3 | 3.2a through 3.7b | planned parity | -| tests/test_neo.py::test_format_token_gated_to_3_7[pane_pb_progress] | Tmux 3.7 pane format tokens must not leak into older -F templates; case inputs `token='pane_pb_progress'`; execute `get_output_format('list-panes', '3.6')`; `get_output_format('list-panes', '3.7')`; assert `FIELD_VERSION[token] == '3.7'`; `token not in fields_old`; `token in fields_new`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_neo_py_test_format_token_gated_to_3_7_pane_pb_progress_06eabfa637 | 3.2a through 3.7b | planned parity | -| tests/test_neo.py::test_format_token_gated_to_3_7[pane_pb_state] | Tmux 3.7 pane format tokens must not leak into older -F templates; case inputs `token='pane_pb_state'`; execute `get_output_format('list-panes', '3.6')`; `get_output_format('list-panes', '3.7')`; assert `FIELD_VERSION[token] == '3.7'`; `token not in fields_old`; `token in fields_new`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_neo_py_test_format_token_gated_to_3_7_pane_pb_state_42a8aa832a | 3.2a through 3.7b | planned parity | -| tests/test_neo.py::test_format_token_gated_to_3_7[pane_pipe_pid] | Tmux 3.7 pane format tokens must not leak into older -F templates; case inputs `token='pane_pipe_pid'`; execute `get_output_format('list-panes', '3.6')`; `get_output_format('list-panes', '3.7')`; assert `FIELD_VERSION[token] == '3.7'`; `token not in fields_old`; `token in fields_new`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_neo_py_test_format_token_gated_to_3_7_pane_pipe_pid_33affb61ec | 3.2a through 3.7b | planned parity | -| tests/test_neo.py::test_format_token_gated_to_3_7[pane_x] | Tmux 3.7 pane format tokens must not leak into older -F templates; case inputs `token='pane_x'`; execute `get_output_format('list-panes', '3.6')`; `get_output_format('list-panes', '3.7')`; assert `FIELD_VERSION[token] == '3.7'`; `token not in fields_old`; `token in fields_new`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_neo_py_test_format_token_gated_to_3_7_pane_x_ecadb0feed | 3.2a through 3.7b | planned parity | -| tests/test_neo.py::test_format_token_gated_to_3_7[pane_y] | Tmux 3.7 pane format tokens must not leak into older -F templates; case inputs `token='pane_y'`; execute `get_output_format('list-panes', '3.6')`; `get_output_format('list-panes', '3.7')`; assert `FIELD_VERSION[token] == '3.7'`; `token not in fields_old`; `token in fields_new`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_neo_py_test_format_token_gated_to_3_7_pane_y_dbe34d50e5 | 3.2a through 3.7b | planned parity | -| tests/test_neo.py::test_format_token_gated_to_3_7[pane_z] | Tmux 3.7 pane format tokens must not leak into older -F templates; case inputs `token='pane_z'`; execute `get_output_format('list-panes', '3.6')`; `get_output_format('list-panes', '3.7')`; assert `FIELD_VERSION[token] == '3.7'`; `token not in fields_old`; `token in fields_new`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_neo_py_test_format_token_gated_to_3_7_pane_z_433a952e9a | 3.2a through 3.7b | planned parity | -| tests/test_neo.py::test_format_token_gated_to_3_7[pane_zoomed_flag] | Tmux 3.7 pane format tokens must not leak into older -F templates; case inputs `token='pane_zoomed_flag'`; execute `get_output_format('list-panes', '3.6')`; `get_output_format('list-panes', '3.7')`; assert `FIELD_VERSION[token] == '3.7'`; `token not in fields_old`; `token in fields_new`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_neo_py_test_format_token_gated_to_3_7_pane_zoomed_flag_a4cefd32be | 3.2a through 3.7b | planned parity | -| tests/test_neo.py::test_format_token_gated_to_3_7[synchronized_output_flag] | Tmux 3.7 pane format tokens must not leak into older -F templates; case inputs `token='synchronized_output_flag'`; execute `get_output_format('list-panes', '3.6')`; `get_output_format('list-panes', '3.7')`; assert `FIELD_VERSION[token] == '3.7'`; `token not in fields_old`; `token in fields_new`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_neo_py_test_format_token_gated_to_3_7_synchronized_output_flag_aa6751d603 | 3.2a through 3.7b | planned parity | +| tests/test_neo.py::test_format_token_gated_to_3_7[bracket_paste_flag] | Tmux 3.7 pane format tokens must not leak into older -F templates; case inputs `token='bracket_paste_flag'`; execute `get_output_format('list-panes', '3.6')`; `get_output_format('list-panes', '3.7')`; assert `FIELD_VERSION[token] == '3.7'`; `token not in fields_old`; `token in fields_new`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_neo_py_test_format_token_gated_to_3_7_bracket_paste_flag_227b867afb | 3.3a through 3.7c | planned parity | +| tests/test_neo.py::test_format_token_gated_to_3_7[pane_flags] | Tmux 3.7 pane format tokens must not leak into older -F templates; case inputs `token='pane_flags'`; execute `get_output_format('list-panes', '3.6')`; `get_output_format('list-panes', '3.7')`; assert `FIELD_VERSION[token] == '3.7'`; `token not in fields_old`; `token in fields_new`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_neo_py_test_format_token_gated_to_3_7_pane_flags_bda1b12178 | 3.3a through 3.7c | planned parity | +| tests/test_neo.py::test_format_token_gated_to_3_7[pane_floating_flag] | Tmux 3.7 pane format tokens must not leak into older -F templates; case inputs `token='pane_floating_flag'`; execute `get_output_format('list-panes', '3.6')`; `get_output_format('list-panes', '3.7')`; assert `FIELD_VERSION[token] == '3.7'`; `token not in fields_old`; `token in fields_new`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_neo_py_test_format_token_gated_to_3_7_pane_floating_flag_a2d37a85c3 | 3.3a through 3.7c | planned parity | +| tests/test_neo.py::test_format_token_gated_to_3_7[pane_pb_progress] | Tmux 3.7 pane format tokens must not leak into older -F templates; case inputs `token='pane_pb_progress'`; execute `get_output_format('list-panes', '3.6')`; `get_output_format('list-panes', '3.7')`; assert `FIELD_VERSION[token] == '3.7'`; `token not in fields_old`; `token in fields_new`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_neo_py_test_format_token_gated_to_3_7_pane_pb_progress_06eabfa637 | 3.3a through 3.7c | planned parity | +| tests/test_neo.py::test_format_token_gated_to_3_7[pane_pb_state] | Tmux 3.7 pane format tokens must not leak into older -F templates; case inputs `token='pane_pb_state'`; execute `get_output_format('list-panes', '3.6')`; `get_output_format('list-panes', '3.7')`; assert `FIELD_VERSION[token] == '3.7'`; `token not in fields_old`; `token in fields_new`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_neo_py_test_format_token_gated_to_3_7_pane_pb_state_42a8aa832a | 3.3a through 3.7c | planned parity | +| tests/test_neo.py::test_format_token_gated_to_3_7[pane_pipe_pid] | Tmux 3.7 pane format tokens must not leak into older -F templates; case inputs `token='pane_pipe_pid'`; execute `get_output_format('list-panes', '3.6')`; `get_output_format('list-panes', '3.7')`; assert `FIELD_VERSION[token] == '3.7'`; `token not in fields_old`; `token in fields_new`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_neo_py_test_format_token_gated_to_3_7_pane_pipe_pid_33affb61ec | 3.3a through 3.7c | planned parity | +| tests/test_neo.py::test_format_token_gated_to_3_7[pane_x] | Tmux 3.7 pane format tokens must not leak into older -F templates; case inputs `token='pane_x'`; execute `get_output_format('list-panes', '3.6')`; `get_output_format('list-panes', '3.7')`; assert `FIELD_VERSION[token] == '3.7'`; `token not in fields_old`; `token in fields_new`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_neo_py_test_format_token_gated_to_3_7_pane_x_ecadb0feed | 3.3a through 3.7c | planned parity | +| tests/test_neo.py::test_format_token_gated_to_3_7[pane_y] | Tmux 3.7 pane format tokens must not leak into older -F templates; case inputs `token='pane_y'`; execute `get_output_format('list-panes', '3.6')`; `get_output_format('list-panes', '3.7')`; assert `FIELD_VERSION[token] == '3.7'`; `token not in fields_old`; `token in fields_new`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_neo_py_test_format_token_gated_to_3_7_pane_y_dbe34d50e5 | 3.3a through 3.7c | planned parity | +| tests/test_neo.py::test_format_token_gated_to_3_7[pane_z] | Tmux 3.7 pane format tokens must not leak into older -F templates; case inputs `token='pane_z'`; execute `get_output_format('list-panes', '3.6')`; `get_output_format('list-panes', '3.7')`; assert `FIELD_VERSION[token] == '3.7'`; `token not in fields_old`; `token in fields_new`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_neo_py_test_format_token_gated_to_3_7_pane_z_433a952e9a | 3.3a through 3.7c | planned parity | +| tests/test_neo.py::test_format_token_gated_to_3_7[pane_zoomed_flag] | Tmux 3.7 pane format tokens must not leak into older -F templates; case inputs `token='pane_zoomed_flag'`; execute `get_output_format('list-panes', '3.6')`; `get_output_format('list-panes', '3.7')`; assert `FIELD_VERSION[token] == '3.7'`; `token not in fields_old`; `token in fields_new`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_neo_py_test_format_token_gated_to_3_7_pane_zoomed_flag_a4cefd32be | 3.3a through 3.7c | planned parity | +| tests/test_neo.py::test_format_token_gated_to_3_7[synchronized_output_flag] | Tmux 3.7 pane format tokens must not leak into older -F templates; case inputs `token='synchronized_output_flag'`; execute `get_output_format('list-panes', '3.6')`; `get_output_format('list-panes', '3.7')`; assert `FIELD_VERSION[token] == '3.7'`; `token not in fields_old`; `token in fields_new`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_neo_py_test_format_token_gated_to_3_7_synchronized_output_flag_aa6751d603 | 3.3a through 3.7c | planned parity | | tests/test_neo.py::test_is_target_not_found_error[daemon-not-running] | Only an unknown ``-t`` target means the object is gone; case inputs `expected=False`, `stderr_text='no server running on /tmp/tmux-1000/default'`, `test_id='daemon-not-running'`; execute `_is_target_not_found_error(stderr_text)`; assert `_is_target_not_found_error(stderr_text) is expected`. | unit test | PythonBehaviorParityTest#tests_test_neo_py_test_is_target_not_found_error_daemon_not_running_8b7c1f4171 | not tmux-bound | planned parity | | tests/test_neo.py::test_is_target_not_found_error[missing-pane] | Only an unknown ``-t`` target means the object is gone; case inputs `expected=True`, `stderr_text="can't find pane: %99"`, `test_id='missing-pane'`; execute `_is_target_not_found_error(stderr_text)`; assert `_is_target_not_found_error(stderr_text) is expected`. | unit test | PythonBehaviorParityTest#tests_test_neo_py_test_is_target_not_found_error_missing_pane_522c8bebd4 | not tmux-bound | planned parity | | tests/test_neo.py::test_is_target_not_found_error[missing-session] | Only an unknown ``-t`` target means the object is gone; case inputs `expected=True`, `stderr_text="can't find session: $99"`, `test_id='missing-session'`; execute `_is_target_not_found_error(stderr_text)`; assert `_is_target_not_found_error(stderr_text) is expected`. | unit test | PythonBehaviorParityTest#tests_test_neo_py_test_is_target_not_found_error_missing_session_302b38ca8f | not tmux-bound | planned parity | @@ -892,134 +892,134 @@ under [`tests/`](https://github.com/tmux-python/libtmux/blob/c4a980b/tests) and | tests/test_neo.py::test_is_target_not_found_error[socket-missing] | Only an unknown ``-t`` target means the object is gone; case inputs `expected=False`, `stderr_text='error connecting to /tmp/tmux-1000/nope (No such file or directory)'`, `test_id='socket-missing'`; execute `_is_target_not_found_error(stderr_text)`; assert `_is_target_not_found_error(stderr_text) is expected`. | unit test | PythonBehaviorParityTest#tests_test_neo_py_test_is_target_not_found_error_socket_missing_29b84c5f80 | not tmux-bound | planned parity | | tests/test_neo.py::test_pane_dead_signal_gated_to_3_3 | ``pane_dead_signal`` first registered in tmux 3.3; execute `get_output_format('list-panes', '3.2a')`; `get_output_format('list-panes', '3.3')`; assert `FIELD_VERSION['pane_dead_signal'] == '3.3'`; `'pane_dead_signal' not in fields_old`; `'pane_dead_signal' in fields_new`. | unit test | PythonBehaviorParityTest#tests_test_neo_py_test_pane_dead_signal_gated_to_3_3_b9712c9fa1 | not tmux-bound | planned parity | | tests/test_neo.py::test_pane_dead_time_gated_to_3_3 | ``pane_dead_time`` first registered in tmux 3.3; execute `get_output_format('list-panes', '3.2a')`; `get_output_format('list-panes', '3.3')`; assert `FIELD_VERSION['pane_dead_time'] == '3.3'`; `'pane_dead_time' not in fields_old`; `'pane_dead_time' in fields_new`. | unit test | PythonBehaviorParityTest#tests_test_neo_py_test_pane_dead_time_gated_to_3_3_a6b2c34832 | not tmux-bound | planned parity | -| tests/test_neo.py::test_scopes_by_list_cmd_downward_cascade | Every ``list-*`` admits universal+session+window+pane scopes; assert `{'universal', 'session', 'window', 'pane'} <= scopes; message=f'{cmd} should admit the downward-cascade core scopes'`; `'client' in SCOPES_BY_LIST_CMD['list-clients']`; `'client' not in SCOPES_BY_LIST_CMD['list-sessions']`; `'client' not in SCOPES_BY_LIST_CMD['list-windows']`; `'client' not in SCOPES_BY_LIST_CMD['list-panes']`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_neo_py_test_scopes_by_list_cmd_downward_cascade_2241a7dbf4 | 3.2a through 3.7b | planned parity | +| tests/test_neo.py::test_scopes_by_list_cmd_downward_cascade | Every ``list-*`` admits universal+session+window+pane scopes; assert `{'universal', 'session', 'window', 'pane'} <= scopes; message=f'{cmd} should admit the downward-cascade core scopes'`; `'client' in SCOPES_BY_LIST_CMD['list-clients']`; `'client' not in SCOPES_BY_LIST_CMD['list-sessions']`; `'client' not in SCOPES_BY_LIST_CMD['list-windows']`; `'client' not in SCOPES_BY_LIST_CMD['list-panes']`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_neo_py_test_scopes_by_list_cmd_downward_cascade_2241a7dbf4 | 3.3a through 3.7c | planned parity | | tests/test_neo.py::test_token_scope_unknown_for_unclassified_field | ``_token_scope`` returns ``"unknown"`` for any unrecognized field; execute `_token_scope('libtmux_test_nonexistent_token')`; assert `_token_scope('libtmux_test_nonexistent_token') == 'unknown'`; `'unknown' not in allowed`. | unit test | PythonBehaviorParityTest#tests_test_neo_py_test_token_scope_unknown_for_unclassified_field_65925616d7 | not tmux-bound | planned parity | -| tests/test_neo.py::test_unprefixed_3_7_tokens_are_pane_scope[bracket_paste_flag] | The 3.7 pane tokens without a ``pane_`` prefix need scope overrides; case inputs `token='bracket_paste_flag'`; execute `_token_scope(token)`; `get_output_format('list-panes', '3.7')`; assert `_token_scope(token) == 'pane'`; `token in fields`. | real-tmux integration test | PythonBehaviorParityTest#test_neo_py_test_unprefixed_3_7_tokens_are_pane_scope_bracket_paste_flag_9f9e5c0997 | 3.2a through 3.7b | planned parity | -| tests/test_neo.py::test_unprefixed_3_7_tokens_are_pane_scope[synchronized_output_flag] | The 3.7 pane tokens without a ``pane_`` prefix need scope overrides; case inputs `token='synchronized_output_flag'`; execute `_token_scope(token)`; `get_output_format('list-panes', '3.7')`; assert `_token_scope(token) == 'pane'`; `token in fields`. | real-tmux integration test | PythonBehaviorParityTest#eo_py_test_unprefixed_3_7_tokens_are_pane_scope_synchronized_output_flag_3c87f2a083 | 3.2a through 3.7b | planned parity | +| tests/test_neo.py::test_unprefixed_3_7_tokens_are_pane_scope[bracket_paste_flag] | The 3.7 pane tokens without a ``pane_`` prefix need scope overrides; case inputs `token='bracket_paste_flag'`; execute `_token_scope(token)`; `get_output_format('list-panes', '3.7')`; assert `_token_scope(token) == 'pane'`; `token in fields`. | real-tmux integration test | PythonBehaviorParityTest#test_neo_py_test_unprefixed_3_7_tokens_are_pane_scope_bracket_paste_flag_9f9e5c0997 | 3.3a through 3.7c | planned parity | +| tests/test_neo.py::test_unprefixed_3_7_tokens_are_pane_scope[synchronized_output_flag] | The 3.7 pane tokens without a ``pane_`` prefix need scope overrides; case inputs `token='synchronized_output_flag'`; execute `_token_scope(token)`; `get_output_format('list-panes', '3.7')`; assert `_token_scope(token) == 'pane'`; `token in fields`. | real-tmux integration test | PythonBehaviorParityTest#eo_py_test_unprefixed_3_7_tokens_are_pane_scope_synchronized_output_flag_3c87f2a083 | 3.3a through 3.7c | planned parity | | tests/test_options.py::test_command_alias | Test tmux's command-alias option destructuring; fixtures `server`, `monkeypatch`; execute `server._show_options()`; `any(('command-alias' in k for k in options_))`; `Options(**options_)`; assert `any(('command-alias' in k for k in options_))`; `options`; `options.command_alias`; `isinstance(options_['command-alias'], dict)`; `not isinstance(options_['command-alias'], SparseArray)`; `isinstance(options_['command-alias']['split-pane'], str)`; `options_['command-alias']['split-pane'] == 'split-window'`. | unit test | PythonBehaviorParityTest#tests_test_options_py_test_command_alias_950b7b6c08 | not tmux-bound | planned parity | -| tests/test_options.py::test_complex_option_values | Test complex option values and edge cases; fixtures `server`; execute `server.new_session(session_name='test')`; `session.set_option('@complex-option', 'value with spaces')`; `session.show_option('@complex-option')`; `session.set_option('@escaped-option', 'line1\\nline2')`; `session.show_option('@escaped-option')`; `session.set_option('@empty-option', '')`; assert `session.show_option('@complex-option') == 'value with spaces'`; `session.show_option('@escaped-option') == 'line1\\nline2'`; `session.show_option('@empty-option') == ''`; when `has_gte_version('3.0')`: execute `session.show_option('status-style', include_inherited=True)`; `session.set_option('status-style', 'fg=red')`; `session.show_option('status-style')`; `session.unset_option('status-style')`; assert `session.show_option('status-style') == 'fg=red'`; `session.show_option('status-style', include_inherited=True) == 'fg=red'`; `session.show_option('status-style', include_inherited=True) == inherited_val`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_complex_option_values_7c482e4804 | 3.2a through 3.7b; conditional boundaries: 3.0 | planned parity | +| tests/test_options.py::test_complex_option_values | Test complex option values and edge cases; fixtures `server`; execute `server.new_session(session_name='test')`; `session.set_option('@complex-option', 'value with spaces')`; `session.show_option('@complex-option')`; `session.set_option('@escaped-option', 'line1\\nline2')`; `session.show_option('@escaped-option')`; `session.set_option('@empty-option', '')`; assert `session.show_option('@complex-option') == 'value with spaces'`; `session.show_option('@escaped-option') == 'line1\\nline2'`; `session.show_option('@empty-option') == ''`; when `has_gte_version('3.0')`: execute `session.show_option('status-style', include_inherited=True)`; `session.set_option('status-style', 'fg=red')`; `session.show_option('status-style')`; `session.unset_option('status-style')`; assert `session.show_option('status-style') == 'fg=red'`; `session.show_option('status-style', include_inherited=True) == 'fg=red'`; `session.show_option('status-style', include_inherited=True) == inherited_val`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_complex_option_values_7c482e4804 | 3.3a through 3.7c; conditional boundaries: 3.0 | planned parity | | tests/test_options.py::test_convert_values_preserves_sparse_keys | Test convert_values preserves sparse array indices; execute `SparseArray()`; `convert_values(sparse)`; assert `isinstance(result, SparseArray)`; `sorted(result.keys()) == [0, 5, 99]`; `result[0] is True`; `result[5] is False`; `result[99] == 100`. | unit test | PythonBehaviorParityTest#tests_test_options_py_test_convert_values_preserves_sparse_keys_b5de8b093c | not tmux-bound | planned parity | | tests/test_options.py::test_convert_values_sparse_array[boolean_on_off] | Test convert_values handles SparseArray via dict branch; case inputs `test_case=ConvertValuesSparseTestCase(test_id='boolean_on_off', initial_values={0: 'on', 1: 'off'}, expected_converted={0: True, 1: False})`; execute `SparseArray()`; `convert_values(sparse)`; assert `isinstance(result, SparseArray)`; `result[idx] == expected; message=f'Index {idx}: {result[idx]} != {expected}'`. | unit test | PythonBehaviorParityTest#tests_test_options_py_test_convert_values_sparse_array_boolean_on_off_056ccd9607 | not tmux-bound | planned parity | | tests/test_options.py::test_convert_values_sparse_array[mixed_values] | Test convert_values handles SparseArray via dict branch; case inputs `test_case=ConvertValuesSparseTestCase(test_id='mixed_values', initial_values={0: 'on', 1: '50', 2: 'text'}, expected_converted={0: True, 1: 50, 2: 'text'})`; execute `SparseArray()`; `convert_values(sparse)`; assert `isinstance(result, SparseArray)`; `result[idx] == expected; message=f'Index {idx}: {result[idx]} != {expected}'`. | unit test | PythonBehaviorParityTest#tests_test_options_py_test_convert_values_sparse_array_mixed_values_5c77be6d70 | not tmux-bound | planned parity | | tests/test_options.py::test_convert_values_sparse_array[numeric_conversion] | Test convert_values handles SparseArray via dict branch; case inputs `test_case=ConvertValuesSparseTestCase(test_id='numeric_conversion', initial_values={0: '50', 5: '100'}, expected_converted={0: 50, 5: 100})`; execute `SparseArray()`; `convert_values(sparse)`; assert `isinstance(result, SparseArray)`; `result[idx] == expected; message=f'Index {idx}: {result[idx]} != {expected}'`. | unit test | PythonBehaviorParityTest#ests_test_options_py_test_convert_values_sparse_array_numeric_conversion_a99850dc83 | not tmux-bound | planned parity | | tests/test_options.py::test_convert_values_sparse_array[sparse_indices] | Test convert_values handles SparseArray via dict branch; case inputs `test_case=ConvertValuesSparseTestCase(test_id='sparse_indices', initial_values={0: 'on', 10: 'off', 100: '42'}, expected_converted={0: True, 10: False, 100: 42})`; execute `SparseArray()`; `convert_values(sparse)`; assert `isinstance(result, SparseArray)`; `result[idx] == expected; message=f'Index {idx}: {result[idx]} != {expected}'`. | unit test | PythonBehaviorParityTest#tests_test_options_py_test_convert_values_sparse_array_sparse_indices_9dc8b27008 | not tmux-bound | planned parity | -| tests/test_options.py::test_custom_options | Test tmux's user (custom) options; fixtures `server`; execute `server.new_session(session_name='test')`; `session.set_option('@custom-option', 'test')`; `session._show_option('@custom-option')`; assert `session._show_option('@custom-option') == 'test'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_custom_options_98004aaa03 | 3.2a through 3.7b | planned parity | +| tests/test_options.py::test_custom_options | Test tmux's user (custom) options; fixtures `server`; execute `server.new_session(session_name='test')`; `session.set_option('@custom-option', 'test')`; `session._show_option('@custom-option')`; assert `session._show_option('@custom-option') == 'test'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_custom_options_98004aaa03 | 3.3a through 3.7c | planned parity | | tests/test_options.py::test_explode_arrays_preserves_inherited_marker[inherited_array_marker_preserved] | Test that explode_arrays preserves the inherited marker (*) for array options; case inputs `test_case=ExplodeArraysInheritedCase(test_id='inherited_array_marker_preserved', raw_input={'status-format[0]*': 'fmt0', 'status-format[1]*': 'fmt1'}, expected_key='status-format*', expected_indices={0: 'fmt0', 1: 'fmt1'})`; execute `explode_arrays(test_case.raw_input)`; assert `test_case.expected_key in result; message=f"Expected key '{test_case.expected_key}' not found in result. Got keys: {list(result.keys())}"`; `isinstance(array_value, SparseArray); message=f'Expected SparseArray for array option, got {type(array_value).__name__}'`; `idx in array_value; message=f'Expected index {idx} not found in array. Got indices: {list(array_value.keys())}'`; `array_value[idx] == expected_val; message=f"Value at index {idx}: expected '{expected_val}', got '{array_value[idx]}'"`. | unit test | PythonBehaviorParityTest#plode_arrays_preserves_inherited_marker_inherited_array_marker_preserved_619b7cbc84 | not tmux-bound | planned parity | | tests/test_options.py::test_explode_arrays_preserves_inherited_marker[mixed_inherited_indices] | Test that explode_arrays preserves the inherited marker (*) for array options; case inputs `test_case=ExplodeArraysInheritedCase(test_id='mixed_inherited_indices', raw_input={'opt[0]*': 'v0', 'opt[5]*': 'v5', 'opt[10]*': 'v10'}, expected_key='opt*', expected_indices={0: 'v0', 5: 'v5', 10: 'v10'})`; execute `explode_arrays(test_case.raw_input)`; assert `test_case.expected_key in result; message=f"Expected key '{test_case.expected_key}' not found in result. Got keys: {list(result.keys())}"`; `isinstance(array_value, SparseArray); message=f'Expected SparseArray for array option, got {type(array_value).__name__}'`; `idx in array_value; message=f'Expected index {idx} not found in array. Got indices: {list(array_value.keys())}'`; `array_value[idx] == expected_val; message=f"Value at index {idx}: expected '{expected_val}', got '{array_value[idx]}'"`. | unit test | PythonBehaviorParityTest#y_test_explode_arrays_preserves_inherited_marker_mixed_inherited_indices_0515665813 | not tmux-bound | planned parity | | tests/test_options.py::test_explode_arrays_preserves_inherited_marker[non_inherited_array_no_marker] | Test that explode_arrays preserves the inherited marker (*) for array options; case inputs `test_case=ExplodeArraysInheritedCase(test_id='non_inherited_array_no_marker', raw_input={'status-format[0]': 'fmt0', 'status-format[1]': 'fmt1'}, expected_key='status-format', expected_indices={0: 'fmt0', 1: 'fmt1'})`; execute `explode_arrays(test_case.raw_input)`; assert `test_case.expected_key in result; message=f"Expected key '{test_case.expected_key}' not found in result. Got keys: {list(result.keys())}"`; `isinstance(array_value, SparseArray); message=f'Expected SparseArray for array option, got {type(array_value).__name__}'`; `idx in array_value; message=f'Expected index {idx} not found in array. Got indices: {list(array_value.keys())}'`; `array_value[idx] == expected_val; message=f"Value at index {idx}: expected '{expected_val}', got '{array_value[idx]}'"`. | unit test | PythonBehaviorParityTest#_explode_arrays_preserves_inherited_marker_non_inherited_array_no_marker_5d4bf6d8c0 | not tmux-bound | planned parity | -| tests/test_options.py::test_high_level_api_expectations | Ensure options and hooks behave as expected; fixtures `server`; the collected body has no executable statement beyond its docstring, so invocation returns normally without tmux calls or state changes. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_high_level_api_expectations_09a250503a | 3.2a through 3.7b | planned parity | +| tests/test_options.py::test_high_level_api_expectations | Ensure options and hooks behave as expected; fixtures `server`; the collected body has no executable statement beyond its docstring, so invocation returns normally without tmux calls or state changes. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_high_level_api_expectations_09a250503a | 3.3a through 3.7c | planned parity | | tests/test_options.py::test_mocked_cmd_stdoutclass_fixture[command-alias] | Parametrized test grid for options; case inputs `dataclass_attribute='command_alias'`, `expected={'split-pane': 'split-window', 'splitp': 'split-window', 'server-info': 'show-messages -JT', 'info': 'show-messages -JT', 'choose-window': 'choose-tree -w', 'choose-session': 'choose-tree -s'}`, `mocked_cmd_stdout=['command-alias[0] split-pane=split-window', 'command-alias[1] splitp=split-window', 'command-alias[2] "server-info=show-messages -JT"', 'command-alias[3] "info=show-messages -JT"', 'command-alias[4] "choose-window=choose-tree -w"', 'command-alias[5] "choose-session=choose-tree -s"']`, `test_id='command-alias'`, `tmux_option='command-alias'`; fixtures `monkeypatch`, `server`; execute `server._show_options()`; `fake_cmd(stdout=mocked_cmd_stdout)`; `any((tmux_option in k for k in options_))`; `Options(**options_)`; assert `any((tmux_option in k for k in options_))`; `options`; `hasattr(options, dataclass_attribute)`; `getattr(options, dataclass_attribute, None) == expected`. | unit test | PythonBehaviorParityTest#tests_test_options_py_test_mocked_cmd_stdoutclass_fixture_command_alias_8d2f0c459e | not tmux-bound | planned parity | | tests/test_options.py::test_mocked_cmd_stdoutclass_fixture[terminal-features] | Parametrized test grid for options; case inputs `dataclass_attribute='terminal_features'`, `expected={'screen*': ['title'], 'xterm*': ['clipboard', 'ccolour', 'cstyle', 'focus']}`, `mocked_cmd_stdout=['terminal-features[0] xterm*:clipboard:ccolour:cstyle:focus', 'terminal-features[1] screen*:title']`, `test_id='terminal-features'`, `tmux_option='terminal-features'`; fixtures `monkeypatch`, `server`; execute `server._show_options()`; `fake_cmd(stdout=mocked_cmd_stdout)`; `any((tmux_option in k for k in options_))`; `Options(**options_)`; assert `any((tmux_option in k for k in options_))`; `options`; `hasattr(options, dataclass_attribute)`; `getattr(options, dataclass_attribute, None) == expected`. | unit test | PythonBehaviorParityTest#ts_test_options_py_test_mocked_cmd_stdoutclass_fixture_terminal_features_4ef3a61813 | not tmux-bound | planned parity | -| tests/test_options.py::test_option_error_handling | Test error handling for options; fixtures `server`; execute `server.new_session(session_name='test')`; `session.show_option('non-existent-option')`; `session.set_option('aggressive-resize', 'invalid')`; `any((msg in error_msg for msg in ['unknown option', 'invalid option']))`; expect `pytest.raises(exc.OptionError)`; assert `any((msg in error_msg for msg in ['unknown option', 'invalid option']))`; when `has_gte_version('2.4')`: execute `session.show_option('window-')`; expect `pytest.raises(exc.OptionError)`; assert `'ambiguous option' in str(exc_info.value).lower()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_error_handling_ffb224428f | 3.2a through 3.7b; conditional boundaries: 2.4 | planned parity | +| tests/test_options.py::test_option_error_handling | Test error handling for options; fixtures `server`; execute `server.new_session(session_name='test')`; `session.show_option('non-existent-option')`; `session.set_option('aggressive-resize', 'invalid')`; `any((msg in error_msg for msg in ['unknown option', 'invalid option']))`; expect `pytest.raises(exc.OptionError)`; assert `any((msg in error_msg for msg in ['unknown option', 'invalid option']))`; when `has_gte_version('2.4')`: execute `session.show_option('window-')`; expect `pytest.raises(exc.OptionError)`; assert `'ambiguous option' in str(exc_info.value).lower()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_error_handling_ffb224428f | 3.3a through 3.7c; conditional boundaries: 2.4 | planned parity | | tests/test_options.py::test_option_set_show_cycle[pane_allow_passthrough] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='pane_allow_passthrough', option='allow-passthrough', scope=<OptionScope.Pane: 'PANE'>, test_value='all', expected_type=<class 'str'>, min_version='3.4', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_pane_allow_passthrough_12a06d295a | 3.4 through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[pane_allow_rename] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='pane_allow_rename', option='allow-rename', scope=<OptionScope.Pane: 'PANE'>, test_value='off', expected_type=<class 'bool'>, min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_pane_allow_rename_3e104b71cb | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[pane_alternate_screen] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='pane_alternate_screen', option='alternate-screen', scope=<OptionScope.Pane: 'PANE'>, test_value='on', expected_type=<class 'bool'>, min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_pane_alternate_screen_664fc6e203 | 3.2a through 3.7b | planned parity | +| tests/test_options.py::test_option_set_show_cycle[pane_allow_rename] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='pane_allow_rename', option='allow-rename', scope=<OptionScope.Pane: 'PANE'>, test_value='off', expected_type=<class 'bool'>, min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_pane_allow_rename_3e104b71cb | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[pane_alternate_screen] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='pane_alternate_screen', option='alternate-screen', scope=<OptionScope.Pane: 'PANE'>, test_value='on', expected_type=<class 'bool'>, min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_pane_alternate_screen_664fc6e203 | 3.3a through 3.7c | planned parity | | tests/test_options.py::test_option_set_show_cycle[pane_pane_active_border_style] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='pane_pane_active_border_style', option='pane-active-border-style', scope=<OptionScope.Pane: 'PANE'>, test_value='fg=green', expected_type=<class 'str'>, min_version='3.7', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#test_options_py_test_option_set_show_cycle_pane_pane_active_border_style_37f4c6d2f6 | 3.7 through 3.7b | planned parity | | tests/test_options.py::test_option_set_show_cycle[pane_pane_border_style] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='pane_pane_border_style', option='pane-border-style', scope=<OptionScope.Pane: 'PANE'>, test_value='fg=red', expected_type=<class 'str'>, min_version='3.7', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_pane_pane_border_style_76e9dfba40 | 3.7 through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[pane_remain_on_exit] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='pane_remain_on_exit', option='remain-on-exit', scope=<OptionScope.Pane: 'PANE'>, test_value='failed', expected_type=<class 'str'>, min_version='3.2', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_pane_remain_on_exit_e37d2f7371 | 3.2a through 3.7b | planned parity | +| tests/test_options.py::test_option_set_show_cycle[pane_remain_on_exit] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='pane_remain_on_exit', option='remain-on-exit', scope=<OptionScope.Pane: 'PANE'>, test_value='failed', expected_type=<class 'str'>, min_version='3.2', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_pane_remain_on_exit_e37d2f7371 | 3.3a through 3.7c | planned parity | | tests/test_options.py::test_option_set_show_cycle[pane_remain_on_exit_key] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='pane_remain_on_exit_key', option='remain-on-exit', scope=<OptionScope.Pane: 'PANE'>, test_value='key', expected_type=<class 'str'>, min_version='3.7', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_pane_remain_on_exit_key_558aee681d | 3.7 through 3.7b | planned parity | | tests/test_options.py::test_option_set_show_cycle[pane_scroll_on_clear] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='pane_scroll_on_clear', option='scroll-on-clear', scope=<OptionScope.Pane: 'PANE'>, test_value='on', expected_type=<class 'bool'>, min_version='3.3', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_pane_scroll_on_clear_79b6bc2569 | 3.3 through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[pane_synchronize_panes] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='pane_synchronize_panes', option='synchronize-panes', scope=<OptionScope.Pane: 'PANE'>, test_value='off', expected_type=<class 'bool'>, min_version='3.1', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_pane_synchronize_panes_be414060d5 | 3.2a through 3.7b | planned parity | +| tests/test_options.py::test_option_set_show_cycle[pane_synchronize_panes] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='pane_synchronize_panes', option='synchronize-panes', scope=<OptionScope.Pane: 'PANE'>, test_value='off', expected_type=<class 'bool'>, min_version='3.1', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_pane_synchronize_panes_be414060d5 | 3.3a through 3.7c | planned parity | | tests/test_options.py::test_option_set_show_cycle[pane_tree_mode_preview_format] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='pane_tree_mode_preview_format', option='tree-mode-preview-format', scope=<OptionScope.Pane: 'PANE'>, test_value='#{pane_index}', expected_type=<class 'str'>, min_version='3.7', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#test_options_py_test_option_set_show_cycle_pane_tree_mode_preview_format_f2b345143c | 3.7 through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[pane_window_active_style] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='pane_window_active_style', option='window-active-style', scope=<OptionScope.Pane: 'PANE'>, test_value='default', expected_type=<class 'str'>, min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_options_py_test_option_set_show_cycle_pane_window_active_style_c4141066c8 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[pane_window_style] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='pane_window_style', option='window-style', scope=<OptionScope.Pane: 'PANE'>, test_value='default', expected_type=<class 'str'>, min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_pane_window_style_e5a06d3db8 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[server_buffer_limit] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='server_buffer_limit', option='buffer-limit', scope=<OptionScope.Server: 'SERVER'>, test_value=100, expected_type=<class 'int'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_server_buffer_limit_fde518f49f | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[server_default_terminal] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='server_default_terminal', option='default-terminal', scope=<OptionScope.Server: 'SERVER'>, test_value='screen-256color', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_server_default_terminal_c68280f866 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[server_editor] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='server_editor', option='editor', scope=<OptionScope.Server: 'SERVER'>, test_value='vim', expected_type=<class 'str'>, min_version='3.2', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_server_editor_486f671cd0 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[server_escape_time] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='server_escape_time', option='escape-time', scope=<OptionScope.Server: 'SERVER'>, test_value=50, expected_type=<class 'int'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_server_escape_time_2242ffb026 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[server_exit_empty] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='server_exit_empty', option='exit-empty', scope=<OptionScope.Server: 'SERVER'>, test_value='off', expected_type=<class 'bool'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_server_exit_empty_1e6a27b2f8 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[server_exit_unattached] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='server_exit_unattached', option='exit-unattached', scope=<OptionScope.Server: 'SERVER'>, test_value='off', expected_type=<class 'bool'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_server_exit_unattached_8f0b340992 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[server_extended_keys] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='server_extended_keys', option='extended-keys', scope=<OptionScope.Server: 'SERVER'>, test_value='always', expected_type=<class 'str'>, min_version='3.2', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_server_extended_keys_c42e8b82ab | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[server_focus_events] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='server_focus_events', option='focus-events', scope=<OptionScope.Server: 'SERVER'>, test_value='on', expected_type=<class 'bool'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_server_focus_events_191244c0ad | 3.2a through 3.7b | planned parity | +| tests/test_options.py::test_option_set_show_cycle[pane_window_active_style] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='pane_window_active_style', option='window-active-style', scope=<OptionScope.Pane: 'PANE'>, test_value='default', expected_type=<class 'str'>, min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_options_py_test_option_set_show_cycle_pane_window_active_style_c4141066c8 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[pane_window_style] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='pane_window_style', option='window-style', scope=<OptionScope.Pane: 'PANE'>, test_value='default', expected_type=<class 'str'>, min_version='3.0', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_pane_window_style_e5a06d3db8 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[server_buffer_limit] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='server_buffer_limit', option='buffer-limit', scope=<OptionScope.Server: 'SERVER'>, test_value=100, expected_type=<class 'int'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_server_buffer_limit_fde518f49f | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[server_default_terminal] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='server_default_terminal', option='default-terminal', scope=<OptionScope.Server: 'SERVER'>, test_value='screen-256color', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_server_default_terminal_c68280f866 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[server_editor] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='server_editor', option='editor', scope=<OptionScope.Server: 'SERVER'>, test_value='vim', expected_type=<class 'str'>, min_version='3.2', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_server_editor_486f671cd0 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[server_escape_time] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='server_escape_time', option='escape-time', scope=<OptionScope.Server: 'SERVER'>, test_value=50, expected_type=<class 'int'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_server_escape_time_2242ffb026 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[server_exit_empty] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='server_exit_empty', option='exit-empty', scope=<OptionScope.Server: 'SERVER'>, test_value='off', expected_type=<class 'bool'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_server_exit_empty_1e6a27b2f8 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[server_exit_unattached] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='server_exit_unattached', option='exit-unattached', scope=<OptionScope.Server: 'SERVER'>, test_value='off', expected_type=<class 'bool'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_server_exit_unattached_8f0b340992 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[server_extended_keys] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='server_extended_keys', option='extended-keys', scope=<OptionScope.Server: 'SERVER'>, test_value='always', expected_type=<class 'str'>, min_version='3.2', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_server_extended_keys_c42e8b82ab | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[server_focus_events] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='server_focus_events', option='focus-events', scope=<OptionScope.Server: 'SERVER'>, test_value='on', expected_type=<class 'bool'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_server_focus_events_191244c0ad | 3.3a through 3.7c | planned parity | | tests/test_options.py::test_option_set_show_cycle[server_get_clipboard] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='server_get_clipboard', option='get-clipboard', scope=<OptionScope.Server: 'SERVER'>, test_value='buffer', expected_type=<class 'str'>, min_version='3.7', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_server_get_clipboard_e757f39410 | 3.7 through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[server_message_limit] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='server_message_limit', option='message-limit', scope=<OptionScope.Server: 'SERVER'>, test_value=500, expected_type=<class 'int'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_server_message_limit_36843a9ec1 | 3.2a through 3.7b | planned parity | +| tests/test_options.py::test_option_set_show_cycle[server_message_limit] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='server_message_limit', option='message-limit', scope=<OptionScope.Server: 'SERVER'>, test_value=500, expected_type=<class 'int'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_server_message_limit_36843a9ec1 | 3.3a through 3.7c | planned parity | | tests/test_options.py::test_option_set_show_cycle[server_prompt_history_limit] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='server_prompt_history_limit', option='prompt-history-limit', scope=<OptionScope.Server: 'SERVER'>, test_value=50, expected_type=<class 'int'>, min_version='3.3', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#s_test_options_py_test_option_set_show_cycle_server_prompt_history_limit_a55725a367 | 3.3 through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[server_set_clipboard] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='server_set_clipboard', option='set-clipboard', scope=<OptionScope.Server: 'SERVER'>, test_value='external', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_server_set_clipboard_263616c43a | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[session_activity_action] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_activity_action', option='activity-action', scope=<OptionScope.Session: 'SESSION'>, test_value='any', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_activity_action_6ec8a6e029 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[session_base_index] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_base_index', option='base-index', scope=<OptionScope.Session: 'SESSION'>, test_value=1, expected_type=<class 'int'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_base_index_867d8579bf | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[session_bell_action] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_bell_action', option='bell-action', scope=<OptionScope.Session: 'SESSION'>, test_value='any', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_bell_action_8cbfcb6638 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[session_default_command] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_default_command', option='default-command', scope=<OptionScope.Session: 'SESSION'>, test_value='', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_default_command_8b2a0d1b6a | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[session_destroy_unattached] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_destroy_unattached', option='destroy-unattached', scope=<OptionScope.Session: 'SESSION'>, test_value='off', expected_type=<class 'bool'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#ts_test_options_py_test_option_set_show_cycle_session_destroy_unattached_16858a4b4d | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[session_detach_on_destroy] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_detach_on_destroy', option='detach-on-destroy', scope=<OptionScope.Session: 'SESSION'>, test_value='no-detached', expected_type=<class 'str'>, min_version='3.2', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#sts_test_options_py_test_option_set_show_cycle_session_detach_on_destroy_6023bb9baa | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[session_display_panes_time] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_display_panes_time', option='display-panes-time', scope=<OptionScope.Session: 'SESSION'>, test_value=2000, expected_type=<class 'int'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#ts_test_options_py_test_option_set_show_cycle_session_display_panes_time_9e7dab4a62 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[session_display_time] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_display_time', option='display-time', scope=<OptionScope.Session: 'SESSION'>, test_value=1000, expected_type=<class 'int'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_display_time_e418680538 | 3.2a through 3.7b | planned parity | +| tests/test_options.py::test_option_set_show_cycle[server_set_clipboard] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='server_set_clipboard', option='set-clipboard', scope=<OptionScope.Server: 'SERVER'>, test_value='external', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_server_set_clipboard_263616c43a | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[session_activity_action] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_activity_action', option='activity-action', scope=<OptionScope.Session: 'SESSION'>, test_value='any', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_activity_action_6ec8a6e029 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[session_base_index] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_base_index', option='base-index', scope=<OptionScope.Session: 'SESSION'>, test_value=1, expected_type=<class 'int'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_base_index_867d8579bf | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[session_bell_action] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_bell_action', option='bell-action', scope=<OptionScope.Session: 'SESSION'>, test_value='any', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_bell_action_8cbfcb6638 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[session_default_command] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_default_command', option='default-command', scope=<OptionScope.Session: 'SESSION'>, test_value='', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_default_command_8b2a0d1b6a | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[session_destroy_unattached] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_destroy_unattached', option='destroy-unattached', scope=<OptionScope.Session: 'SESSION'>, test_value='off', expected_type=<class 'bool'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#ts_test_options_py_test_option_set_show_cycle_session_destroy_unattached_16858a4b4d | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[session_detach_on_destroy] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_detach_on_destroy', option='detach-on-destroy', scope=<OptionScope.Session: 'SESSION'>, test_value='no-detached', expected_type=<class 'str'>, min_version='3.2', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#sts_test_options_py_test_option_set_show_cycle_session_detach_on_destroy_6023bb9baa | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[session_display_panes_time] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_display_panes_time', option='display-panes-time', scope=<OptionScope.Session: 'SESSION'>, test_value=2000, expected_type=<class 'int'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#ts_test_options_py_test_option_set_show_cycle_session_display_panes_time_9e7dab4a62 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[session_display_time] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_display_time', option='display-time', scope=<OptionScope.Session: 'SESSION'>, test_value=1000, expected_type=<class 'int'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_display_time_e418680538 | 3.3a through 3.7c | planned parity | | tests/test_options.py::test_option_set_show_cycle[session_focus_follows_mouse] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_focus_follows_mouse', option='focus-follows-mouse', scope=<OptionScope.Session: 'SESSION'>, test_value='on', expected_type=<class 'bool'>, min_version='3.7', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#s_test_options_py_test_option_set_show_cycle_session_focus_follows_mouse_a5adeb878d | 3.7 through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[session_history_limit] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_history_limit', option='history-limit', scope=<OptionScope.Session: 'SESSION'>, test_value=5000, expected_type=<class 'int'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_history_limit_47a8ea057b | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[session_lock_after_time] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_lock_after_time', option='lock-after-time', scope=<OptionScope.Session: 'SESSION'>, test_value=300, expected_type=<class 'int'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_lock_after_time_4a657dbcc5 | 3.2a through 3.7b | planned parity | +| tests/test_options.py::test_option_set_show_cycle[session_history_limit] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_history_limit', option='history-limit', scope=<OptionScope.Session: 'SESSION'>, test_value=5000, expected_type=<class 'int'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_history_limit_47a8ea057b | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[session_lock_after_time] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_lock_after_time', option='lock-after-time', scope=<OptionScope.Session: 'SESSION'>, test_value=300, expected_type=<class 'int'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_lock_after_time_4a657dbcc5 | 3.3a through 3.7c | planned parity | | tests/test_options.py::test_option_set_show_cycle[session_message_format] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_message_format', option='message-format', scope=<OptionScope.Session: 'SESSION'>, test_value='#[align=centre]#{message}', expected_type=<class 'str'>, min_version='3.7', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_message_format_1864643569 | 3.7 through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[session_message_style] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_message_style', option='message-style', scope=<OptionScope.Session: 'SESSION'>, test_value='fg=red', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_message_style_a256c72f06 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[session_mouse] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_mouse', option='mouse', scope=<OptionScope.Session: 'SESSION'>, test_value='on', expected_type=<class 'bool'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_mouse_ff6214a68c | 3.2a through 3.7b | planned parity | +| tests/test_options.py::test_option_set_show_cycle[session_message_style] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_message_style', option='message-style', scope=<OptionScope.Session: 'SESSION'>, test_value='fg=red', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_message_style_a256c72f06 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[session_mouse] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_mouse', option='mouse', scope=<OptionScope.Session: 'SESSION'>, test_value='on', expected_type=<class 'bool'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_mouse_ff6214a68c | 3.3a through 3.7c | planned parity | | tests/test_options.py::test_option_set_show_cycle[session_prompt_command_cursor_style] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_prompt_command_cursor_style', option='prompt-command-cursor-style', scope=<OptionScope.Session: 'SESSION'>, test_value='blinking-block', expected_type=<class 'str'>, min_version='3.7', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#ptions_py_test_option_set_show_cycle_session_prompt_command_cursor_style_31ed24c94b | 3.7 through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[session_renumber_windows] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_renumber_windows', option='renumber-windows', scope=<OptionScope.Session: 'SESSION'>, test_value='on', expected_type=<class 'bool'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_options_py_test_option_set_show_cycle_session_renumber_windows_8be38a4fdf | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[session_repeat_time] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_repeat_time', option='repeat-time', scope=<OptionScope.Session: 'SESSION'>, test_value=500, expected_type=<class 'int'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_repeat_time_55f36ff80e | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[session_set_titles] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_set_titles', option='set-titles', scope=<OptionScope.Session: 'SESSION'>, test_value='on', expected_type=<class 'bool'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_set_titles_54cd8c349d | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[session_silence_action] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_silence_action', option='silence-action', scope=<OptionScope.Session: 'SESSION'>, test_value='none', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_silence_action_ce97da6076 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[session_status_interval] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_status_interval', option='status-interval', scope=<OptionScope.Session: 'SESSION'>, test_value=5, expected_type=<class 'int'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_status_interval_1880b698af | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[session_status_justify] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_status_justify', option='status-justify', scope=<OptionScope.Session: 'SESSION'>, test_value='left', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_status_justify_06b13901fd | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[session_status_keys] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_status_keys', option='status-keys', scope=<OptionScope.Session: 'SESSION'>, test_value='vi', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_status_keys_1211187a15 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[session_status_left] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_status_left', option='status-left', scope=<OptionScope.Session: 'SESSION'>, test_value='[#S]', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_status_left_743bba617f | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[session_status_left_length] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_status_left_length', option='status-left-length', scope=<OptionScope.Session: 'SESSION'>, test_value=20, expected_type=<class 'int'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#ts_test_options_py_test_option_set_show_cycle_session_status_left_length_56b8b180d1 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[session_status_left_style] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_status_left_style', option='status-left-style', scope=<OptionScope.Session: 'SESSION'>, test_value='fg=blue', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#sts_test_options_py_test_option_set_show_cycle_session_status_left_style_cca978a727 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[session_status_position] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_status_position', option='status-position', scope=<OptionScope.Session: 'SESSION'>, test_value='bottom', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_status_position_76f3498d14 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[session_status_right] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_status_right', option='status-right', scope=<OptionScope.Session: 'SESSION'>, test_value='%H:%M', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_status_right_913acd3558 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[session_status_right_length] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_status_right_length', option='status-right-length', scope=<OptionScope.Session: 'SESSION'>, test_value=30, expected_type=<class 'int'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#s_test_options_py_test_option_set_show_cycle_session_status_right_length_14dba990ab | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[session_status_right_style] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_status_right_style', option='status-right-style', scope=<OptionScope.Session: 'SESSION'>, test_value='fg=yellow', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#ts_test_options_py_test_option_set_show_cycle_session_status_right_style_b2f65c65b6 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[session_status_style] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_status_style', option='status-style', scope=<OptionScope.Session: 'SESSION'>, test_value='fg=green', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_status_style_d25802d7b3 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[session_visual_activity] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_visual_activity', option='visual-activity', scope=<OptionScope.Session: 'SESSION'>, test_value='both', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_visual_activity_8f9110a162 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[session_visual_bell] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_visual_bell', option='visual-bell', scope=<OptionScope.Session: 'SESSION'>, test_value='both', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_visual_bell_93d992471b | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[session_visual_silence] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_visual_silence', option='visual-silence', scope=<OptionScope.Session: 'SESSION'>, test_value='both', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_visual_silence_2b3bab3d1b | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[window_aggressive_resize] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_aggressive_resize', option='aggressive-resize', scope=<OptionScope.Window: 'WINDOW'>, test_value='on', expected_type=<class 'bool'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_options_py_test_option_set_show_cycle_window_aggressive_resize_34a4e0a35f | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[window_automatic_rename] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_automatic_rename', option='automatic-rename', scope=<OptionScope.Window: 'WINDOW'>, test_value='off', expected_type=<class 'bool'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_window_automatic_rename_9cdbdcfde2 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[window_clock_mode_style] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_clock_mode_style', option='clock-mode-style', scope=<OptionScope.Window: 'WINDOW'>, test_value='24', expected_type=<class 'int'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_window_clock_mode_style_7f71d3b7b2 | 3.2a through 3.7b | planned parity | +| tests/test_options.py::test_option_set_show_cycle[session_renumber_windows] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_renumber_windows', option='renumber-windows', scope=<OptionScope.Session: 'SESSION'>, test_value='on', expected_type=<class 'bool'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_options_py_test_option_set_show_cycle_session_renumber_windows_8be38a4fdf | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[session_repeat_time] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_repeat_time', option='repeat-time', scope=<OptionScope.Session: 'SESSION'>, test_value=500, expected_type=<class 'int'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_repeat_time_55f36ff80e | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[session_set_titles] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_set_titles', option='set-titles', scope=<OptionScope.Session: 'SESSION'>, test_value='on', expected_type=<class 'bool'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_set_titles_54cd8c349d | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[session_silence_action] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_silence_action', option='silence-action', scope=<OptionScope.Session: 'SESSION'>, test_value='none', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_silence_action_ce97da6076 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[session_status_interval] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_status_interval', option='status-interval', scope=<OptionScope.Session: 'SESSION'>, test_value=5, expected_type=<class 'int'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_status_interval_1880b698af | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[session_status_justify] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_status_justify', option='status-justify', scope=<OptionScope.Session: 'SESSION'>, test_value='left', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_status_justify_06b13901fd | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[session_status_keys] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_status_keys', option='status-keys', scope=<OptionScope.Session: 'SESSION'>, test_value='vi', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_status_keys_1211187a15 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[session_status_left] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_status_left', option='status-left', scope=<OptionScope.Session: 'SESSION'>, test_value='[#S]', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_status_left_743bba617f | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[session_status_left_length] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_status_left_length', option='status-left-length', scope=<OptionScope.Session: 'SESSION'>, test_value=20, expected_type=<class 'int'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#ts_test_options_py_test_option_set_show_cycle_session_status_left_length_56b8b180d1 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[session_status_left_style] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_status_left_style', option='status-left-style', scope=<OptionScope.Session: 'SESSION'>, test_value='fg=blue', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#sts_test_options_py_test_option_set_show_cycle_session_status_left_style_cca978a727 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[session_status_position] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_status_position', option='status-position', scope=<OptionScope.Session: 'SESSION'>, test_value='bottom', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_status_position_76f3498d14 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[session_status_right] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_status_right', option='status-right', scope=<OptionScope.Session: 'SESSION'>, test_value='%H:%M', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_status_right_913acd3558 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[session_status_right_length] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_status_right_length', option='status-right-length', scope=<OptionScope.Session: 'SESSION'>, test_value=30, expected_type=<class 'int'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#s_test_options_py_test_option_set_show_cycle_session_status_right_length_14dba990ab | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[session_status_right_style] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_status_right_style', option='status-right-style', scope=<OptionScope.Session: 'SESSION'>, test_value='fg=yellow', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#ts_test_options_py_test_option_set_show_cycle_session_status_right_style_b2f65c65b6 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[session_status_style] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_status_style', option='status-style', scope=<OptionScope.Session: 'SESSION'>, test_value='fg=green', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_status_style_d25802d7b3 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[session_visual_activity] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_visual_activity', option='visual-activity', scope=<OptionScope.Session: 'SESSION'>, test_value='both', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_visual_activity_8f9110a162 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[session_visual_bell] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_visual_bell', option='visual-bell', scope=<OptionScope.Session: 'SESSION'>, test_value='both', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_visual_bell_93d992471b | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[session_visual_silence] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='session_visual_silence', option='visual-silence', scope=<OptionScope.Session: 'SESSION'>, test_value='both', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_session_visual_silence_2b3bab3d1b | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[window_aggressive_resize] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_aggressive_resize', option='aggressive-resize', scope=<OptionScope.Window: 'WINDOW'>, test_value='on', expected_type=<class 'bool'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_options_py_test_option_set_show_cycle_window_aggressive_resize_34a4e0a35f | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[window_automatic_rename] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_automatic_rename', option='automatic-rename', scope=<OptionScope.Window: 'WINDOW'>, test_value='off', expected_type=<class 'bool'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_window_automatic_rename_9cdbdcfde2 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[window_clock_mode_style] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_clock_mode_style', option='clock-mode-style', scope=<OptionScope.Window: 'WINDOW'>, test_value='24', expected_type=<class 'int'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_window_clock_mode_style_7f71d3b7b2 | 3.3a through 3.7c | planned parity | | tests/test_options.py::test_option_set_show_cycle[window_copy_mode_current_line_number_style] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_copy_mode_current_line_number_style', option='copy-mode-current-line-number-style', scope=<OptionScope.Window: 'WINDOW'>, test_value='fg=cyan', expected_type=<class 'str'>, min_version='3.7', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#py_test_option_set_show_cycle_window_copy_mode_current_line_number_style_e873b68304 | 3.7 through 3.7b | planned parity | | tests/test_options.py::test_option_set_show_cycle[window_copy_mode_line_number_style] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_copy_mode_line_number_style', option='copy-mode-line-number-style', scope=<OptionScope.Window: 'WINDOW'>, test_value='fg=cyan', expected_type=<class 'str'>, min_version='3.7', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#options_py_test_option_set_show_cycle_window_copy_mode_line_number_style_ffc6b1354c | 3.7 through 3.7b | planned parity | | tests/test_options.py::test_option_set_show_cycle[window_copy_mode_line_numbers] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_copy_mode_line_numbers', option='copy-mode-line-numbers', scope=<OptionScope.Window: 'WINDOW'>, test_value='absolute', expected_type=<class 'str'>, min_version='3.7', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#test_options_py_test_option_set_show_cycle_window_copy_mode_line_numbers_121d412219 | 3.7 through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[window_mode_keys] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_mode_keys', option='mode-keys', scope=<OptionScope.Window: 'WINDOW'>, test_value='vi', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_window_mode_keys_8ab71db3dc | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[window_mode_style] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_mode_style', option='mode-style', scope=<OptionScope.Window: 'WINDOW'>, test_value='fg=white', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_window_mode_style_f5dce16645 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[window_monitor_activity] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_monitor_activity', option='monitor-activity', scope=<OptionScope.Window: 'WINDOW'>, test_value='on', expected_type=<class 'bool'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_window_monitor_activity_61be93bd2a | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[window_monitor_bell] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_monitor_bell', option='monitor-bell', scope=<OptionScope.Window: 'WINDOW'>, test_value='on', expected_type=<class 'bool'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_window_monitor_bell_7a8ffb195a | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[window_monitor_silence] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_monitor_silence', option='monitor-silence', scope=<OptionScope.Window: 'WINDOW'>, test_value=10, expected_type=<class 'int'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_window_monitor_silence_c959269f1e | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[window_pane_active_border_style] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_pane_active_border_style', option='pane-active-border-style', scope=<OptionScope.Window: 'WINDOW'>, test_value='fg=red', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#st_options_py_test_option_set_show_cycle_window_pane_active_border_style_081cc7791f | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[window_pane_base_index] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_pane_base_index', option='pane-base-index', scope=<OptionScope.Window: 'WINDOW'>, test_value=1, expected_type=<class 'int'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_window_pane_base_index_43b173517e | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[window_pane_border_format] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_pane_border_format', option='pane-border-format', scope=<OptionScope.Window: 'WINDOW'>, test_value='#{pane_index}', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#sts_test_options_py_test_option_set_show_cycle_window_pane_border_format_e577a5cdd1 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[window_pane_border_status] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_pane_border_status', option='pane-border-status', scope=<OptionScope.Window: 'WINDOW'>, test_value='top', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#sts_test_options_py_test_option_set_show_cycle_window_pane_border_status_6de6e0f39d | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[window_pane_border_style] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_pane_border_style', option='pane-border-style', scope=<OptionScope.Window: 'WINDOW'>, test_value='fg=green', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_options_py_test_option_set_show_cycle_window_pane_border_style_545969255a | 3.2a through 3.7b | planned parity | +| tests/test_options.py::test_option_set_show_cycle[window_mode_keys] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_mode_keys', option='mode-keys', scope=<OptionScope.Window: 'WINDOW'>, test_value='vi', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_window_mode_keys_8ab71db3dc | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[window_mode_style] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_mode_style', option='mode-style', scope=<OptionScope.Window: 'WINDOW'>, test_value='fg=white', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_window_mode_style_f5dce16645 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[window_monitor_activity] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_monitor_activity', option='monitor-activity', scope=<OptionScope.Window: 'WINDOW'>, test_value='on', expected_type=<class 'bool'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_window_monitor_activity_61be93bd2a | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[window_monitor_bell] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_monitor_bell', option='monitor-bell', scope=<OptionScope.Window: 'WINDOW'>, test_value='on', expected_type=<class 'bool'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_window_monitor_bell_7a8ffb195a | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[window_monitor_silence] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_monitor_silence', option='monitor-silence', scope=<OptionScope.Window: 'WINDOW'>, test_value=10, expected_type=<class 'int'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_window_monitor_silence_c959269f1e | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[window_pane_active_border_style] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_pane_active_border_style', option='pane-active-border-style', scope=<OptionScope.Window: 'WINDOW'>, test_value='fg=red', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#st_options_py_test_option_set_show_cycle_window_pane_active_border_style_081cc7791f | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[window_pane_base_index] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_pane_base_index', option='pane-base-index', scope=<OptionScope.Window: 'WINDOW'>, test_value=1, expected_type=<class 'int'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_window_pane_base_index_43b173517e | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[window_pane_border_format] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_pane_border_format', option='pane-border-format', scope=<OptionScope.Window: 'WINDOW'>, test_value='#{pane_index}', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#sts_test_options_py_test_option_set_show_cycle_window_pane_border_format_e577a5cdd1 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[window_pane_border_status] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_pane_border_status', option='pane-border-status', scope=<OptionScope.Window: 'WINDOW'>, test_value='top', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#sts_test_options_py_test_option_set_show_cycle_window_pane_border_status_6de6e0f39d | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[window_pane_border_style] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_pane_border_style', option='pane-border-style', scope=<OptionScope.Window: 'WINDOW'>, test_value='fg=green', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_options_py_test_option_set_show_cycle_window_pane_border_style_545969255a | 3.3a through 3.7c | planned parity | | tests/test_options.py::test_option_set_show_cycle[window_tree_mode_preview_format] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_tree_mode_preview_format', option='tree-mode-preview-format', scope=<OptionScope.Window: 'WINDOW'>, test_value='#{window_name}', expected_type=<class 'str'>, min_version='3.7', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#st_options_py_test_option_set_show_cycle_window_tree_mode_preview_format_b5a4de9145 | 3.7 through 3.7b | planned parity | | tests/test_options.py::test_option_set_show_cycle[window_tree_mode_preview_style] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_tree_mode_preview_style', option='tree-mode-preview-style', scope=<OptionScope.Window: 'WINDOW'>, test_value='fg=red', expected_type=<class 'str'>, min_version='3.7', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#est_options_py_test_option_set_show_cycle_window_tree_mode_preview_style_dcbea25680 | 3.7 through 3.7b | planned parity | | tests/test_options.py::test_option_set_show_cycle[window_window_pane_current_status_format] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_window_pane_current_status_format', option='window-pane-current-status-format', scope=<OptionScope.Window: 'WINDOW'>, test_value='#{pane_index}', expected_type=<class 'str'>, min_version='3.7', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#s_py_test_option_set_show_cycle_window_window_pane_current_status_format_be9fe0c7a6 | 3.7 through 3.7b | planned parity | | tests/test_options.py::test_option_set_show_cycle[window_window_pane_status_format] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_window_pane_status_format', option='window-pane-status-format', scope=<OptionScope.Window: 'WINDOW'>, test_value='#{pane_index}', expected_type=<class 'str'>, min_version='3.7', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#t_options_py_test_option_set_show_cycle_window_window_pane_status_format_162f7613b7 | 3.7 through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[window_window_size] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_window_size', option='window-size', scope=<OptionScope.Window: 'WINDOW'>, test_value='latest', expected_type=<class 'str'>, min_version='3.1', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_window_window_size_0372372133 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[window_window_status_current_style] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_window_status_current_style', option='window-status-current-style', scope=<OptionScope.Window: 'WINDOW'>, test_value='fg=magenta', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#options_py_test_option_set_show_cycle_window_window_status_current_style_b56a680eb1 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[window_window_status_style] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_window_status_style', option='window-status-style', scope=<OptionScope.Window: 'WINDOW'>, test_value='fg=cyan', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#ts_test_options_py_test_option_set_show_cycle_window_window_status_style_dddbcc656b | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_option_set_show_cycle[window_wrap_search] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_wrap_search', option='wrap-search', scope=<OptionScope.Window: 'WINDOW'>, test_value='on', expected_type=<class 'bool'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_window_wrap_search_1349e42c1d | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_options | Test basic options; fixtures `server`; execute `server.new_session(session_name='test')`; `session.new_window(window_name='test')`; `window.split(attach=False)`; `obj._show_options()`; `obj._show_options(global_=True)`; `obj._show_options(include_inherited=True)`; expect `pytest.raises(exc.OptionError)`; when `has_gte_version('3.0')`: execute `obj._show_option('test', ignore_errors=True)`; when `isinstance(obj, Pane) and has_gte_version('3.0')`: execute `obj.set_option('test', 'invalid', ignore_errors=True)`; when `isinstance(obj, Pane) and not (has_gte_version('3.0'))`: execute `obj.set_option('test', 'invalid', ignore_errors=True)`; expect `pytest.raises(exc.OptionError)`; when `not (isinstance(obj, Pane))`: execute `obj.set_option('test', 'invalid', ignore_errors=True)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_options_60e34563cc | 3.2a through 3.7b; conditional boundaries: 3.0 | planned parity | -| tests/test_options.py::test_options_grid | Test options against grid; fixtures `server`; execute `server.new_session(session_name='test')`; `session.new_window(window_name='test')`; `window.split(attach=False)`; `obj._show_options(scope=scope, include_inherited=include_inherited, global_=global_)`; `Options(**objglobal__options_)`; `dataclasses.fields(objglobal__options)`; assert `getattr(objglobal__options, field.name, default_value) == expected; message=f'Expect {field.name} to be {expected} when ' + f'scope={scope}, global_={global_}'`; when `include_inherited and expected is None`: execute `field.name.replace('_', '-')`; when `field.default_factory is not dataclasses.MISSING`: execute `field.default_factory()`; when `has_gte_version('3.0') and scope == OptionScope.Window and global_`: assert `objglobal__options.pane_base_index == 0`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_options_grid_56d3bbfac3 | 3.2a through 3.7b; conditional boundaries: 3.0 | planned parity | -| tests/test_options.py::test_options_pane | Test pane options; fixtures `server`; execute `server.new_session(session_name='test')`; `session.new_window(window_name='test')`; `window.split(attach=False)`; `pane._show_options(scope=OptionScope.Pane)`; `PaneOptions(**pane_options_)`; assert `pane_options.window_active_style == pane_options_.get('window-active-style')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_options_pane_9775209292 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_options_server | Test server options; fixtures `server`; execute `server.new_session(session_name='test')`; `session.new_window(window_name='test')`; `window.split(attach=False)`; `server.set_option('buffer-limit', 100)`; `server._show_option('buffer-limit')`; `server.set_option('buffer-limit', 150, scope=OptionScope.Server)`; assert `server._show_option('buffer-limit') == 100`; when `has_gte_version('3.0')`: execute `server.set_option('buffer-limit', 150, scope=OptionScope.Pane)`; when `has_gte_version('3.0')`: execute `server.set_option('buffer-limit', 150, scope=OptionScope.Pane, ignore_errors=True)`; when `has_gte_version('3.0')`: execute `session._show_option('buffer-limit')`; assert `session._show_option('buffer-limit') == 150`; when `has_gte_version('3.0')`: execute `pane.set_option('buffer-limit', 100)`; when `not (has_gte_version('3.0'))`: execute `pane.set_option('buffer-limit', 100)`; expect `pytest.raises(exc.OptionError)`; when `has_gte_version('3.0')`: execute `pane._show_option('buffer-limit')`; `window._show_option('buffer-limit')`; `server._show_option('buffer-limit')`; assert `pane._show_option('buffer-limit') == 100`; `window._show_option('buffer-limit') == 100`; `server._show_option('buffer-limit') == 100`; when `has_gte_version('3.0')`: execute `server._show_option('buffer-limit')`; `server.set_option('buffer-limit', 150, scope=OptionScope.Server)`; `server.unset_option('buffer-limit')`; assert `server._show_option('buffer-limit') == 100`; `server_options.buffer_limit == 100`; `server._show_option('buffer-limit') == 150`; `server._show_option('buffer-limit') == 50`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_options_server_a45f3cd180 | 3.2a through 3.7b; conditional boundaries: 3.0 | planned parity | -| tests/test_options.py::test_options_session | Test session options; fixtures `server`; execute `server.new_session(session_name='test')`; `session.new_window(window_name='test')`; `session._show_options(scope=OptionScope.Session)`; `SessionOptions(**session_options_)`; assert `session_options.default_size == session_options_.get('default-size')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_options_session_8228c4200a | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_options_window | Test window options; fixtures `server`; execute `server.new_session(session_name='test')`; `session.new_window(window_name='test')`; `window.split(attach=False)`; `window._show_options(scope=OptionScope.Window)`; `WindowOptions(**window_options_)`; assert `window_options.automatic_rename == window_options_.get('automatic-rename')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_options_window_c2401e6cd2 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_pane_options_declares_tmux_3_7_shared_fields[pane_active_border_style] | Tmux 3.7 options shared with window scope must type on panes too; case inputs `field_name='pane_active_border_style'`; assert `field_name in PaneOptions.__dataclass_fields__`; `field_name in Options.__dataclass_fields__`. | real-tmux integration test | PythonBehaviorParityTest#st_pane_options_declares_tmux_3_7_shared_fields_pane_active_border_style_a990236eb7 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_pane_options_declares_tmux_3_7_shared_fields[pane_border_style] | Tmux 3.7 options shared with window scope must type on panes too; case inputs `field_name='pane_border_style'`; assert `field_name in PaneOptions.__dataclass_fields__`; `field_name in Options.__dataclass_fields__`. | real-tmux integration test | PythonBehaviorParityTest#s_py_test_pane_options_declares_tmux_3_7_shared_fields_pane_border_style_7a6b256c0c | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_pane_options_declares_tmux_3_7_shared_fields[tree_mode_preview_format] | Tmux 3.7 options shared with window scope must type on panes too; case inputs `field_name='tree_mode_preview_format'`; assert `field_name in PaneOptions.__dataclass_fields__`; `field_name in Options.__dataclass_fields__`. | real-tmux integration test | PythonBehaviorParityTest#st_pane_options_declares_tmux_3_7_shared_fields_tree_mode_preview_format_2da75694e9 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_set_environment_expand_format | Test set_environment with expand_format flag; fixtures `session`; execute `session.set_environment('FORMAT_TEST_VAR', '#{session_name}', expand_format=True)`; `session.show_environment()`; assert `'FORMAT_TEST_VAR' in env`; `env['FORMAT_TEST_VAR'] != '#{session_name}'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_set_environment_expand_format_f4e7cbe5b8 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_set_environment_hidden | Test set_environment with hidden flag; fixtures `session`; execute `session.set_environment('HIDDEN_TEST_VAR', 'secret', hidden=True)`; `session.show_environment()`; assert `'HIDDEN_TEST_VAR' not in env`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_set_environment_hidden_d679ff730d | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_show_option_g_parameter_emits_deprecation_warning | Test show_option() emits DeprecationWarning when g parameter is used; fixtures `session`; execute `server.show_option('buffer-limit', g=True)`; expect `pytest.warns(DeprecationWarning, match='g argument is deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#s_test_options_py_test_show_option_g_parameter_emits_deprecation_warning_487469f9b6 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_show_option_indexed_array[base_name_returns_sparse_array] | Test show_option handles bracketed array indices correctly; case inputs `test_case=ShowOptionIndexedTestCase(test_id='base_name_returns_sparse_array', option='status-format', expect_sparse_array=True)`; fixtures `session`; execute `session.show_option(test_case.option, global_=True)`; when `test_case.expect_sparse_array`: assert `isinstance(result, SparseArray); message=f"Expected SparseArray for '{test_case.option}', got {type(result).__name__}"`; when `not (test_case.expect_sparse_array)`: assert `result is not None; message=f"Expected a value for '{test_case.option}', got None"`; `not isinstance(result, SparseArray); message=f"Expected single value for '{test_case.option}', got SparseArray"`. | real-tmux integration test | PythonBehaviorParityTest#options_py_test_show_option_indexed_array_base_name_returns_sparse_array_5b7214defc | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_show_option_indexed_array[indexed_returns_value] | Test show_option handles bracketed array indices correctly; case inputs `test_case=ShowOptionIndexedTestCase(test_id='indexed_returns_value', option='status-format[0]', expect_sparse_array=False)`; fixtures `session`; execute `session.show_option(test_case.option, global_=True)`; when `test_case.expect_sparse_array`: assert `isinstance(result, SparseArray); message=f"Expected SparseArray for '{test_case.option}', got {type(result).__name__}"`; when `not (test_case.expect_sparse_array)`: assert `result is not None; message=f"Expected a value for '{test_case.option}', got None"`; `not isinstance(result, SparseArray); message=f"Expected single value for '{test_case.option}', got SparseArray"`. | real-tmux integration test | PythonBehaviorParityTest#sts_test_options_py_test_show_option_indexed_array_indexed_returns_value_f76c7e2a78 | 3.2a through 3.7b | planned parity | +| tests/test_options.py::test_option_set_show_cycle[window_window_size] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_window_size', option='window-size', scope=<OptionScope.Window: 'WINDOW'>, test_value='latest', expected_type=<class 'str'>, min_version='3.1', xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_window_window_size_0372372133 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[window_window_status_current_style] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_window_status_current_style', option='window-status-current-style', scope=<OptionScope.Window: 'WINDOW'>, test_value='fg=magenta', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#options_py_test_option_set_show_cycle_window_window_status_current_style_b56a680eb1 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[window_window_status_style] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_window_status_style', option='window-status-style', scope=<OptionScope.Window: 'WINDOW'>, test_value='fg=cyan', expected_type=<class 'str'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#ts_test_options_py_test_option_set_show_cycle_window_window_status_style_dddbcc656b | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_option_set_show_cycle[window_wrap_search] | Test set/show cycle for each option type; case inputs `test_case=OptionTestCase(test_id='window_wrap_search', option='wrap-search', scope=<OptionScope.Window: 'WINDOW'>, test_value='on', expected_type=<class 'bool'>, min_version=None, xfail_reason=None)`; fixtures `server`; execute `server.new_session(session_name='test_option_cycle')`; `target.set_option(test_case.option, test_case.test_value)`; `target.show_option(test_case.option)`; assert `window is not None`; `pane is not None`; `result is not None; message=f'Expected {test_case.option} to be set, got None'`; `isinstance(result, test_case.expected_type); message=f'Expected {test_case.expected_type.__name__}, got {type(result).__name__}'`; when `not has_gte_version('3.0')`: skip via `pytest.skip('Option tests require tmux 3.0+')`; when `test_case.min_version and (not has_gte_version(test_case.min_version))`: skip via `pytest.skip(f'Requires tmux {test_case.min_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_option_set_show_cycle_window_wrap_search_1349e42c1d | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_options | Test basic options; fixtures `server`; execute `server.new_session(session_name='test')`; `session.new_window(window_name='test')`; `window.split(attach=False)`; `obj._show_options()`; `obj._show_options(global_=True)`; `obj._show_options(include_inherited=True)`; expect `pytest.raises(exc.OptionError)`; when `has_gte_version('3.0')`: execute `obj._show_option('test', ignore_errors=True)`; when `isinstance(obj, Pane) and has_gte_version('3.0')`: execute `obj.set_option('test', 'invalid', ignore_errors=True)`; when `isinstance(obj, Pane) and not (has_gte_version('3.0'))`: execute `obj.set_option('test', 'invalid', ignore_errors=True)`; expect `pytest.raises(exc.OptionError)`; when `not (isinstance(obj, Pane))`: execute `obj.set_option('test', 'invalid', ignore_errors=True)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_options_60e34563cc | 3.3a through 3.7c; conditional boundaries: 3.0 | planned parity | +| tests/test_options.py::test_options_grid | Test options against grid; fixtures `server`; execute `server.new_session(session_name='test')`; `session.new_window(window_name='test')`; `window.split(attach=False)`; `obj._show_options(scope=scope, include_inherited=include_inherited, global_=global_)`; `Options(**objglobal__options_)`; `dataclasses.fields(objglobal__options)`; assert `getattr(objglobal__options, field.name, default_value) == expected; message=f'Expect {field.name} to be {expected} when ' + f'scope={scope}, global_={global_}'`; when `include_inherited and expected is None`: execute `field.name.replace('_', '-')`; when `field.default_factory is not dataclasses.MISSING`: execute `field.default_factory()`; when `has_gte_version('3.0') and scope == OptionScope.Window and global_`: assert `objglobal__options.pane_base_index == 0`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_options_grid_56d3bbfac3 | 3.3a through 3.7c; conditional boundaries: 3.0 | planned parity | +| tests/test_options.py::test_options_pane | Test pane options; fixtures `server`; execute `server.new_session(session_name='test')`; `session.new_window(window_name='test')`; `window.split(attach=False)`; `pane._show_options(scope=OptionScope.Pane)`; `PaneOptions(**pane_options_)`; assert `pane_options.window_active_style == pane_options_.get('window-active-style')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_options_pane_9775209292 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_options_server | Test server options; fixtures `server`; execute `server.new_session(session_name='test')`; `session.new_window(window_name='test')`; `window.split(attach=False)`; `server.set_option('buffer-limit', 100)`; `server._show_option('buffer-limit')`; `server.set_option('buffer-limit', 150, scope=OptionScope.Server)`; assert `server._show_option('buffer-limit') == 100`; when `has_gte_version('3.0')`: execute `server.set_option('buffer-limit', 150, scope=OptionScope.Pane)`; when `has_gte_version('3.0')`: execute `server.set_option('buffer-limit', 150, scope=OptionScope.Pane, ignore_errors=True)`; when `has_gte_version('3.0')`: execute `session._show_option('buffer-limit')`; assert `session._show_option('buffer-limit') == 150`; when `has_gte_version('3.0')`: execute `pane.set_option('buffer-limit', 100)`; when `not (has_gte_version('3.0'))`: execute `pane.set_option('buffer-limit', 100)`; expect `pytest.raises(exc.OptionError)`; when `has_gte_version('3.0')`: execute `pane._show_option('buffer-limit')`; `window._show_option('buffer-limit')`; `server._show_option('buffer-limit')`; assert `pane._show_option('buffer-limit') == 100`; `window._show_option('buffer-limit') == 100`; `server._show_option('buffer-limit') == 100`; when `has_gte_version('3.0')`: execute `server._show_option('buffer-limit')`; `server.set_option('buffer-limit', 150, scope=OptionScope.Server)`; `server.unset_option('buffer-limit')`; assert `server._show_option('buffer-limit') == 100`; `server_options.buffer_limit == 100`; `server._show_option('buffer-limit') == 150`; `server._show_option('buffer-limit') == 50`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_options_server_a45f3cd180 | 3.3a through 3.7c; conditional boundaries: 3.0 | planned parity | +| tests/test_options.py::test_options_session | Test session options; fixtures `server`; execute `server.new_session(session_name='test')`; `session.new_window(window_name='test')`; `session._show_options(scope=OptionScope.Session)`; `SessionOptions(**session_options_)`; assert `session_options.default_size == session_options_.get('default-size')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_options_session_8228c4200a | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_options_window | Test window options; fixtures `server`; execute `server.new_session(session_name='test')`; `session.new_window(window_name='test')`; `window.split(attach=False)`; `window._show_options(scope=OptionScope.Window)`; `WindowOptions(**window_options_)`; assert `window_options.automatic_rename == window_options_.get('automatic-rename')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_options_window_c2401e6cd2 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_pane_options_declares_tmux_3_7_shared_fields[pane_active_border_style] | Tmux 3.7 options shared with window scope must type on panes too; case inputs `field_name='pane_active_border_style'`; assert `field_name in PaneOptions.__dataclass_fields__`; `field_name in Options.__dataclass_fields__`. | real-tmux integration test | PythonBehaviorParityTest#st_pane_options_declares_tmux_3_7_shared_fields_pane_active_border_style_a990236eb7 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_pane_options_declares_tmux_3_7_shared_fields[pane_border_style] | Tmux 3.7 options shared with window scope must type on panes too; case inputs `field_name='pane_border_style'`; assert `field_name in PaneOptions.__dataclass_fields__`; `field_name in Options.__dataclass_fields__`. | real-tmux integration test | PythonBehaviorParityTest#s_py_test_pane_options_declares_tmux_3_7_shared_fields_pane_border_style_7a6b256c0c | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_pane_options_declares_tmux_3_7_shared_fields[tree_mode_preview_format] | Tmux 3.7 options shared with window scope must type on panes too; case inputs `field_name='tree_mode_preview_format'`; assert `field_name in PaneOptions.__dataclass_fields__`; `field_name in Options.__dataclass_fields__`. | real-tmux integration test | PythonBehaviorParityTest#st_pane_options_declares_tmux_3_7_shared_fields_tree_mode_preview_format_2da75694e9 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_set_environment_expand_format | Test set_environment with expand_format flag; fixtures `session`; execute `session.set_environment('FORMAT_TEST_VAR', '#{session_name}', expand_format=True)`; `session.show_environment()`; assert `'FORMAT_TEST_VAR' in env`; `env['FORMAT_TEST_VAR'] != '#{session_name}'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_set_environment_expand_format_f4e7cbe5b8 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_set_environment_hidden | Test set_environment with hidden flag; fixtures `session`; execute `session.set_environment('HIDDEN_TEST_VAR', 'secret', hidden=True)`; `session.show_environment()`; assert `'HIDDEN_TEST_VAR' not in env`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_set_environment_hidden_d679ff730d | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_show_option_g_parameter_emits_deprecation_warning | Test show_option() emits DeprecationWarning when g parameter is used; fixtures `session`; execute `server.show_option('buffer-limit', g=True)`; expect `pytest.warns(DeprecationWarning, match='g argument is deprecated')`. | real-tmux integration test | PythonBehaviorParityTest#s_test_options_py_test_show_option_g_parameter_emits_deprecation_warning_487469f9b6 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_show_option_indexed_array[base_name_returns_sparse_array] | Test show_option handles bracketed array indices correctly; case inputs `test_case=ShowOptionIndexedTestCase(test_id='base_name_returns_sparse_array', option='status-format', expect_sparse_array=True)`; fixtures `session`; execute `session.show_option(test_case.option, global_=True)`; when `test_case.expect_sparse_array`: assert `isinstance(result, SparseArray); message=f"Expected SparseArray for '{test_case.option}', got {type(result).__name__}"`; when `not (test_case.expect_sparse_array)`: assert `result is not None; message=f"Expected a value for '{test_case.option}', got None"`; `not isinstance(result, SparseArray); message=f"Expected single value for '{test_case.option}', got SparseArray"`. | real-tmux integration test | PythonBehaviorParityTest#options_py_test_show_option_indexed_array_base_name_returns_sparse_array_5b7214defc | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_show_option_indexed_array[indexed_returns_value] | Test show_option handles bracketed array indices correctly; case inputs `test_case=ShowOptionIndexedTestCase(test_id='indexed_returns_value', option='status-format[0]', expect_sparse_array=False)`; fixtures `session`; execute `session.show_option(test_case.option, global_=True)`; when `test_case.expect_sparse_array`: assert `isinstance(result, SparseArray); message=f"Expected SparseArray for '{test_case.option}', got {type(result).__name__}"`; when `not (test_case.expect_sparse_array)`: assert `result is not None; message=f"Expected a value for '{test_case.option}', got None"`; `not isinstance(result, SparseArray); message=f"Expected single value for '{test_case.option}', got SparseArray"`. | real-tmux integration test | PythonBehaviorParityTest#sts_test_options_py_test_show_option_indexed_array_indexed_returns_value_f76c7e2a78 | 3.3a through 3.7c | planned parity | | tests/test_options.py::test_show_option_pane_fixture[command-alias] | Test Pane.show_option(s)?; case inputs `dataclass_attribute='command_alias'`, `expected={'split-pane': 'split-window', 'splitp': 'split-window', 'server-info': 'show-messages -JT', 'info': 'show-messages -JT', 'choose-window': 'choose-tree -w', 'choose-session': 'choose-tree -s'}`, `mocked_cmd_stdout=['command-alias[0] split-pane=split-window', 'command-alias[1] splitp=split-window', 'command-alias[2] "server-info=show-messages -JT"', 'command-alias[3] "info=show-messages -JT"', 'command-alias[4] "choose-window=choose-tree -w"', 'command-alias[5] "choose-session=choose-tree -s"']`, `test_id='command-alias'`, `tmux_option='command-alias'`; fixtures `monkeypatch`, `server`; execute `server.new_session(session_name='test')`; `session.new_window(window_name='test')`; `window.split(attach=False)`; `pane.show_option(tmux_option)`; `fake_cmd(stdout=mocked_cmd_stdout)`; assert `result == expected`. | unit test | PythonBehaviorParityTest#tests_test_options_py_test_show_option_pane_fixture_command_alias_bbe2bb81f5 | not tmux-bound | planned parity | | tests/test_options.py::test_show_option_pane_fixture[terminal-features] | Test Pane.show_option(s)?; case inputs `dataclass_attribute='terminal_features'`, `expected={'screen*': ['title'], 'xterm*': ['clipboard', 'ccolour', 'cstyle', 'focus']}`, `mocked_cmd_stdout=['terminal-features[0] xterm*:clipboard:ccolour:cstyle:focus', 'terminal-features[1] screen*:title']`, `test_id='terminal-features'`, `tmux_option='terminal-features'`; fixtures `monkeypatch`, `server`; execute `server.new_session(session_name='test')`; `session.new_window(window_name='test')`; `window.split(attach=False)`; `pane.show_option(tmux_option)`; `fake_cmd(stdout=mocked_cmd_stdout)`; assert `result == expected`. | unit test | PythonBehaviorParityTest#tests_test_options_py_test_show_option_pane_fixture_terminal_features_72f6b9092e | not tmux-bound | planned parity | -| tests/test_options.py::test_show_options_quiet | Test _show_options_raw with quiet flag suppresses errors; fixtures `server`; execute `server._show_options_raw(scope=OptionScope.Server, quiet=True)`; assert `isinstance(result.stdout, list)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_show_options_quiet_e3cdb42710 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_show_options_quiet_public | Public show_options() forwards quiet= to _show_options_raw(); fixtures `server`; execute `server.show_options(quiet=True)`; assert `isinstance(options, dict)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_show_options_quiet_public_6820ad9146 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_show_options_returns_expected_keys[server_global] | Test that show_options() returns dict with expected scope-specific keys; case inputs `test_case=ShowOptionsTestCase(test_id='server_global', scope=<OptionScope.Server: 'SERVER'>, expected_keys=['buffer-limit', 'escape-time', 'exit-empty', 'focus-events'], global_=True, include_inherited=False, min_version='3.2')`; fixtures `server`; execute `server.new_session(session_name='test_show_options')`; `server.show_options(global_=test_case.global_, scope=test_case.scope, include_inherited=test_case.include_inherited)`; assert `pane is not None`; `isinstance(options, dict)`; `len(options) > 0; message=f'Expected non-empty dict for scope={test_case.scope.name}, global_={test_case.global_}'`; `key in options or f'{key}*' in options; message=f"Expected '{key}' in {test_case.scope.name} options, got keys: {list(options.keys())[:10]}..."`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux >= {test_case.min_version}')`. | real-tmux integration test | PythonBehaviorParityTest#ts_test_options_py_test_show_options_returns_expected_keys_server_global_b69dd85c17 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_show_options_returns_expected_keys[server_global_inherited] | Test that show_options() returns dict with expected scope-specific keys; case inputs `test_case=ShowOptionsTestCase(test_id='server_global_inherited', scope=<OptionScope.Server: 'SERVER'>, expected_keys=['buffer-limit', 'escape-time', 'exit-empty', 'focus-events'], global_=True, include_inherited=True, min_version='3.2')`; fixtures `server`; execute `server.new_session(session_name='test_show_options')`; `server.show_options(global_=test_case.global_, scope=test_case.scope, include_inherited=test_case.include_inherited)`; assert `pane is not None`; `isinstance(options, dict)`; `len(options) > 0; message=f'Expected non-empty dict for scope={test_case.scope.name}, global_={test_case.global_}'`; `key in options or f'{key}*' in options; message=f"Expected '{key}' in {test_case.scope.name} options, got keys: {list(options.keys())[:10]}..."`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux >= {test_case.min_version}')`. | real-tmux integration test | PythonBehaviorParityTest#tions_py_test_show_options_returns_expected_keys_server_global_inherited_a3c8277ec0 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_show_options_returns_expected_keys[session_global] | Test that show_options() returns dict with expected scope-specific keys; case inputs `test_case=ShowOptionsTestCase(test_id='session_global', scope=<OptionScope.Session: 'SESSION'>, expected_keys=['base-index', 'history-limit', 'status'], global_=True, include_inherited=False, min_version='3.2')`; fixtures `server`; execute `server.new_session(session_name='test_show_options')`; `server.show_options(global_=test_case.global_, scope=test_case.scope, include_inherited=test_case.include_inherited)`; assert `pane is not None`; `isinstance(options, dict)`; `len(options) > 0; message=f'Expected non-empty dict for scope={test_case.scope.name}, global_={test_case.global_}'`; `key in options or f'{key}*' in options; message=f"Expected '{key}' in {test_case.scope.name} options, got keys: {list(options.keys())[:10]}..."`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux >= {test_case.min_version}')`. | real-tmux integration test | PythonBehaviorParityTest#s_test_options_py_test_show_options_returns_expected_keys_session_global_9a7596348c | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_show_options_returns_expected_keys[session_global_inherited] | Test that show_options() returns dict with expected scope-specific keys; case inputs `test_case=ShowOptionsTestCase(test_id='session_global_inherited', scope=<OptionScope.Session: 'SESSION'>, expected_keys=['base-index', 'history-limit', 'status'], global_=True, include_inherited=True, min_version='3.2')`; fixtures `server`; execute `server.new_session(session_name='test_show_options')`; `server.show_options(global_=test_case.global_, scope=test_case.scope, include_inherited=test_case.include_inherited)`; assert `pane is not None`; `isinstance(options, dict)`; `len(options) > 0; message=f'Expected non-empty dict for scope={test_case.scope.name}, global_={test_case.global_}'`; `key in options or f'{key}*' in options; message=f"Expected '{key}' in {test_case.scope.name} options, got keys: {list(options.keys())[:10]}..."`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux >= {test_case.min_version}')`. | real-tmux integration test | PythonBehaviorParityTest#ions_py_test_show_options_returns_expected_keys_session_global_inherited_208500143a | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_show_options_returns_expected_keys[window_global] | Test that show_options() returns dict with expected scope-specific keys; case inputs `test_case=ShowOptionsTestCase(test_id='window_global', scope=<OptionScope.Window: 'WINDOW'>, expected_keys=['mode-keys', 'pane-base-index', 'automatic-rename'], global_=True, include_inherited=False, min_version='3.2')`; fixtures `server`; execute `server.new_session(session_name='test_show_options')`; `server.show_options(global_=test_case.global_, scope=test_case.scope, include_inherited=test_case.include_inherited)`; assert `pane is not None`; `isinstance(options, dict)`; `len(options) > 0; message=f'Expected non-empty dict for scope={test_case.scope.name}, global_={test_case.global_}'`; `key in options or f'{key}*' in options; message=f"Expected '{key}' in {test_case.scope.name} options, got keys: {list(options.keys())[:10]}..."`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux >= {test_case.min_version}')`. | real-tmux integration test | PythonBehaviorParityTest#ts_test_options_py_test_show_options_returns_expected_keys_window_global_269e877f5b | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_show_options_returns_expected_keys[window_global_inherited] | Test that show_options() returns dict with expected scope-specific keys; case inputs `test_case=ShowOptionsTestCase(test_id='window_global_inherited', scope=<OptionScope.Window: 'WINDOW'>, expected_keys=['mode-keys', 'pane-base-index', 'automatic-rename'], global_=True, include_inherited=True, min_version='3.2')`; fixtures `server`; execute `server.new_session(session_name='test_show_options')`; `server.show_options(global_=test_case.global_, scope=test_case.scope, include_inherited=test_case.include_inherited)`; assert `pane is not None`; `isinstance(options, dict)`; `len(options) > 0; message=f'Expected non-empty dict for scope={test_case.scope.name}, global_={test_case.global_}'`; `key in options or f'{key}*' in options; message=f"Expected '{key}' in {test_case.scope.name} options, got keys: {list(options.keys())[:10]}..."`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux >= {test_case.min_version}')`. | real-tmux integration test | PythonBehaviorParityTest#tions_py_test_show_options_returns_expected_keys_window_global_inherited_4ffb5247d1 | 3.2a through 3.7b | planned parity | -| tests/test_options.py::test_stable_baseline_options_and_hooks | Ensure stable baseline across tmux versions; fixtures `server`; execute `server.new_session(session_name='test', detach=True)`; `server.show_option('command-alias')`; `server.show_option('user-keys')`; assert `server.show_option('command-alias') == {'choose-session': 'choose-tree -s', 'choose-window': 'choose-tree -w', 'info': 'show-messages -JT', 'server-info': 'show-messages -JT', 'split-pane': 'split-window', 'splitp': 'split-window'}`; `server.show_option('user-keys') is None`; `pane is not None`; when `has_gte_version('3.2')`: execute `server.show_option('terminal-features')`; assert `isinstance(terminal_features, dict)`; `'screen*' in terminal_features`; `terminal_features['screen*'] == ['title']`; `'xterm*' in terminal_features`; `terminal_features['xterm*'] == ['clipboard', 'ccolour', 'cstyle', 'focus', 'title']`; when `has_gte_version('3.2') and has_gte_version('3.4')`: assert `'rxvt*' in terminal_features`; `terminal_features['rxvt*'] == ['ignorefkeys']`; when `has_gte_version('3.2')`: execute `server.show_option('terminal-overrides')`; assert `isinstance(terminal_overrides, (dict, type(None)))`; when `not (has_gte_version('3.2')) and has_gte_version('3.0')`: execute `server.show_option('terminal-overrides')`; assert `isinstance(terminal_overrides, dict)`; `'screen*' in terminal_overrides or 'xterm*' in terminal_overrides`; when `not (has_gte_version('3.2')) and not (has_gte_version('3.0'))`: execute `server.show_option('terminal-overrides')`; assert `isinstance(terminal_overrides, dict)`; `'screen*' in terminal_overrides`; `'xterm*' in terminal_overrides`; when `has_gte_version('2.9')`: execute `server.show_option('status-format')`; assert `isinstance(status_format, (dict, type(None)))`; when `not (has_gte_version('2.9'))`: execute `server.show_option('status-format')`; expect `pytest.raises(exc.OptionError)`; when `has_gte_version('3.0')`: execute `server.show_option('update-environment')`; assert `isinstance(update_env, (list, type(None)))`; when `not (has_gte_version('3.0'))`: execute `server.show_option('update-environment')`; expect `pytest.raises(exc.OptionError)`; when `has_gte_version('3.3')`: execute `pane.show_option('pane-colours')`; assert `pane.show_option('pane-colours') is None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_stable_baseline_options_and_hooks_4f936586b7 | 3.2a through 3.7b; conditional boundaries: 3.2, 3.4, 3.0, 2.9, 3.3 | planned parity | -| tests/test_options.py::test_style_option_validation | Test style option validation; fixtures `server`; execute `server.new_session(session_name='test')`; `session.set_option('status-style', 'fg=red,bg=default,bold')`; `session.show_option('status-style')`; `session.set_option('status-style', 'invalid-style')`; expect `pytest.raises(exc.OptionError)`; assert `isinstance(style, str)`; `'fg=red' in str(style)`; when `has_gte_version('3.2')`: assert `'bg=default' in str(style)`; `'bold' in str(style)`; when `not (has_gte_version('3.2'))`: assert `'bright' in str(style)`; `'bg=default' not in str(style)`; when `has_gte_version('3.0')`: execute `session.set_option('status-style', 'fg=colour240,bg=#525252,bold,underscore')`; `session.show_option('status-style')`; assert `isinstance(style, str)`; when `has_gte_version('3.0') and has_gte_version('3.2')`: assert `style == 'fg=colour240,bg=#525252,bold,underscore'`; when `has_gte_version('3.0') and not (has_gte_version('3.2'))`: assert `style == 'fg=colour240,bg=#525252,bright,underscore'`; when `has_gte_version('3.0') and has_gte_version('3.2')`: execute `session.set_option('status-style', 'fg=#{?pane_in_mode,red,green}')`; `session.show_option('status-style')`; assert `isinstance(style, str)`; `style == 'fg=#{?pane_in_mode,red,green}'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_style_option_validation_029080827c | 3.2a through 3.7b; conditional boundaries: 3.2, 3.0 | planned parity | +| tests/test_options.py::test_show_options_quiet | Test _show_options_raw with quiet flag suppresses errors; fixtures `server`; execute `server._show_options_raw(scope=OptionScope.Server, quiet=True)`; assert `isinstance(result.stdout, list)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_show_options_quiet_e3cdb42710 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_show_options_quiet_public | Public show_options() forwards quiet= to _show_options_raw(); fixtures `server`; execute `server.show_options(quiet=True)`; assert `isinstance(options, dict)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_show_options_quiet_public_6820ad9146 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_show_options_returns_expected_keys[server_global] | Test that show_options() returns dict with expected scope-specific keys; case inputs `test_case=ShowOptionsTestCase(test_id='server_global', scope=<OptionScope.Server: 'SERVER'>, expected_keys=['buffer-limit', 'escape-time', 'exit-empty', 'focus-events'], global_=True, include_inherited=False, min_version='3.2')`; fixtures `server`; execute `server.new_session(session_name='test_show_options')`; `server.show_options(global_=test_case.global_, scope=test_case.scope, include_inherited=test_case.include_inherited)`; assert `pane is not None`; `isinstance(options, dict)`; `len(options) > 0; message=f'Expected non-empty dict for scope={test_case.scope.name}, global_={test_case.global_}'`; `key in options or f'{key}*' in options; message=f"Expected '{key}' in {test_case.scope.name} options, got keys: {list(options.keys())[:10]}..."`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux >= {test_case.min_version}')`. | real-tmux integration test | PythonBehaviorParityTest#ts_test_options_py_test_show_options_returns_expected_keys_server_global_b69dd85c17 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_show_options_returns_expected_keys[server_global_inherited] | Test that show_options() returns dict with expected scope-specific keys; case inputs `test_case=ShowOptionsTestCase(test_id='server_global_inherited', scope=<OptionScope.Server: 'SERVER'>, expected_keys=['buffer-limit', 'escape-time', 'exit-empty', 'focus-events'], global_=True, include_inherited=True, min_version='3.2')`; fixtures `server`; execute `server.new_session(session_name='test_show_options')`; `server.show_options(global_=test_case.global_, scope=test_case.scope, include_inherited=test_case.include_inherited)`; assert `pane is not None`; `isinstance(options, dict)`; `len(options) > 0; message=f'Expected non-empty dict for scope={test_case.scope.name}, global_={test_case.global_}'`; `key in options or f'{key}*' in options; message=f"Expected '{key}' in {test_case.scope.name} options, got keys: {list(options.keys())[:10]}..."`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux >= {test_case.min_version}')`. | real-tmux integration test | PythonBehaviorParityTest#tions_py_test_show_options_returns_expected_keys_server_global_inherited_a3c8277ec0 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_show_options_returns_expected_keys[session_global] | Test that show_options() returns dict with expected scope-specific keys; case inputs `test_case=ShowOptionsTestCase(test_id='session_global', scope=<OptionScope.Session: 'SESSION'>, expected_keys=['base-index', 'history-limit', 'status'], global_=True, include_inherited=False, min_version='3.2')`; fixtures `server`; execute `server.new_session(session_name='test_show_options')`; `server.show_options(global_=test_case.global_, scope=test_case.scope, include_inherited=test_case.include_inherited)`; assert `pane is not None`; `isinstance(options, dict)`; `len(options) > 0; message=f'Expected non-empty dict for scope={test_case.scope.name}, global_={test_case.global_}'`; `key in options or f'{key}*' in options; message=f"Expected '{key}' in {test_case.scope.name} options, got keys: {list(options.keys())[:10]}..."`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux >= {test_case.min_version}')`. | real-tmux integration test | PythonBehaviorParityTest#s_test_options_py_test_show_options_returns_expected_keys_session_global_9a7596348c | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_show_options_returns_expected_keys[session_global_inherited] | Test that show_options() returns dict with expected scope-specific keys; case inputs `test_case=ShowOptionsTestCase(test_id='session_global_inherited', scope=<OptionScope.Session: 'SESSION'>, expected_keys=['base-index', 'history-limit', 'status'], global_=True, include_inherited=True, min_version='3.2')`; fixtures `server`; execute `server.new_session(session_name='test_show_options')`; `server.show_options(global_=test_case.global_, scope=test_case.scope, include_inherited=test_case.include_inherited)`; assert `pane is not None`; `isinstance(options, dict)`; `len(options) > 0; message=f'Expected non-empty dict for scope={test_case.scope.name}, global_={test_case.global_}'`; `key in options or f'{key}*' in options; message=f"Expected '{key}' in {test_case.scope.name} options, got keys: {list(options.keys())[:10]}..."`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux >= {test_case.min_version}')`. | real-tmux integration test | PythonBehaviorParityTest#ions_py_test_show_options_returns_expected_keys_session_global_inherited_208500143a | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_show_options_returns_expected_keys[window_global] | Test that show_options() returns dict with expected scope-specific keys; case inputs `test_case=ShowOptionsTestCase(test_id='window_global', scope=<OptionScope.Window: 'WINDOW'>, expected_keys=['mode-keys', 'pane-base-index', 'automatic-rename'], global_=True, include_inherited=False, min_version='3.2')`; fixtures `server`; execute `server.new_session(session_name='test_show_options')`; `server.show_options(global_=test_case.global_, scope=test_case.scope, include_inherited=test_case.include_inherited)`; assert `pane is not None`; `isinstance(options, dict)`; `len(options) > 0; message=f'Expected non-empty dict for scope={test_case.scope.name}, global_={test_case.global_}'`; `key in options or f'{key}*' in options; message=f"Expected '{key}' in {test_case.scope.name} options, got keys: {list(options.keys())[:10]}..."`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux >= {test_case.min_version}')`. | real-tmux integration test | PythonBehaviorParityTest#ts_test_options_py_test_show_options_returns_expected_keys_window_global_269e877f5b | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_show_options_returns_expected_keys[window_global_inherited] | Test that show_options() returns dict with expected scope-specific keys; case inputs `test_case=ShowOptionsTestCase(test_id='window_global_inherited', scope=<OptionScope.Window: 'WINDOW'>, expected_keys=['mode-keys', 'pane-base-index', 'automatic-rename'], global_=True, include_inherited=True, min_version='3.2')`; fixtures `server`; execute `server.new_session(session_name='test_show_options')`; `server.show_options(global_=test_case.global_, scope=test_case.scope, include_inherited=test_case.include_inherited)`; assert `pane is not None`; `isinstance(options, dict)`; `len(options) > 0; message=f'Expected non-empty dict for scope={test_case.scope.name}, global_={test_case.global_}'`; `key in options or f'{key}*' in options; message=f"Expected '{key}' in {test_case.scope.name} options, got keys: {list(options.keys())[:10]}..."`; when `not has_gte_version(test_case.min_version)`: skip via `pytest.skip(f'Requires tmux >= {test_case.min_version}')`. | real-tmux integration test | PythonBehaviorParityTest#tions_py_test_show_options_returns_expected_keys_window_global_inherited_4ffb5247d1 | 3.3a through 3.7c | planned parity | +| tests/test_options.py::test_stable_baseline_options_and_hooks | Ensure stable baseline across tmux versions; fixtures `server`; execute `server.new_session(session_name='test', detach=True)`; `server.show_option('command-alias')`; `server.show_option('user-keys')`; assert `server.show_option('command-alias') == {'choose-session': 'choose-tree -s', 'choose-window': 'choose-tree -w', 'info': 'show-messages -JT', 'server-info': 'show-messages -JT', 'split-pane': 'split-window', 'splitp': 'split-window'}`; `server.show_option('user-keys') is None`; `pane is not None`; when `has_gte_version('3.2')`: execute `server.show_option('terminal-features')`; assert `isinstance(terminal_features, dict)`; `'screen*' in terminal_features`; `terminal_features['screen*'] == ['title']`; `'xterm*' in terminal_features`; `terminal_features['xterm*'] == ['clipboard', 'ccolour', 'cstyle', 'focus', 'title']`; when `has_gte_version('3.2') and has_gte_version('3.4')`: assert `'rxvt*' in terminal_features`; `terminal_features['rxvt*'] == ['ignorefkeys']`; when `has_gte_version('3.2')`: execute `server.show_option('terminal-overrides')`; assert `isinstance(terminal_overrides, (dict, type(None)))`; when `not (has_gte_version('3.2')) and has_gte_version('3.0')`: execute `server.show_option('terminal-overrides')`; assert `isinstance(terminal_overrides, dict)`; `'screen*' in terminal_overrides or 'xterm*' in terminal_overrides`; when `not (has_gte_version('3.2')) and not (has_gte_version('3.0'))`: execute `server.show_option('terminal-overrides')`; assert `isinstance(terminal_overrides, dict)`; `'screen*' in terminal_overrides`; `'xterm*' in terminal_overrides`; when `has_gte_version('2.9')`: execute `server.show_option('status-format')`; assert `isinstance(status_format, (dict, type(None)))`; when `not (has_gte_version('2.9'))`: execute `server.show_option('status-format')`; expect `pytest.raises(exc.OptionError)`; when `has_gte_version('3.0')`: execute `server.show_option('update-environment')`; assert `isinstance(update_env, (list, type(None)))`; when `not (has_gte_version('3.0'))`: execute `server.show_option('update-environment')`; expect `pytest.raises(exc.OptionError)`; when `has_gte_version('3.3')`: execute `pane.show_option('pane-colours')`; assert `pane.show_option('pane-colours') is None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_stable_baseline_options_and_hooks_4f936586b7 | 3.3a through 3.7c; conditional boundaries: 3.2, 3.4, 3.0, 2.9, 3.3 | planned parity | +| tests/test_options.py::test_style_option_validation | Test style option validation; fixtures `server`; execute `server.new_session(session_name='test')`; `session.set_option('status-style', 'fg=red,bg=default,bold')`; `session.show_option('status-style')`; `session.set_option('status-style', 'invalid-style')`; expect `pytest.raises(exc.OptionError)`; assert `isinstance(style, str)`; `'fg=red' in str(style)`; when `has_gte_version('3.2')`: assert `'bg=default' in str(style)`; `'bold' in str(style)`; when `not (has_gte_version('3.2'))`: assert `'bright' in str(style)`; `'bg=default' not in str(style)`; when `has_gte_version('3.0')`: execute `session.set_option('status-style', 'fg=colour240,bg=#525252,bold,underscore')`; `session.show_option('status-style')`; assert `isinstance(style, str)`; when `has_gte_version('3.0') and has_gte_version('3.2')`: assert `style == 'fg=colour240,bg=#525252,bold,underscore'`; when `has_gte_version('3.0') and not (has_gte_version('3.2'))`: assert `style == 'fg=colour240,bg=#525252,bright,underscore'`; when `has_gte_version('3.0') and has_gte_version('3.2')`: execute `session.set_option('status-style', 'fg=#{?pane_in_mode,red,green}')`; `session.show_option('status-style')`; assert `isinstance(style, str)`; `style == 'fg=#{?pane_in_mode,red,green}'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_options_py_test_style_option_validation_029080827c | 3.3a through 3.7c; conditional boundaries: 3.2, 3.0 | planned parity | | tests/test_options.py::test_terminal_features | Test tmux's terminal-feature option destructuring; fixtures `server`, `monkeypatch`; execute `server._show_options()`; `fake_cmd(stdout=MOCKED_GLOBAL_OPTIONS)`; `any(('terminal-features' in k for k in options_))`; `Options(**options_)`; assert `any(('terminal-features' in k for k in options_))`; `options`; `options.terminal_features`; `options.terminal_features['screen*'] == ['title']`; `options.terminal_features['xterm*'] == ['clipboard', 'ccolour', 'cstyle', 'focus']`. | unit test | PythonBehaviorParityTest#tests_test_options_py_test_terminal_features_fb34d208ca | not tmux-bound | planned parity | -| tests/test_options.py::test_terminal_features_edge_cases | Test edge cases for terminal-features option; fixtures `server`, `monkeypatch`; execute `server._show_options()`; `fake_cmd(stdout=['terminal-features[0] xterm*:'])`; `t.cast(dict[str, list[str]], options['terminal-features'])`; `fake_cmd(stdout=['terminal-features[0] xterm*:invalid:feature:with:colons'])`; `any((f == 'invalid' for f in xterm_features))`; `any((f == 'feature' for f in xterm_features))`; assert `isinstance(terminal_features, dict)`; `isinstance(xterm_features, list)`; `xterm_features == ['']`; `any((f == 'invalid' for f in xterm_features))`; `any((f == 'feature' for f in xterm_features))`; `any((f == 'feature with space' for f in xterm_features))`; `any((f == 'special*char' for f in xterm_features))`; when `not has_gte_version('3.2')`: skip via `pytest.skip('terminal-features requires tmux >= 3.2')`. | unit test | PythonBehaviorParityTest#tests_test_options_py_test_terminal_features_edge_cases_edde661357 | 3.2a through 3.7b | planned parity | +| tests/test_options.py::test_terminal_features_edge_cases | Test edge cases for terminal-features option; fixtures `server`, `monkeypatch`; execute `server._show_options()`; `fake_cmd(stdout=['terminal-features[0] xterm*:'])`; `t.cast(dict[str, list[str]], options['terminal-features'])`; `fake_cmd(stdout=['terminal-features[0] xterm*:invalid:feature:with:colons'])`; `any((f == 'invalid' for f in xterm_features))`; `any((f == 'feature' for f in xterm_features))`; assert `isinstance(terminal_features, dict)`; `isinstance(xterm_features, list)`; `xterm_features == ['']`; `any((f == 'invalid' for f in xterm_features))`; `any((f == 'feature' for f in xterm_features))`; `any((f == 'feature with space' for f in xterm_features))`; `any((f == 'special*char' for f in xterm_features))`; when `not has_gte_version('3.2')`: skip via `pytest.skip('terminal-features requires tmux >= 3.2')`. | unit test | PythonBehaviorParityTest#tests_test_options_py_test_terminal_features_edge_cases_edde661357 | 3.3a through 3.7c | planned parity | | tests/test_options.py::test_terminal_overrides | Test tmux's terminal-overrides option destructuring; fixtures `server`, `monkeypatch`; execute `server._show_options()`; `any(('terminal-overrides' in k for k in options_))`; `Options(**options_)`; assert `any(('terminal-overrides' in k for k in options_))`; `options`; `options.terminal_overrides`; `options_['terminal-overrides'] is not None`; `isinstance(options_['terminal-overrides'], dict)`; `not isinstance(options_['terminal-overrides'], SparseArray)`; `'xterm-256color' in options_['terminal-overrides']`; `isinstance(options_['terminal-overrides']['xterm-256color'], dict)`; `options_['terminal-overrides']['xterm-256color'] == {'Tc': None}`. | unit test | PythonBehaviorParityTest#tests_test_options_py_test_terminal_overrides_e3dccbff7a | not tmux-bound | planned parity | | tests/test_options.py::test_terminal_overrides_multi_feature[flag_and_keyval-xterm*:XT:Ms=clipboard-xterm*-expected_features1] | Test terminal-overrides parsing with multiple features per terminal; case inputs `expected_features={'XT': None, 'Ms': 'clipboard'}`, `expected_term='xterm*'`, `raw_entry='xterm*:XT:Ms=clipboard'`, `test_id='flag_and_keyval'`; fixtures `server`, `monkeypatch`; execute `server._show_options()`; `fake_cmd(stdout=mock_options)`; `t.cast(TerminalOverrides, options_['terminal-overrides'])`; assert `'terminal-overrides' in options_`; `expected_term in term_overrides`; `term_overrides[expected_term] == expected_features`. | unit test | PythonBehaviorParityTest#i_feature_flag_and_keyval_xterm_XT_Ms_clipboard_xterm_expected_features1_561bc3e455 | not tmux-bound | planned parity | | tests/test_options.py::test_terminal_overrides_multi_feature[integer_value-tmux*:colors=256:Tc-tmux*-expected_features3] | Test terminal-overrides parsing with multiple features per terminal; case inputs `expected_features={'colors': 256, 'Tc': None}`, `expected_term='tmux*'`, `raw_entry='tmux*:colors=256:Tc'`, `test_id='integer_value'`; fixtures `server`, `monkeypatch`; execute `server._show_options()`; `fake_cmd(stdout=mock_options)`; `t.cast(TerminalOverrides, options_['terminal-overrides'])`; assert `'terminal-overrides' in options_`; `expected_term in term_overrides`; `term_overrides[expected_term] == expected_features`. | unit test | PythonBehaviorParityTest#s_multi_feature_integer_value_tmux_colors_256_Tc_tmux_expected_features3_4bfd500687 | not tmux-bound | planned parity | @@ -1032,213 +1032,213 @@ under [`tests/`](https://github.com/tmux-python/libtmux/blob/c4a980b/tests) and | tests/test_options.py::test_window_only_3_7_options_absent_from_pane[tree_mode_preview_style] | Window-only tmux 3.7 options must not be typed on PaneOptions; case inputs `field_name='tree_mode_preview_style'`; assert `field_name in WindowOptions.__dataclass_fields__`; `field_name not in PaneOptions.__dataclass_fields__`. | unit test | PythonBehaviorParityTest#py_test_window_only_3_7_options_absent_from_pane_tree_mode_preview_style_75d3564fd0 | not tmux-bound | planned parity | | tests/test_options.py::test_window_only_3_7_options_absent_from_pane[window_pane_current_status_format] | Window-only tmux 3.7 options must not be typed on PaneOptions; case inputs `field_name='window_pane_current_status_format'`; assert `field_name in WindowOptions.__dataclass_fields__`; `field_name not in PaneOptions.__dataclass_fields__`. | unit test | PythonBehaviorParityTest#ndow_only_3_7_options_absent_from_pane_window_pane_current_status_format_efbcedc6a3 | not tmux-bound | planned parity | | tests/test_options.py::test_window_only_3_7_options_absent_from_pane[window_pane_status_format] | Window-only tmux 3.7 options must not be typed on PaneOptions; case inputs `field_name='window_pane_status_format'`; assert `field_name in WindowOptions.__dataclass_fields__`; `field_name not in PaneOptions.__dataclass_fields__`. | unit test | PythonBehaviorParityTest#_test_window_only_3_7_options_absent_from_pane_window_pane_status_format_46da9681d0 | not tmux-bound | planned parity | -| tests/test_pane.py::test_break_pane_basic | Test Pane.break_pane() creates a new window; fixtures `session`; execute `session.new_window(window_name='test_break')`; `pane.split(shell='sleep 1m')`; `session.refresh()`; `window.refresh()`; `new_pane.break_pane()`; assert `pane is not None`; `len(window.panes) == 2`; `len(session.windows) == initial_window_count + 1`; `len(window.panes) == 1`; `new_window.window_id is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_break_pane_basic_88615b93f8 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_break_pane_no_name_uses_natural_name | Pane.break_pane() without a name keeps tmux's default window name; fixtures `session`; execute `session.new_window(window_name='test_break_natural')`; `pane.split(shell='sleep 123')`; `new_pane.break_pane()`; assert `pane is not None`; `broken.window_name == 'sleep'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_break_pane_no_name_uses_natural_name_460bb1fba9 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_break_pane_with_name | Test Pane.break_pane() with window_name; fixtures `session`; execute `session.new_window(window_name='test_break_name')`; `pane.split(shell='sleep 1m')`; `new_pane.break_pane(window_name='my_broken')`; assert `pane is not None`; `new_window.window_name == 'my_broken'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_break_pane_with_name_5e3188fbf6 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_capture_pane | Verify Pane.capture_pane(); fixtures `session`; execute `session.new_window(attach=True, window_name='capture_pane', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys('printf "\\n%s\\n" "Hello World !"', literal=True, suppress_history=False)`; `shutil.which('env')`; `'\n'.join(pane.capture_pane())`; `'$ printf "\\n%s\\n" "Hello World !"{}'.format('\n\nHello World !\n$')`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; `pane_contents == '$'`; `pane_contents == '$ printf "\\n%s\\n" "Hello World !"{}'.format('\n\nHello World !\n$')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_capture_pane_5c79c5858d | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_capture_pane_3_7_flags[hyperlinks] | Pane.capture_pane() -H/-L/-F on tmux 3.7+; warn-and-ignore below; case inputs `kwargs={'hyperlinks': True}`, `match='hyperlinks requires tmux 3.7'`, `test_id='hyperlinks'`; fixtures `session`; execute `session.new_window(window_name='capture_37')`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.capture_pane(**kwargs)`; assert `isinstance(pane.capture_pane(**kwargs), list)`; when `not (has_gte_version('3.7'))`: execute `pane.capture_pane(**kwargs)`; expect `pytest.warns(UserWarning, match=match)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_capture_pane_3_7_flags_hyperlinks_459c25abda | 3.2a through 3.7b; conditional boundaries: 3.7 | planned parity | -| tests/test_pane.py::test_capture_pane_3_7_flags[line_flags] | Pane.capture_pane() -H/-L/-F on tmux 3.7+; warn-and-ignore below; case inputs `kwargs={'line_flags': True}`, `match='line_flags requires tmux 3.7'`, `test_id='line_flags'`; fixtures `session`; execute `session.new_window(window_name='capture_37')`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.capture_pane(**kwargs)`; assert `isinstance(pane.capture_pane(**kwargs), list)`; when `not (has_gte_version('3.7'))`: execute `pane.capture_pane(**kwargs)`; expect `pytest.warns(UserWarning, match=match)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_capture_pane_3_7_flags_line_flags_b3fe130bf7 | 3.2a through 3.7b; conditional boundaries: 3.7 | planned parity | -| tests/test_pane.py::test_capture_pane_3_7_flags[line_numbers] | Pane.capture_pane() -H/-L/-F on tmux 3.7+; warn-and-ignore below; case inputs `kwargs={'line_numbers': True}`, `match='line_numbers requires tmux 3.7'`, `test_id='line_numbers'`; fixtures `session`; execute `session.new_window(window_name='capture_37')`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.capture_pane(**kwargs)`; assert `isinstance(pane.capture_pane(**kwargs), list)`; when `not (has_gte_version('3.7'))`: execute `pane.capture_pane(**kwargs)`; expect `pytest.warns(UserWarning, match=match)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_capture_pane_3_7_flags_line_numbers_cbeafcfc86 | 3.2a through 3.7b; conditional boundaries: 3.7 | planned parity | -| tests/test_pane.py::test_capture_pane_end | Assert Pane.capture_pane() with ``end`` param; fixtures `session`; execute `session.new_window(attach=True, window_name='capture_pane_end', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys('printf "%s"', literal=True, suppress_history=False)`; `pane.capture_pane(end=0)`; `pane.capture_pane(end='-')`; `shutil.which('env')`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; `pane_contents == '$'`; `pane_contents == '$ printf "%s"\n$'`; `pane_contents == '$ printf "%s"'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_capture_pane_end_87c7445a38 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_capture_pane_start | Assert Pane.capture_pane() with ``start`` param; fixtures `session`; execute `session.new_window(attach=True, window_name='capture_pane_start', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys('printf "%s"', literal=True, suppress_history=False)`; `pane.send_keys('clear -x', literal=True, suppress_history=False)`; `pane.capture_pane(start=-2)`; `pane.send_keys('')`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; `pane_contents == '$'`; `pane_contents == '$ printf "%s"\n$'`; `pane_contents_history_start[0] == '$ printf "%s"'`; `pane_contents_history_start[1] == '$ clear -x'`; `pane_contents_history_start[-1] == '$'`; `retry_until(pane_contents_capture_visible_only_shows_prompt, 1, raises=True)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_capture_pane_start_7508de4c44 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_choose_tree_with_flags | Test Pane.choose_tree() with format, filter, sort, reverse, zoom; fixtures `session`; execute `pane.choose_tree(format_string='#{session_name}', filter_expression='#{?session_attached,1,0}', sort_order='name', reverse=True, zoom=True)`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_choose_tree_with_flags_8590687499 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_chooser_smoke[choose_buffer] | Smoke test: chooser/customize methods invoke without error; case inputs `method='choose_buffer'`; fixtures `session`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_chooser_smoke_choose_buffer_f5e29cc822 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_chooser_smoke[choose_client] | Smoke test: chooser/customize methods invoke without error; case inputs `method='choose_client'`; fixtures `session`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_chooser_smoke_choose_client_940e1d7237 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_chooser_smoke[choose_tree] | Smoke test: chooser/customize methods invoke without error; case inputs `method='choose_tree'`; fixtures `session`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_chooser_smoke_choose_tree_49e32edb8b | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_chooser_smoke[customize_mode] | Smoke test: chooser/customize methods invoke without error; case inputs `method='customize_mode'`; fixtures `session`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_chooser_smoke_customize_mode_f0a891a32d | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_clear_history | Test Pane.clear_history(); fixtures `session`; execute `session.new_window(window_name='test_clearhist', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys('echo line1', enter=True)`; `pane.send_keys('echo line2', enter=True)`; `pane.clear_history()`; `pane.capture_pane(start=-100)`; assert `env is not None`; `pane is not None`; `len(history) <= 30`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_clear_history_3c49111e24 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_clock_mode | Test Pane.clock_mode() enters clock mode; fixtures `session`; execute `pane.clock_mode()`; `pane.send_keys('q', enter=False)`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_clock_mode_76d4a16346 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_copy_mode | Test Pane.copy_mode() enters copy mode; fixtures `session`; execute `pane.copy_mode()`; `pane.send_keys('q', enter=False)`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_copy_mode_321d51fa2b | 3.2a through 3.7b | planned parity | +| tests/test_pane.py::test_break_pane_basic | Test Pane.break_pane() creates a new window; fixtures `session`; execute `session.new_window(window_name='test_break')`; `pane.split(shell='sleep 1m')`; `session.refresh()`; `window.refresh()`; `new_pane.break_pane()`; assert `pane is not None`; `len(window.panes) == 2`; `len(session.windows) == initial_window_count + 1`; `len(window.panes) == 1`; `new_window.window_id is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_break_pane_basic_88615b93f8 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_break_pane_no_name_uses_natural_name | Pane.break_pane() without a name keeps tmux's default window name; fixtures `session`; execute `session.new_window(window_name='test_break_natural')`; `pane.split(shell='sleep 123')`; `new_pane.break_pane()`; assert `pane is not None`; `broken.window_name == 'sleep'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_break_pane_no_name_uses_natural_name_460bb1fba9 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_break_pane_with_name | Test Pane.break_pane() with window_name; fixtures `session`; execute `session.new_window(window_name='test_break_name')`; `pane.split(shell='sleep 1m')`; `new_pane.break_pane(window_name='my_broken')`; assert `pane is not None`; `new_window.window_name == 'my_broken'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_break_pane_with_name_5e3188fbf6 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_capture_pane | Verify Pane.capture_pane(); fixtures `session`; execute `session.new_window(attach=True, window_name='capture_pane', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys('printf "\\n%s\\n" "Hello World !"', literal=True, suppress_history=False)`; `shutil.which('env')`; `'\n'.join(pane.capture_pane())`; `'$ printf "\\n%s\\n" "Hello World !"{}'.format('\n\nHello World !\n$')`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; `pane_contents == '$'`; `pane_contents == '$ printf "\\n%s\\n" "Hello World !"{}'.format('\n\nHello World !\n$')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_capture_pane_5c79c5858d | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_capture_pane_3_7_flags[hyperlinks] | Pane.capture_pane() -H/-L/-F on tmux 3.7+; warn-and-ignore below; case inputs `kwargs={'hyperlinks': True}`, `match='hyperlinks requires tmux 3.7'`, `test_id='hyperlinks'`; fixtures `session`; execute `session.new_window(window_name='capture_37')`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.capture_pane(**kwargs)`; assert `isinstance(pane.capture_pane(**kwargs), list)`; when `not (has_gte_version('3.7'))`: execute `pane.capture_pane(**kwargs)`; expect `pytest.warns(UserWarning, match=match)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_capture_pane_3_7_flags_hyperlinks_459c25abda | 3.3a through 3.7c; conditional boundaries: 3.7 | planned parity | +| tests/test_pane.py::test_capture_pane_3_7_flags[line_flags] | Pane.capture_pane() -H/-L/-F on tmux 3.7+; warn-and-ignore below; case inputs `kwargs={'line_flags': True}`, `match='line_flags requires tmux 3.7'`, `test_id='line_flags'`; fixtures `session`; execute `session.new_window(window_name='capture_37')`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.capture_pane(**kwargs)`; assert `isinstance(pane.capture_pane(**kwargs), list)`; when `not (has_gte_version('3.7'))`: execute `pane.capture_pane(**kwargs)`; expect `pytest.warns(UserWarning, match=match)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_capture_pane_3_7_flags_line_flags_b3fe130bf7 | 3.3a through 3.7c; conditional boundaries: 3.7 | planned parity | +| tests/test_pane.py::test_capture_pane_3_7_flags[line_numbers] | Pane.capture_pane() -H/-L/-F on tmux 3.7+; warn-and-ignore below; case inputs `kwargs={'line_numbers': True}`, `match='line_numbers requires tmux 3.7'`, `test_id='line_numbers'`; fixtures `session`; execute `session.new_window(window_name='capture_37')`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.capture_pane(**kwargs)`; assert `isinstance(pane.capture_pane(**kwargs), list)`; when `not (has_gte_version('3.7'))`: execute `pane.capture_pane(**kwargs)`; expect `pytest.warns(UserWarning, match=match)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_capture_pane_3_7_flags_line_numbers_cbeafcfc86 | 3.3a through 3.7c; conditional boundaries: 3.7 | planned parity | +| tests/test_pane.py::test_capture_pane_end | Assert Pane.capture_pane() with ``end`` param; fixtures `session`; execute `session.new_window(attach=True, window_name='capture_pane_end', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys('printf "%s"', literal=True, suppress_history=False)`; `pane.capture_pane(end=0)`; `pane.capture_pane(end='-')`; `shutil.which('env')`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; `pane_contents == '$'`; `pane_contents == '$ printf "%s"\n$'`; `pane_contents == '$ printf "%s"'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_capture_pane_end_87c7445a38 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_capture_pane_start | Assert Pane.capture_pane() with ``start`` param; fixtures `session`; execute `session.new_window(attach=True, window_name='capture_pane_start', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys('printf "%s"', literal=True, suppress_history=False)`; `pane.send_keys('clear -x', literal=True, suppress_history=False)`; `pane.capture_pane(start=-2)`; `pane.send_keys('')`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; `pane_contents == '$'`; `pane_contents == '$ printf "%s"\n$'`; `pane_contents_history_start[0] == '$ printf "%s"'`; `pane_contents_history_start[1] == '$ clear -x'`; `pane_contents_history_start[-1] == '$'`; `retry_until(pane_contents_capture_visible_only_shows_prompt, 1, raises=True)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_capture_pane_start_7508de4c44 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_choose_tree_with_flags | Test Pane.choose_tree() with format, filter, sort, reverse, zoom; fixtures `session`; execute `pane.choose_tree(format_string='#{session_name}', filter_expression='#{?session_attached,1,0}', sort_order='name', reverse=True, zoom=True)`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_choose_tree_with_flags_8590687499 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_chooser_smoke[choose_buffer] | Smoke test: chooser/customize methods invoke without error; case inputs `method='choose_buffer'`; fixtures `session`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_chooser_smoke_choose_buffer_f5e29cc822 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_chooser_smoke[choose_client] | Smoke test: chooser/customize methods invoke without error; case inputs `method='choose_client'`; fixtures `session`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_chooser_smoke_choose_client_940e1d7237 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_chooser_smoke[choose_tree] | Smoke test: chooser/customize methods invoke without error; case inputs `method='choose_tree'`; fixtures `session`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_chooser_smoke_choose_tree_49e32edb8b | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_chooser_smoke[customize_mode] | Smoke test: chooser/customize methods invoke without error; case inputs `method='customize_mode'`; fixtures `session`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_chooser_smoke_customize_mode_f0a891a32d | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_clear_history | Test Pane.clear_history(); fixtures `session`; execute `session.new_window(window_name='test_clearhist', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys('echo line1', enter=True)`; `pane.send_keys('echo line2', enter=True)`; `pane.clear_history()`; `pane.capture_pane(start=-100)`; assert `env is not None`; `pane is not None`; `len(history) <= 30`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_clear_history_3c49111e24 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_clock_mode | Test Pane.clock_mode() enters clock mode; fixtures `session`; execute `pane.clock_mode()`; `pane.send_keys('q', enter=False)`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_clock_mode_76d4a16346 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_copy_mode | Test Pane.copy_mode() enters copy mode; fixtures `session`; execute `pane.copy_mode()`; `pane.send_keys('q', enter=False)`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_copy_mode_321d51fa2b | 3.3a through 3.7c | planned parity | | tests/test_pane.py::test_copy_mode_page_down | Test Pane.copy_mode(page_down=True) on tmux 3.5+; fixtures `session`; execute `pane.copy_mode()`; `pane.copy_mode(page_down=True)`; `pane.send_keys('q', enter=False)`; assert `pane is not None`; when `not has_gte_version('3.5')`: skip via `pytest.skip('page_down requires tmux 3.5+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_copy_mode_page_down_5f89234414 | 3.5 through 3.7b | planned parity | -| tests/test_pane.py::test_copy_mode_source_pane | Test Pane.copy_mode(source_pane=...) reads another pane's history; fixtures `session`; execute `session.new_window(window_name='copy_src')`; `window.split()`; `src.send_keys('echo SOURCE_TEXT', enter=True)`; `dest.copy_mode(source_pane=str(src.pane_id))`; `dest.send_keys('q', enter=False)`; assert `src is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_copy_mode_source_pane_1abd42b965 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_display_message_flags[all_formats] | Test display_message() with various flag combinations; case inputs `cmd=''`, `expected_in_output='session_name'`, `kwargs={'get_text': True, 'all_formats': True}`, `min_tmux_version=None`, `test_id='all_formats'`; fixtures `session`; execute `pane.display_message(cmd, **kwargs)`; assert `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_in_output is not None`: execute `'\n'.join(result)`; assert `result is not None`; `expected_in_output in output`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_display_message_flags_all_formats_a7cb1286c6 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_display_message_flags[format_string] | Test display_message() with various flag combinations; case inputs `cmd=''`, `expected_in_output='%'`, `kwargs={'get_text': True, 'format_string': '#{pane_id}'}`, `min_tmux_version=None`, `test_id='format_string'`; fixtures `session`; execute `pane.display_message(cmd, **kwargs)`; assert `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_in_output is not None`: execute `'\n'.join(result)`; assert `result is not None`; `expected_in_output in output`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_display_message_flags_format_string_4c94bb7d53 | 3.2a through 3.7b | planned parity | +| tests/test_pane.py::test_copy_mode_source_pane | Test Pane.copy_mode(source_pane=...) reads another pane's history; fixtures `session`; execute `session.new_window(window_name='copy_src')`; `window.split()`; `src.send_keys('echo SOURCE_TEXT', enter=True)`; `dest.copy_mode(source_pane=str(src.pane_id))`; `dest.send_keys('q', enter=False)`; assert `src is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_copy_mode_source_pane_1abd42b965 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_display_message_flags[all_formats] | Test display_message() with various flag combinations; case inputs `cmd=''`, `expected_in_output='session_name'`, `kwargs={'get_text': True, 'all_formats': True}`, `min_tmux_version=None`, `test_id='all_formats'`; fixtures `session`; execute `pane.display_message(cmd, **kwargs)`; assert `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_in_output is not None`: execute `'\n'.join(result)`; assert `result is not None`; `expected_in_output in output`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_display_message_flags_all_formats_a7cb1286c6 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_display_message_flags[format_string] | Test display_message() with various flag combinations; case inputs `cmd=''`, `expected_in_output='%'`, `kwargs={'get_text': True, 'format_string': '#{pane_id}'}`, `min_tmux_version=None`, `test_id='format_string'`; fixtures `session`; execute `pane.display_message(cmd, **kwargs)`; assert `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_in_output is not None`: execute `'\n'.join(result)`; assert `result is not None`; `expected_in_output in output`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_display_message_flags_format_string_4c94bb7d53 | 3.3a through 3.7c | planned parity | | tests/test_pane.py::test_display_message_flags[no_expand] | Test display_message() with various flag combinations; case inputs `cmd='#{pane_id}'`, `expected_in_output='#{pane_id}'`, `kwargs={'get_text': True, 'no_expand': True}`, `min_tmux_version='3.4'`, `test_id='no_expand'`; fixtures `session`; execute `pane.display_message(cmd, **kwargs)`; assert `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_in_output is not None`: execute `'\n'.join(result)`; assert `result is not None`; `expected_in_output in output`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_display_message_flags_no_expand_a4d76d3352 | 3.4 through 3.7b | planned parity | -| tests/test_pane.py::test_display_message_flags[verbose] | Test display_message() with various flag combinations; case inputs `cmd=''`, `expected_in_output='session_name'`, `kwargs={'get_text': True, 'verbose': True, 'all_formats': True}`, `min_tmux_version=None`, `test_id='verbose'`; fixtures `session`; execute `pane.display_message(cmd, **kwargs)`; assert `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_in_output is not None`: execute `'\n'.join(result)`; assert `result is not None`; `expected_in_output in output`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_display_message_flags_verbose_7c17dc099c | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_display_message_warns_on_tmux_error | Tmux stderr on ``display-message`` surfaces as a :class:`UserWarning`; fixtures `session`; execute `pane.display_message('x', get_text=True, format_string='#{pane_id}')`; expect `pytest.warns(UserWarning, match='only one of -F or argument')`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_display_message_warns_on_tmux_error_752ca8c041 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_display_panes | Test Pane.display_panes() shows pane numbers; fixtures `control_mode`, `session`; execute `control_mode()`; `pane.display_panes()`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_display_panes_d1be737cbf | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_display_popup_close_existing | Test Pane.display_popup(close_existing=True) returns cleanly; fixtures `control_mode`, `session`; execute `control_mode()`; `pane.display_popup(close_existing=True)`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_display_popup_close_existing_f3dfd2c33b | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_display_popup_close_on_success | Test Pane.display_popup() with close_on_success (-EE) alone; fixtures `control_mode`, `session`, `tmp_path`; execute `control_mode()`; `pane.display_popup(command=f'touch {marker}', close_on_success=True)`; `retry_until(lambda: marker.exists(), 3, raises=True)`; `marker.exists()`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_display_popup_close_on_success_48a77d755a | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_display_popup_flags[basic] | Test Pane.display_popup() flag combinations; case inputs `kwargs={}`, `min_tmux_version=None`, `test_id='basic'`; fixtures `control_mode`, `session`, `tmp_path`; execute `control_mode()`; `pane.display_popup(**call_kwargs)`; `retry_until(lambda: marker.exists(), 3, raises=True)`; `marker.exists()`; assert `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_display_popup_flags_basic_bc482c9f76 | 3.2a through 3.7b | planned parity | +| tests/test_pane.py::test_display_message_flags[verbose] | Test display_message() with various flag combinations; case inputs `cmd=''`, `expected_in_output='session_name'`, `kwargs={'get_text': True, 'verbose': True, 'all_formats': True}`, `min_tmux_version=None`, `test_id='verbose'`; fixtures `session`; execute `pane.display_message(cmd, **kwargs)`; assert `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_in_output is not None`: execute `'\n'.join(result)`; assert `result is not None`; `expected_in_output in output`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_display_message_flags_verbose_7c17dc099c | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_display_message_warns_on_tmux_error | Tmux stderr on ``display-message`` surfaces as a :class:`UserWarning`; fixtures `session`; execute `pane.display_message('x', get_text=True, format_string='#{pane_id}')`; expect `pytest.warns(UserWarning, match='only one of -F or argument')`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_display_message_warns_on_tmux_error_752ca8c041 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_display_panes | Test Pane.display_panes() shows pane numbers; fixtures `control_mode`, `session`; execute `control_mode()`; `pane.display_panes()`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_display_panes_d1be737cbf | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_display_popup_close_existing | Test Pane.display_popup(close_existing=True) returns cleanly; fixtures `control_mode`, `session`; execute `control_mode()`; `pane.display_popup(close_existing=True)`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_display_popup_close_existing_f3dfd2c33b | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_display_popup_close_on_success | Test Pane.display_popup() with close_on_success (-EE) alone; fixtures `control_mode`, `session`, `tmp_path`; execute `control_mode()`; `pane.display_popup(command=f'touch {marker}', close_on_success=True)`; `retry_until(lambda: marker.exists(), 3, raises=True)`; `marker.exists()`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_display_popup_close_on_success_48a77d755a | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_display_popup_flags[basic] | Test Pane.display_popup() flag combinations; case inputs `kwargs={}`, `min_tmux_version=None`, `test_id='basic'`; fixtures `control_mode`, `session`, `tmp_path`; execute `control_mode()`; `pane.display_popup(**call_kwargs)`; `retry_until(lambda: marker.exists(), 3, raises=True)`; `marker.exists()`; assert `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_display_popup_flags_basic_bc482c9f76 | 3.3a through 3.7c | planned parity | | tests/test_pane.py::test_display_popup_flags[border_lines_v33] | Test Pane.display_popup() flag combinations; case inputs `kwargs={'border_lines': 'single'}`, `min_tmux_version='3.3'`, `test_id='border_lines_v33'`; fixtures `control_mode`, `session`, `tmp_path`; execute `control_mode()`; `pane.display_popup(**call_kwargs)`; `retry_until(lambda: marker.exists(), 3, raises=True)`; `marker.exists()`; assert `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_display_popup_flags_border_lines_v33_e860d627fb | 3.3 through 3.7b | planned parity | | tests/test_pane.py::test_display_popup_flags[border_style_v33] | Test Pane.display_popup() flag combinations; case inputs `kwargs={'border_style': 'fg=red'}`, `min_tmux_version='3.3'`, `test_id='border_style_v33'`; fixtures `control_mode`, `session`, `tmp_path`; execute `control_mode()`; `pane.display_popup(**call_kwargs)`; `retry_until(lambda: marker.exists(), 3, raises=True)`; `marker.exists()`; assert `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_display_popup_flags_border_style_v33_df76b3a3fe | 3.3 through 3.7b | planned parity | | tests/test_pane.py::test_display_popup_flags[close_on_any_key_v36] | Test Pane.display_popup() flag combinations; case inputs `kwargs={'close_on_any_key': True}`, `min_tmux_version='3.6'`, `test_id='close_on_any_key_v36'`; fixtures `control_mode`, `session`, `tmp_path`; execute `control_mode()`; `pane.display_popup(**call_kwargs)`; `retry_until(lambda: marker.exists(), 3, raises=True)`; `marker.exists()`; assert `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_display_popup_flags_close_on_any_key_v36_86b847dfd8 | 3.6 through 3.7b | planned parity | -| tests/test_pane.py::test_display_popup_flags[dimensions] | Test Pane.display_popup() flag combinations; case inputs `kwargs={'width': 40, 'height': 10}`, `min_tmux_version=None`, `test_id='dimensions'`; fixtures `control_mode`, `session`, `tmp_path`; execute `control_mode()`; `pane.display_popup(**call_kwargs)`; `retry_until(lambda: marker.exists(), 3, raises=True)`; `marker.exists()`; assert `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_display_popup_flags_dimensions_6b52563d9e | 3.2a through 3.7b | planned parity | +| tests/test_pane.py::test_display_popup_flags[dimensions] | Test Pane.display_popup() flag combinations; case inputs `kwargs={'width': 40, 'height': 10}`, `min_tmux_version=None`, `test_id='dimensions'`; fixtures `control_mode`, `session`, `tmp_path`; execute `control_mode()`; `pane.display_popup(**call_kwargs)`; `retry_until(lambda: marker.exists(), 3, raises=True)`; `marker.exists()`; assert `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_display_popup_flags_dimensions_6b52563d9e | 3.3a through 3.7c | planned parity | | tests/test_pane.py::test_display_popup_flags[environment_v33] | Test Pane.display_popup() flag combinations; case inputs `kwargs={'environment': {'FOO': 'bar'}}`, `min_tmux_version='3.3'`, `test_id='environment_v33'`; fixtures `control_mode`, `session`, `tmp_path`; execute `control_mode()`; `pane.display_popup(**call_kwargs)`; `retry_until(lambda: marker.exists(), 3, raises=True)`; `marker.exists()`; assert `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_display_popup_flags_environment_v33_0e77730b00 | 3.3 through 3.7b | planned parity | | tests/test_pane.py::test_display_popup_flags[no_border_v33] | Test Pane.display_popup() flag combinations; case inputs `kwargs={'no_border': True}`, `min_tmux_version='3.3'`, `test_id='no_border_v33'`; fixtures `control_mode`, `session`, `tmp_path`; execute `control_mode()`; `pane.display_popup(**call_kwargs)`; `retry_until(lambda: marker.exists(), 3, raises=True)`; `marker.exists()`; assert `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_display_popup_flags_no_border_v33_a51011ca4d | 3.3 through 3.7b | planned parity | | tests/test_pane.py::test_display_popup_flags[no_keys_v36] | Test Pane.display_popup() flag combinations; case inputs `kwargs={'no_keys': True}`, `min_tmux_version='3.6'`, `test_id='no_keys_v36'`; fixtures `control_mode`, `session`, `tmp_path`; execute `control_mode()`; `pane.display_popup(**call_kwargs)`; `retry_until(lambda: marker.exists(), 3, raises=True)`; `marker.exists()`; assert `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_display_popup_flags_no_keys_v36_fcf004fe41 | 3.6 through 3.7b | planned parity | -| tests/test_pane.py::test_display_popup_flags[position] | Test Pane.display_popup() flag combinations; case inputs `kwargs={'x': 'C', 'y': 'C'}`, `min_tmux_version=None`, `test_id='position'`; fixtures `control_mode`, `session`, `tmp_path`; execute `control_mode()`; `pane.display_popup(**call_kwargs)`; `retry_until(lambda: marker.exists(), 3, raises=True)`; `marker.exists()`; assert `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_display_popup_flags_position_487ae89d40 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_display_popup_flags[start_directory] | Test Pane.display_popup() flag combinations; case inputs `kwargs={'start_directory': PosixPath('/tmp')}`, `min_tmux_version=None`, `test_id='start_directory'`; fixtures `control_mode`, `session`, `tmp_path`; execute `control_mode()`; `pane.display_popup(**call_kwargs)`; `retry_until(lambda: marker.exists(), 3, raises=True)`; `marker.exists()`; assert `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_display_popup_flags_start_directory_077a667155 | 3.2a through 3.7b | planned parity | +| tests/test_pane.py::test_display_popup_flags[position] | Test Pane.display_popup() flag combinations; case inputs `kwargs={'x': 'C', 'y': 'C'}`, `min_tmux_version=None`, `test_id='position'`; fixtures `control_mode`, `session`, `tmp_path`; execute `control_mode()`; `pane.display_popup(**call_kwargs)`; `retry_until(lambda: marker.exists(), 3, raises=True)`; `marker.exists()`; assert `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_display_popup_flags_position_487ae89d40 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_display_popup_flags[start_directory] | Test Pane.display_popup() flag combinations; case inputs `kwargs={'start_directory': PosixPath('/tmp')}`, `min_tmux_version=None`, `test_id='start_directory'`; fixtures `control_mode`, `session`, `tmp_path`; execute `control_mode()`; `pane.display_popup(**call_kwargs)`; `retry_until(lambda: marker.exists(), 3, raises=True)`; `marker.exists()`; assert `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_display_popup_flags_start_directory_077a667155 | 3.3a through 3.7c | planned parity | | tests/test_pane.py::test_display_popup_flags[style_v33] | Test Pane.display_popup() flag combinations; case inputs `kwargs={'style': 'bg=blue'}`, `min_tmux_version='3.3'`, `test_id='style_v33'`; fixtures `control_mode`, `session`, `tmp_path`; execute `control_mode()`; `pane.display_popup(**call_kwargs)`; `retry_until(lambda: marker.exists(), 3, raises=True)`; `marker.exists()`; assert `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_display_popup_flags_style_v33_4d78d07047 | 3.3 through 3.7b | planned parity | | tests/test_pane.py::test_display_popup_flags[title_v33] | Test Pane.display_popup() flag combinations; case inputs `kwargs={'title': 'popup_title'}`, `min_tmux_version='3.3'`, `test_id='title_v33'`; fixtures `control_mode`, `session`, `tmp_path`; execute `control_mode()`; `pane.display_popup(**call_kwargs)`; `retry_until(lambda: marker.exists(), 3, raises=True)`; `marker.exists()`; assert `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_display_popup_flags_title_v33_6ae6423d00 | 3.3 through 3.7b | planned parity | -| tests/test_pane.py::test_display_popup_mutual_exclusion | close_on_exit and close_on_success are mutually exclusive; fixtures `session`; execute `pane.display_popup(close_on_exit=True, close_on_success=True)`; expect `pytest.raises(ValueError, match='mutually exclusive')`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_display_popup_mutual_exclusion_f3463fcb69 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_display_popup_target_client | Test Pane.display_popup(target_client=...) emits ``-c <client>``; fixtures `control_mode`, `session`, `tmp_path`; execute `control_mode()`; `pane.display_popup(command=f'touch {marker}', close_on_exit=True, target_client=ctl.client_name)`; `retry_until(lambda: marker.exists(), 3, raises=True)`; `marker.exists()`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_display_popup_target_client_2d1fc1c6a4 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_find_window | Test Pane.find_window() opens filtered tree; fixtures `session`; execute `pane.find_window('sh')`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_find_window_8ed3f18c08 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_join_pane | Test Pane.join() roundtrip with break_pane; fixtures `session`; execute `session.new_window(window_name='test_join')`; `pane.split(shell='sleep 1m')`; `window.refresh()`; `session.refresh()`; `new_pane.break_pane()`; `new_pane.join(window)`; assert `pane is not None`; `len(window.panes) == 2`; `len(window.panes) == 1`; `new_window.window_id not in window_ids`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_join_pane_5ae329eb74 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_join_pane_horizontal | Test Pane.join() with horizontal split; fixtures `session`; execute `session.new_window(window_name='test_join_h')`; `window.resize(height=40, width=80)`; `pane.split(shell='sleep 1m')`; `window.refresh()`; `new_pane.break_pane()`; `new_pane.join(window, vertical=False)`; assert `pane is not None`; `len(window.panes) == 2`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_join_pane_horizontal_fc114aa456 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_move_pane | Test Pane.move() moves pane to another window; fixtures `session`; execute `session.new_window(window_name='move_src')`; `pane.split(shell='sleep 1m')`; `session.new_window(window_name='move_dst')`; `pane_to_move.move(w2)`; `w1.refresh()`; `w2.refresh()`; assert `pane is not None`; `len(w1.panes) == 2`; `len(w1.panes) == 1`; `len(w2.panes) == initial_w2_panes + 1`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_move_pane_d3c9554da4 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_new_pane_error_tags_subcommand | Pane.new_pane() tags LibTmuxException with the new-pane subcommand; fixtures `session`; execute `session.new_window(window_name='new_pane_err')`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.new_pane(target='%99999')`; expect `pytest.raises(exc.LibTmuxException)`; assert `excinfo.value.subcommand == 'new-pane'`; `str(excinfo.value).startswith('new-pane:')`; when `not (has_gte_version('3.7'))`: execute `pane.new_pane(target='%99999')`; expect `pytest.raises(exc.LibTmuxException, match='requires tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_new_pane_error_tags_subcommand_4a743db95a | 3.2a through 3.7b; conditional boundaries: 3.7 | planned parity | -| tests/test_pane.py::test_new_pane_floating | Pane.new_pane() creates a floating pane on tmux 3.7+ (else raises); fixtures `session`; execute `session.new_window(window_name='floating_pane')`; `window.resize(height=50, width=200)`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.new_pane(width=80, height=15, x=5, y=3, shell='sleep 30')`; assert `floating.pane_floating_flag == '1'`; `floating.pane_width == '80'`; `floating.pane_height == '15'`; when `not (has_gte_version('3.7'))`: execute `pane.new_pane(width=40, height=10)`; expect `pytest.raises(exc.LibTmuxException, match='new_pane .*requires tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_new_pane_floating_6b77d7b2b5 | 3.2a through 3.7b; conditional boundaries: 3.7 | planned parity | -| tests/test_pane.py::test_new_pane_keep | Pane.new_pane(keep=True) sets remain-on-exit (-k) on tmux 3.7+; fixtures `session`; execute `session.new_window(window_name='floating_keep')`; `window.resize(height=50, width=200)`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.new_pane(width=80, height=15, keep=True, shell='sleep 30')`; `floating.cmd('show-options', '-p', '-v', 'remain-on-exit')`; assert `floating.pane_floating_flag == '1'`; `remain == ['key']`; when `not (has_gte_version('3.7'))`: execute `pane.new_pane(width=40, height=10, keep=True)`; expect `pytest.raises(exc.LibTmuxException, match='requires tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_new_pane_keep_4b5ffc0512 | 3.2a through 3.7b; conditional boundaries: 3.7 | planned parity | -| tests/test_pane.py::test_new_pane_preserves_empty_option_values[active_border_style] | Pane.new_pane() preserves empty string option values; case inputs `option_kwargs={'active_border_style': ''}`, `test_id='active_border_style'`; fixtures `session`; execute `session.new_window(window_name=f'floating_empty_{test_id}')`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.new_pane(width=40, height=10, shell='sleep 30', **option_kwargs)`; assert `floating.pane_floating_flag == '1'`; when `not (has_gte_version('3.7'))`: execute `pane.new_pane(width=40, height=10, **option_kwargs)`; expect `pytest.raises(exc.LibTmuxException, match='requires tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#_pane_py_test_new_pane_preserves_empty_option_values_active_border_style_8570ad6ce9 | 3.2a through 3.7b; conditional boundaries: 3.7 | planned parity | -| tests/test_pane.py::test_new_pane_preserves_empty_option_values[inactive_border_style] | Pane.new_pane() preserves empty string option values; case inputs `option_kwargs={'inactive_border_style': ''}`, `test_id='inactive_border_style'`; fixtures `session`; execute `session.new_window(window_name=f'floating_empty_{test_id}')`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.new_pane(width=40, height=10, shell='sleep 30', **option_kwargs)`; assert `floating.pane_floating_flag == '1'`; when `not (has_gte_version('3.7'))`: execute `pane.new_pane(width=40, height=10, **option_kwargs)`; expect `pytest.raises(exc.LibTmuxException, match='requires tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#ane_py_test_new_pane_preserves_empty_option_values_inactive_border_style_6ccbab863c | 3.2a through 3.7b; conditional boundaries: 3.7 | planned parity | -| tests/test_pane.py::test_new_pane_preserves_empty_option_values[message] | Pane.new_pane() preserves empty string option values; case inputs `option_kwargs={'message': ''}`, `test_id='message'`; fixtures `session`; execute `session.new_window(window_name=f'floating_empty_{test_id}')`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.new_pane(width=40, height=10, shell='sleep 30', **option_kwargs)`; assert `floating.pane_floating_flag == '1'`; when `not (has_gte_version('3.7'))`: execute `pane.new_pane(width=40, height=10, **option_kwargs)`; expect `pytest.raises(exc.LibTmuxException, match='requires tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_new_pane_preserves_empty_option_values_message_c68b33a3e7 | 3.2a through 3.7b; conditional boundaries: 3.7 | planned parity | -| tests/test_pane.py::test_new_pane_preserves_empty_option_values[style] | Pane.new_pane() preserves empty string option values; case inputs `option_kwargs={'style': ''}`, `test_id='style'`; fixtures `session`; execute `session.new_window(window_name=f'floating_empty_{test_id}')`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.new_pane(width=40, height=10, shell='sleep 30', **option_kwargs)`; assert `floating.pane_floating_flag == '1'`; when `not (has_gte_version('3.7'))`: execute `pane.new_pane(width=40, height=10, **option_kwargs)`; expect `pytest.raises(exc.LibTmuxException, match='requires tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_new_pane_preserves_empty_option_values_style_d25122e330 | 3.2a through 3.7b; conditional boundaries: 3.7 | planned parity | -| tests/test_pane.py::test_pane_context_manager | Test Pane context manager functionality; fixtures `session`; execute `session.new_window()`; `window.split()`; `window.refresh()`; assert `len(window.panes) == initial_pane_count + 1`; `pane in window.panes`; `len(window.panes) == initial_pane_count`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_pane_context_manager_62ed08382e | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_format_field_declared_and_hydrated[pane_dead] | Tmux's pane-scope format tokens hydrate onto the typed ``Pane`` object; case inputs `field_name='pane_dead'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `field_name in pane.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_pane_py_test_pane_format_field_declared_and_hydrated_pane_dead_ae18d02006 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_format_field_declared_and_hydrated[pane_format] | Tmux's pane-scope format tokens hydrate onto the typed ``Pane`` object; case inputs `field_name='pane_format'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `field_name in pane.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#ts_test_pane_py_test_pane_format_field_declared_and_hydrated_pane_format_ed1d8fbe23 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_format_field_declared_and_hydrated[pane_in_mode] | Tmux's pane-scope format tokens hydrate onto the typed ``Pane`` object; case inputs `field_name='pane_in_mode'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `field_name in pane.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#s_test_pane_py_test_pane_format_field_declared_and_hydrated_pane_in_mode_9596e57fe2 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_format_field_declared_and_hydrated[pane_input_off] | Tmux's pane-scope format tokens hydrate onto the typed ``Pane`` object; case inputs `field_name='pane_input_off'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `field_name in pane.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#test_pane_py_test_pane_format_field_declared_and_hydrated_pane_input_off_461985887b | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_format_field_declared_and_hydrated[pane_last] | Tmux's pane-scope format tokens hydrate onto the typed ``Pane`` object; case inputs `field_name='pane_last'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `field_name in pane.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_pane_py_test_pane_format_field_declared_and_hydrated_pane_last_34a343452d | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_format_field_declared_and_hydrated[pane_marked] | Tmux's pane-scope format tokens hydrate onto the typed ``Pane`` object; case inputs `field_name='pane_marked'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `field_name in pane.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#ts_test_pane_py_test_pane_format_field_declared_and_hydrated_pane_marked_605d545bd1 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_format_field_declared_and_hydrated[pane_marked_set] | Tmux's pane-scope format tokens hydrate onto the typed ``Pane`` object; case inputs `field_name='pane_marked_set'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `field_name in pane.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#est_pane_py_test_pane_format_field_declared_and_hydrated_pane_marked_set_b0e3ed48e9 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_format_field_declared_and_hydrated[pane_mode] | Tmux's pane-scope format tokens hydrate onto the typed ``Pane`` object; case inputs `field_name='pane_mode'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `field_name in pane.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_pane_py_test_pane_format_field_declared_and_hydrated_pane_mode_37ec00ef51 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_format_field_declared_and_hydrated[pane_path] | Tmux's pane-scope format tokens hydrate onto the typed ``Pane`` object; case inputs `field_name='pane_path'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `field_name in pane.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_pane_py_test_pane_format_field_declared_and_hydrated_pane_path_fa345ec315 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_format_field_declared_and_hydrated[pane_pipe] | Tmux's pane-scope format tokens hydrate onto the typed ``Pane`` object; case inputs `field_name='pane_pipe'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `field_name in pane.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_pane_py_test_pane_format_field_declared_and_hydrated_pane_pipe_b422778718 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_format_field_declared_and_hydrated[pane_synchronized] | Tmux's pane-scope format tokens hydrate onto the typed ``Pane`` object; case inputs `field_name='pane_synchronized'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `field_name in pane.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#t_pane_py_test_pane_format_field_declared_and_hydrated_pane_synchronized_65a88cc186 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_refresh_raises_when_pane_id_is_none | ``Pane.refresh()`` raises ``ValueError`` when ``pane_id`` is unset; fixtures `session`; execute `pane.refresh()`; `Pane(server=session.server)`; expect `pytest.raises(ValueError, match='pane_id')`; assert `pane.pane_id is None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_pane_refresh_raises_when_pane_id_is_none_66f817fe96 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_reset_clears_history_and_sends_reset | Pane.reset() runs both ``send-keys -R`` and ``clear-history`` (#650); fixtures `session`; execute `session.new_window(window_name='test_reset_650', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(f'echo reset_marker_{n}', enter=True)`; `pane.capture_pane(start=-100)`; `pane.reset()`; `shutil.which('env')`; assert `env is not None`; `pane is not None`; `any(('reset_marker_' in line for line in pre))`; `result is pane`; `not any(('reset_marker_' in line for line in post))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_pane_reset_clears_history_and_sends_reset_3e29bfb3f5 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_reset_targets_non_active_pane | Pane.reset() clears the target pane, not tmux's cmdq default; fixtures `session`; execute `session.new_window(window_name='test_reset_targets', window_shell=f"{env} PS1='$ ' sh")`; `window.split(shell=f"{env} PS1='$ ' sh", attach=False)`; `window.refresh()`; `pane.cmd('display-message', '-p', '#{history_size}')`; `shutil.which('env')`; `retry_until(lambda: '$' in '\n'.join(active_sibling.capture_pane()), 2, raises=True)`; assert `env is not None`; `active_sibling is not None`; `target.pane_id != active_sibling.pane_id`; `window.active_pane is not None`; `window.active_pane.pane_id == active_sibling.pane_id`; `target_pre > 0`; `sibling_pre > 0`; `_history_size(target) == 0`; `_history_size(active_sibling) == sibling_pre`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_pane_reset_targets_non_active_pane_5a58caa018 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_scope_override_field_hydrates[alternate_saved_x] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='alternate_saved_x'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#s_test_pane_py_test_pane_scope_override_field_hydrates_alternate_saved_x_46815334e9 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_scope_override_field_hydrates[alternate_saved_y] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='alternate_saved_y'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#s_test_pane_py_test_pane_scope_override_field_hydrates_alternate_saved_y_3e0f3e7dc9 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_scope_override_field_hydrates[cursor_flag] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='cursor_flag'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_pane_scope_override_field_hydrates_cursor_flag_2a795ab287 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_scope_override_field_hydrates[cursor_x] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='cursor_x'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_pane_scope_override_field_hydrates_cursor_x_d4f9b7de10 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_scope_override_field_hydrates[cursor_y] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='cursor_y'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_pane_scope_override_field_hydrates_cursor_y_873ac3adee | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_scope_override_field_hydrates[history_bytes] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='history_bytes'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_pane_scope_override_field_hydrates_history_bytes_c312d4adc7 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_scope_override_field_hydrates[history_limit] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='history_limit'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_pane_scope_override_field_hydrates_history_limit_487cddb770 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_scope_override_field_hydrates[history_size] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='history_size'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_pane_scope_override_field_hydrates_history_size_777bb7d2c3 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_scope_override_field_hydrates[insert_flag] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='insert_flag'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_pane_scope_override_field_hydrates_insert_flag_5037a94cc9 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_scope_override_field_hydrates[keypad_cursor_flag] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='keypad_cursor_flag'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#_test_pane_py_test_pane_scope_override_field_hydrates_keypad_cursor_flag_a006dd02a9 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_scope_override_field_hydrates[keypad_flag] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='keypad_flag'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_pane_scope_override_field_hydrates_keypad_flag_3a6819feee | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_scope_override_field_hydrates[mouse_all_flag] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='mouse_all_flag'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_pane_py_test_pane_scope_override_field_hydrates_mouse_all_flag_148b334003 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_scope_override_field_hydrates[mouse_any_flag] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='mouse_any_flag'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_pane_py_test_pane_scope_override_field_hydrates_mouse_any_flag_fb70b3e8c7 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_scope_override_field_hydrates[mouse_button_flag] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='mouse_button_flag'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#s_test_pane_py_test_pane_scope_override_field_hydrates_mouse_button_flag_9afa1efe7c | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_scope_override_field_hydrates[mouse_sgr_flag] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='mouse_sgr_flag'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_pane_py_test_pane_scope_override_field_hydrates_mouse_sgr_flag_8944c5767a | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_scope_override_field_hydrates[mouse_standard_flag] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='mouse_standard_flag'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#test_pane_py_test_pane_scope_override_field_hydrates_mouse_standard_flag_b5d6bfaea5 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_scope_override_field_hydrates[origin_flag] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='origin_flag'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_pane_scope_override_field_hydrates_origin_flag_3898d39be2 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_scope_override_field_hydrates[scroll_region_lower] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='scroll_region_lower'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#test_pane_py_test_pane_scope_override_field_hydrates_scroll_region_lower_f95cb8d620 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_scope_override_field_hydrates[scroll_region_upper] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='scroll_region_upper'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#test_pane_py_test_pane_scope_override_field_hydrates_scroll_region_upper_1a2328f65d | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_scope_override_field_hydrates[wrap_flag] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='wrap_flag'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_pane_scope_override_field_hydrates_wrap_flag_668d7959ab | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_split_window_zoom | Verify splitting window with zoom; fixtures `session`; execute `session.new_window(window_name='split_without_zoom')`; `session.new_window(window_name='split_with_zoom')`; `initial_pane_without_zoom.split(zoom=False)`; `initial_pane_with_zoom.split(zoom=True)`; assert `initial_pane_without_zoom is not None`; `initial_pane_with_zoom is not None`; `pane_without_zoom.width == pane_without_zoom.window_width`; `pane_without_zoom.height is not None`; `pane_without_zoom.window_height is not None`; `pane_without_zoom.height < pane_without_zoom.window_height`; `pane_with_zoom.width == pane_with_zoom.window_width`; `pane_with_zoom.height == pane_with_zoom.window_height`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_pane_split_window_zoom_618e4c0a8e | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_pane_synchronized_reflects_window_state | ``pane.pane_synchronized`` flips when synchronize-panes toggles; fixtures `session`; execute `window.split()`; `window.set_option('synchronize-panes', 'on')`; `pane.refresh()`; `window.set_option('synchronize-panes', 'off')`; assert `pane is not None`; `pane.pane_synchronized == '1'`; `pane.pane_synchronized == '0'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_pane_synchronized_reflects_window_state_bddbaaffa5 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_paste_buffer | Test Pane.paste_buffer() pastes buffer content into pane; fixtures `session`; execute `session.new_window(window_name='test_paste', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `session.server.set_buffer('pasted_content', buffer_name='paste_test')`; `pane.paste_buffer(buffer_name='paste_test')`; `shutil.which('env')`; `retry_until(lambda: '$' in '\n'.join(pane.capture_pane()), 2, raises=True)`; assert `env is not None`; `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_paste_buffer_e8a928f842 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_paste_buffer_no_vis | Pane.paste_buffer(no_vis=True) uses -S on tmux 3.7+; warn below; fixtures `session`; execute `session.new_window(window_name='paste_novis', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `session.server.set_buffer('raw_content', buffer_name='novis_test')`; `shutil.which('env')`; `retry_until(lambda: '$' in '\n'.join(pane.capture_pane()), 2, raises=True)`; `'\n'.join(pane.capture_pane())`; assert `env is not None`; `pane is not None`; when `has_gte_version('3.7')`: execute `pane.paste_buffer(buffer_name='novis_test', no_vis=True)`; `pane.capture_pane()`; `retry_until(lambda: 'raw_content' in '\n'.join(pane.capture_pane()), 3, raises=True)`; `'\n'.join(pane.capture_pane())`; when `not (has_gte_version('3.7'))`: execute `pane.paste_buffer(buffer_name='novis_test', no_vis=True)`; expect `pytest.warns(UserWarning, match='no_vis requires tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_paste_buffer_no_vis_a1475a57ed | 3.2a through 3.7b; conditional boundaries: 3.7 | planned parity | -| tests/test_pane.py::test_pipe_pane | Test Pane.pipe() pipes output to a file; fixtures `session`, `tmp_path`; execute `session.new_window(window_name='test_pipe', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.pipe(f'cat >> {pipe_file}')`; `pane.send_keys('echo pipe_test_ok', enter=True)`; `pane.pipe()`; `shutil.which('env')`; assert `env is not None`; `pane is not None`; `'pipe_test_ok' in content`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_pipe_pane_e8f22fe893 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_resize | Verify resizing pane; fixtures `session`; execute `session.cmd('detach-client', '-s')`; `window.split(attach=False)`; `window.split(direction=PaneDirection.Above, attach=False)`; `window.resize(height=500, width=500)`; `pane.resize(height='50')`; `window.select_layout('main-horizontal')`; assert `pane is not None`; `pane.pane_height is not None`; `pane.pane_width is not None`; `int(pane.pane_height) == 50`; `int(pane.pane_width) == 75`; `int(pane.pane_height) == 75`; `pane_height_before - pane_height_adjustment * 2 == int(pane.pane_height)`; `pane_height_before + pane_height_adjustment == int(pane.pane_height)`; `pane_height_before < pane_height_expanded`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_resize_7efb639998 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_respawn_pane_kill | Test Pane.respawn() with kill flag on active pane; fixtures `session`; execute `session.new_window(window_name='test_respawn')`; `pane.respawn(kill=True, shell='sh')`; `pane.refresh()`; assert `pane is not None`; `pane in window.panes`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_respawn_pane_kill_a42cf02479 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_select_pane_direction | Test Pane.select() with direction flags; fixtures `session`; execute `session.new_window(window_name='test_select_dir')`; `window.resize(height=40, width=80)`; `pane_top.split(direction=PaneDirection.Below)`; `pane_bottom.select()`; `pane_bottom.refresh()`; `pane_bottom.select(direction=ResizeAdjustmentDirection.Up)`; assert `pane_top is not None`; `pane_bottom.pane_active == '1'`; `pane_top.pane_active == '1'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_select_pane_direction_fb5f1d3c55 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_select_pane_disable_enable_input | Test Pane.select() with disable/enable input flags; fixtures `session`; execute `session.new_window(window_name='test_input_toggle', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.select(disable_input=True)`; `pane.send_keys('echo disabled_test', enter=False)`; `pane.select(enable_input=True)`; `pane.send_keys('echo enabled_ok', enter=True)`; assert `env is not None`; `pane is not None`; `'disabled_test' not in contents`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_select_pane_disable_enable_input_b4359f56b1 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_select_pane_last | Test Pane.select() with last flag; fixtures `session`; execute `session.new_window(window_name='test_select_last')`; `pane1.split()`; `pane2.select()`; `pane2.refresh()`; `pane1.select()`; `pane1.refresh()`; assert `pane1 is not None`; `pane2.pane_active == '1'`; `pane1.pane_active == '1'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_select_pane_last_94129b39c1 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_select_pane_mark | Test Pane.select() with mark/clear_mark flags; fixtures `session`; execute `session.new_window(window_name='test_select_mark')`; `pane.select(mark=True)`; `pane.select(clear_mark=True)`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_select_pane_mark_2345b5e28f | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_send_keys | Verify Pane.send_keys(); fixtures `session`; execute `pane.send_keys('c-c', literal=True)`; `pane.cmd('capture-pane', '-p')`; `pane.send_keys('c-a', literal=False)`; `'\n'.join(pane.cmd('capture-pane', '-p').stdout)`; assert `pane is not None`; `'c-c' in pane_contents`; `'c-a' not in pane_contents; message='should not print to pane'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_send_keys_13758b0615 | 3.2a through 3.7b | planned parity | +| tests/test_pane.py::test_display_popup_mutual_exclusion | close_on_exit and close_on_success are mutually exclusive; fixtures `session`; execute `pane.display_popup(close_on_exit=True, close_on_success=True)`; expect `pytest.raises(ValueError, match='mutually exclusive')`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_display_popup_mutual_exclusion_f3463fcb69 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_display_popup_target_client | Test Pane.display_popup(target_client=...) emits ``-c <client>``; fixtures `control_mode`, `session`, `tmp_path`; execute `control_mode()`; `pane.display_popup(command=f'touch {marker}', close_on_exit=True, target_client=ctl.client_name)`; `retry_until(lambda: marker.exists(), 3, raises=True)`; `marker.exists()`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_display_popup_target_client_2d1fc1c6a4 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_find_window | Test Pane.find_window() opens filtered tree; fixtures `session`; execute `pane.find_window('sh')`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_find_window_8ed3f18c08 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_join_pane | Test Pane.join() roundtrip with break_pane; fixtures `session`; execute `session.new_window(window_name='test_join')`; `pane.split(shell='sleep 1m')`; `window.refresh()`; `session.refresh()`; `new_pane.break_pane()`; `new_pane.join(window)`; assert `pane is not None`; `len(window.panes) == 2`; `len(window.panes) == 1`; `new_window.window_id not in window_ids`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_join_pane_5ae329eb74 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_join_pane_horizontal | Test Pane.join() with horizontal split; fixtures `session`; execute `session.new_window(window_name='test_join_h')`; `window.resize(height=40, width=80)`; `pane.split(shell='sleep 1m')`; `window.refresh()`; `new_pane.break_pane()`; `new_pane.join(window, vertical=False)`; assert `pane is not None`; `len(window.panes) == 2`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_join_pane_horizontal_fc114aa456 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_move_pane | Test Pane.move() moves pane to another window; fixtures `session`; execute `session.new_window(window_name='move_src')`; `pane.split(shell='sleep 1m')`; `session.new_window(window_name='move_dst')`; `pane_to_move.move(w2)`; `w1.refresh()`; `w2.refresh()`; assert `pane is not None`; `len(w1.panes) == 2`; `len(w1.panes) == 1`; `len(w2.panes) == initial_w2_panes + 1`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_move_pane_d3c9554da4 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_new_pane_error_tags_subcommand | Pane.new_pane() tags LibTmuxException with the new-pane subcommand; fixtures `session`; execute `session.new_window(window_name='new_pane_err')`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.new_pane(target='%99999')`; expect `pytest.raises(exc.LibTmuxException)`; assert `excinfo.value.subcommand == 'new-pane'`; `str(excinfo.value).startswith('new-pane:')`; when `not (has_gte_version('3.7'))`: execute `pane.new_pane(target='%99999')`; expect `pytest.raises(exc.LibTmuxException, match='requires tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_new_pane_error_tags_subcommand_4a743db95a | 3.3a through 3.7c; conditional boundaries: 3.7 | planned parity | +| tests/test_pane.py::test_new_pane_floating | Pane.new_pane() creates a floating pane on tmux 3.7+ (else raises); fixtures `session`; execute `session.new_window(window_name='floating_pane')`; `window.resize(height=50, width=200)`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.new_pane(width=80, height=15, x=5, y=3, shell='sleep 30')`; assert `floating.pane_floating_flag == '1'`; `floating.pane_width == '80'`; `floating.pane_height == '15'`; when `not (has_gte_version('3.7'))`: execute `pane.new_pane(width=40, height=10)`; expect `pytest.raises(exc.LibTmuxException, match='new_pane .*requires tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_new_pane_floating_6b77d7b2b5 | 3.3a through 3.7c; conditional boundaries: 3.7 | planned parity | +| tests/test_pane.py::test_new_pane_keep | Pane.new_pane(keep=True) sets remain-on-exit (-k) on tmux 3.7+; fixtures `session`; execute `session.new_window(window_name='floating_keep')`; `window.resize(height=50, width=200)`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.new_pane(width=80, height=15, keep=True, shell='sleep 30')`; `floating.cmd('show-options', '-p', '-v', 'remain-on-exit')`; assert `floating.pane_floating_flag == '1'`; `remain == ['key']`; when `not (has_gte_version('3.7'))`: execute `pane.new_pane(width=40, height=10, keep=True)`; expect `pytest.raises(exc.LibTmuxException, match='requires tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_new_pane_keep_4b5ffc0512 | 3.3a through 3.7c; conditional boundaries: 3.7 | planned parity | +| tests/test_pane.py::test_new_pane_preserves_empty_option_values[active_border_style] | Pane.new_pane() preserves empty string option values; case inputs `option_kwargs={'active_border_style': ''}`, `test_id='active_border_style'`; fixtures `session`; execute `session.new_window(window_name=f'floating_empty_{test_id}')`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.new_pane(width=40, height=10, shell='sleep 30', **option_kwargs)`; assert `floating.pane_floating_flag == '1'`; when `not (has_gte_version('3.7'))`: execute `pane.new_pane(width=40, height=10, **option_kwargs)`; expect `pytest.raises(exc.LibTmuxException, match='requires tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#_pane_py_test_new_pane_preserves_empty_option_values_active_border_style_8570ad6ce9 | 3.3a through 3.7c; conditional boundaries: 3.7 | planned parity | +| tests/test_pane.py::test_new_pane_preserves_empty_option_values[inactive_border_style] | Pane.new_pane() preserves empty string option values; case inputs `option_kwargs={'inactive_border_style': ''}`, `test_id='inactive_border_style'`; fixtures `session`; execute `session.new_window(window_name=f'floating_empty_{test_id}')`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.new_pane(width=40, height=10, shell='sleep 30', **option_kwargs)`; assert `floating.pane_floating_flag == '1'`; when `not (has_gte_version('3.7'))`: execute `pane.new_pane(width=40, height=10, **option_kwargs)`; expect `pytest.raises(exc.LibTmuxException, match='requires tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#ane_py_test_new_pane_preserves_empty_option_values_inactive_border_style_6ccbab863c | 3.3a through 3.7c; conditional boundaries: 3.7 | planned parity | +| tests/test_pane.py::test_new_pane_preserves_empty_option_values[message] | Pane.new_pane() preserves empty string option values; case inputs `option_kwargs={'message': ''}`, `test_id='message'`; fixtures `session`; execute `session.new_window(window_name=f'floating_empty_{test_id}')`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.new_pane(width=40, height=10, shell='sleep 30', **option_kwargs)`; assert `floating.pane_floating_flag == '1'`; when `not (has_gte_version('3.7'))`: execute `pane.new_pane(width=40, height=10, **option_kwargs)`; expect `pytest.raises(exc.LibTmuxException, match='requires tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_new_pane_preserves_empty_option_values_message_c68b33a3e7 | 3.3a through 3.7c; conditional boundaries: 3.7 | planned parity | +| tests/test_pane.py::test_new_pane_preserves_empty_option_values[style] | Pane.new_pane() preserves empty string option values; case inputs `option_kwargs={'style': ''}`, `test_id='style'`; fixtures `session`; execute `session.new_window(window_name=f'floating_empty_{test_id}')`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.new_pane(width=40, height=10, shell='sleep 30', **option_kwargs)`; assert `floating.pane_floating_flag == '1'`; when `not (has_gte_version('3.7'))`: execute `pane.new_pane(width=40, height=10, **option_kwargs)`; expect `pytest.raises(exc.LibTmuxException, match='requires tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_new_pane_preserves_empty_option_values_style_d25122e330 | 3.3a through 3.7c; conditional boundaries: 3.7 | planned parity | +| tests/test_pane.py::test_pane_context_manager | Test Pane context manager functionality; fixtures `session`; execute `session.new_window()`; `window.split()`; `window.refresh()`; assert `len(window.panes) == initial_pane_count + 1`; `pane in window.panes`; `len(window.panes) == initial_pane_count`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_pane_context_manager_62ed08382e | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_format_field_declared_and_hydrated[pane_dead] | Tmux's pane-scope format tokens hydrate onto the typed ``Pane`` object; case inputs `field_name='pane_dead'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `field_name in pane.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_pane_py_test_pane_format_field_declared_and_hydrated_pane_dead_ae18d02006 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_format_field_declared_and_hydrated[pane_format] | Tmux's pane-scope format tokens hydrate onto the typed ``Pane`` object; case inputs `field_name='pane_format'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `field_name in pane.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#ts_test_pane_py_test_pane_format_field_declared_and_hydrated_pane_format_ed1d8fbe23 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_format_field_declared_and_hydrated[pane_in_mode] | Tmux's pane-scope format tokens hydrate onto the typed ``Pane`` object; case inputs `field_name='pane_in_mode'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `field_name in pane.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#s_test_pane_py_test_pane_format_field_declared_and_hydrated_pane_in_mode_9596e57fe2 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_format_field_declared_and_hydrated[pane_input_off] | Tmux's pane-scope format tokens hydrate onto the typed ``Pane`` object; case inputs `field_name='pane_input_off'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `field_name in pane.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#test_pane_py_test_pane_format_field_declared_and_hydrated_pane_input_off_461985887b | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_format_field_declared_and_hydrated[pane_last] | Tmux's pane-scope format tokens hydrate onto the typed ``Pane`` object; case inputs `field_name='pane_last'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `field_name in pane.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_pane_py_test_pane_format_field_declared_and_hydrated_pane_last_34a343452d | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_format_field_declared_and_hydrated[pane_marked] | Tmux's pane-scope format tokens hydrate onto the typed ``Pane`` object; case inputs `field_name='pane_marked'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `field_name in pane.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#ts_test_pane_py_test_pane_format_field_declared_and_hydrated_pane_marked_605d545bd1 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_format_field_declared_and_hydrated[pane_marked_set] | Tmux's pane-scope format tokens hydrate onto the typed ``Pane`` object; case inputs `field_name='pane_marked_set'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `field_name in pane.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#est_pane_py_test_pane_format_field_declared_and_hydrated_pane_marked_set_b0e3ed48e9 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_format_field_declared_and_hydrated[pane_mode] | Tmux's pane-scope format tokens hydrate onto the typed ``Pane`` object; case inputs `field_name='pane_mode'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `field_name in pane.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_pane_py_test_pane_format_field_declared_and_hydrated_pane_mode_37ec00ef51 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_format_field_declared_and_hydrated[pane_path] | Tmux's pane-scope format tokens hydrate onto the typed ``Pane`` object; case inputs `field_name='pane_path'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `field_name in pane.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_pane_py_test_pane_format_field_declared_and_hydrated_pane_path_fa345ec315 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_format_field_declared_and_hydrated[pane_pipe] | Tmux's pane-scope format tokens hydrate onto the typed ``Pane`` object; case inputs `field_name='pane_pipe'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `field_name in pane.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_pane_py_test_pane_format_field_declared_and_hydrated_pane_pipe_b422778718 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_format_field_declared_and_hydrated[pane_synchronized] | Tmux's pane-scope format tokens hydrate onto the typed ``Pane`` object; case inputs `field_name='pane_synchronized'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `field_name in pane.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#t_pane_py_test_pane_format_field_declared_and_hydrated_pane_synchronized_65a88cc186 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_refresh_raises_when_pane_id_is_none | ``Pane.refresh()`` raises ``ValueError`` when ``pane_id`` is unset; fixtures `session`; execute `pane.refresh()`; `Pane(server=session.server)`; expect `pytest.raises(ValueError, match='pane_id')`; assert `pane.pane_id is None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_pane_refresh_raises_when_pane_id_is_none_66f817fe96 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_reset_clears_history_and_sends_reset | Pane.reset() runs both ``send-keys -R`` and ``clear-history`` (#650); fixtures `session`; execute `session.new_window(window_name='test_reset_650', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(f'echo reset_marker_{n}', enter=True)`; `pane.capture_pane(start=-100)`; `pane.reset()`; `shutil.which('env')`; assert `env is not None`; `pane is not None`; `any(('reset_marker_' in line for line in pre))`; `result is pane`; `not any(('reset_marker_' in line for line in post))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_pane_reset_clears_history_and_sends_reset_3e29bfb3f5 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_reset_targets_non_active_pane | Pane.reset() clears the target pane, not tmux's cmdq default; fixtures `session`; execute `session.new_window(window_name='test_reset_targets', window_shell=f"{env} PS1='$ ' sh")`; `window.split(shell=f"{env} PS1='$ ' sh", attach=False)`; `window.refresh()`; `pane.cmd('display-message', '-p', '#{history_size}')`; `shutil.which('env')`; `retry_until(lambda: '$' in '\n'.join(active_sibling.capture_pane()), 2, raises=True)`; assert `env is not None`; `active_sibling is not None`; `target.pane_id != active_sibling.pane_id`; `window.active_pane is not None`; `window.active_pane.pane_id == active_sibling.pane_id`; `target_pre > 0`; `sibling_pre > 0`; `_history_size(target) == 0`; `_history_size(active_sibling) == sibling_pre`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_pane_reset_targets_non_active_pane_5a58caa018 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_scope_override_field_hydrates[alternate_saved_x] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='alternate_saved_x'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#s_test_pane_py_test_pane_scope_override_field_hydrates_alternate_saved_x_46815334e9 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_scope_override_field_hydrates[alternate_saved_y] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='alternate_saved_y'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#s_test_pane_py_test_pane_scope_override_field_hydrates_alternate_saved_y_3e0f3e7dc9 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_scope_override_field_hydrates[cursor_flag] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='cursor_flag'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_pane_scope_override_field_hydrates_cursor_flag_2a795ab287 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_scope_override_field_hydrates[cursor_x] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='cursor_x'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_pane_scope_override_field_hydrates_cursor_x_d4f9b7de10 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_scope_override_field_hydrates[cursor_y] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='cursor_y'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_pane_scope_override_field_hydrates_cursor_y_873ac3adee | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_scope_override_field_hydrates[history_bytes] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='history_bytes'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_pane_scope_override_field_hydrates_history_bytes_c312d4adc7 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_scope_override_field_hydrates[history_limit] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='history_limit'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_pane_scope_override_field_hydrates_history_limit_487cddb770 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_scope_override_field_hydrates[history_size] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='history_size'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_pane_scope_override_field_hydrates_history_size_777bb7d2c3 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_scope_override_field_hydrates[insert_flag] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='insert_flag'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_pane_scope_override_field_hydrates_insert_flag_5037a94cc9 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_scope_override_field_hydrates[keypad_cursor_flag] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='keypad_cursor_flag'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#_test_pane_py_test_pane_scope_override_field_hydrates_keypad_cursor_flag_a006dd02a9 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_scope_override_field_hydrates[keypad_flag] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='keypad_flag'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_pane_scope_override_field_hydrates_keypad_flag_3a6819feee | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_scope_override_field_hydrates[mouse_all_flag] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='mouse_all_flag'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_pane_py_test_pane_scope_override_field_hydrates_mouse_all_flag_148b334003 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_scope_override_field_hydrates[mouse_any_flag] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='mouse_any_flag'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_pane_py_test_pane_scope_override_field_hydrates_mouse_any_flag_fb70b3e8c7 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_scope_override_field_hydrates[mouse_button_flag] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='mouse_button_flag'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#s_test_pane_py_test_pane_scope_override_field_hydrates_mouse_button_flag_9afa1efe7c | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_scope_override_field_hydrates[mouse_sgr_flag] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='mouse_sgr_flag'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_pane_py_test_pane_scope_override_field_hydrates_mouse_sgr_flag_8944c5767a | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_scope_override_field_hydrates[mouse_standard_flag] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='mouse_standard_flag'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#test_pane_py_test_pane_scope_override_field_hydrates_mouse_standard_flag_b5d6bfaea5 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_scope_override_field_hydrates[origin_flag] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='origin_flag'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_pane_scope_override_field_hydrates_origin_flag_3898d39be2 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_scope_override_field_hydrates[scroll_region_lower] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='scroll_region_lower'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#test_pane_py_test_pane_scope_override_field_hydrates_scroll_region_lower_f95cb8d620 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_scope_override_field_hydrates[scroll_region_upper] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='scroll_region_upper'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#test_pane_py_test_pane_scope_override_field_hydrates_scroll_region_upper_1a2328f65d | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_scope_override_field_hydrates[wrap_flag] | Per-token scope overrides admit each token into list-panes -F; case inputs `field_name='wrap_flag'`; fixtures `session`; execute `pane.refresh()`; assert `pane is not None`; `value is not None; message=f'{field_name} should hydrate via list-panes'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_pane_scope_override_field_hydrates_wrap_flag_668d7959ab | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_split_window_zoom | Verify splitting window with zoom; fixtures `session`; execute `session.new_window(window_name='split_without_zoom')`; `session.new_window(window_name='split_with_zoom')`; `initial_pane_without_zoom.split(zoom=False)`; `initial_pane_with_zoom.split(zoom=True)`; assert `initial_pane_without_zoom is not None`; `initial_pane_with_zoom is not None`; `pane_without_zoom.width == pane_without_zoom.window_width`; `pane_without_zoom.height is not None`; `pane_without_zoom.window_height is not None`; `pane_without_zoom.height < pane_without_zoom.window_height`; `pane_with_zoom.width == pane_with_zoom.window_width`; `pane_with_zoom.height == pane_with_zoom.window_height`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_pane_split_window_zoom_618e4c0a8e | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_pane_synchronized_reflects_window_state | ``pane.pane_synchronized`` flips when synchronize-panes toggles; fixtures `session`; execute `window.split()`; `window.set_option('synchronize-panes', 'on')`; `pane.refresh()`; `window.set_option('synchronize-panes', 'off')`; assert `pane is not None`; `pane.pane_synchronized == '1'`; `pane.pane_synchronized == '0'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_pane_synchronized_reflects_window_state_bddbaaffa5 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_paste_buffer | Test Pane.paste_buffer() pastes buffer content into pane; fixtures `session`; execute `session.new_window(window_name='test_paste', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `session.server.set_buffer('pasted_content', buffer_name='paste_test')`; `pane.paste_buffer(buffer_name='paste_test')`; `shutil.which('env')`; `retry_until(lambda: '$' in '\n'.join(pane.capture_pane()), 2, raises=True)`; assert `env is not None`; `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_paste_buffer_e8a928f842 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_paste_buffer_no_vis | Pane.paste_buffer(no_vis=True) uses -S on tmux 3.7+; warn below; fixtures `session`; execute `session.new_window(window_name='paste_novis', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `session.server.set_buffer('raw_content', buffer_name='novis_test')`; `shutil.which('env')`; `retry_until(lambda: '$' in '\n'.join(pane.capture_pane()), 2, raises=True)`; `'\n'.join(pane.capture_pane())`; assert `env is not None`; `pane is not None`; when `has_gte_version('3.7')`: execute `pane.paste_buffer(buffer_name='novis_test', no_vis=True)`; `pane.capture_pane()`; `retry_until(lambda: 'raw_content' in '\n'.join(pane.capture_pane()), 3, raises=True)`; `'\n'.join(pane.capture_pane())`; when `not (has_gte_version('3.7'))`: execute `pane.paste_buffer(buffer_name='novis_test', no_vis=True)`; expect `pytest.warns(UserWarning, match='no_vis requires tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_paste_buffer_no_vis_a1475a57ed | 3.3a through 3.7c; conditional boundaries: 3.7 | planned parity | +| tests/test_pane.py::test_pipe_pane | Test Pane.pipe() pipes output to a file; fixtures `session`, `tmp_path`; execute `session.new_window(window_name='test_pipe', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.pipe(f'cat >> {pipe_file}')`; `pane.send_keys('echo pipe_test_ok', enter=True)`; `pane.pipe()`; `shutil.which('env')`; assert `env is not None`; `pane is not None`; `'pipe_test_ok' in content`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_pipe_pane_e8f22fe893 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_resize | Verify resizing pane; fixtures `session`; execute `session.cmd('detach-client', '-s')`; `window.split(attach=False)`; `window.split(direction=PaneDirection.Above, attach=False)`; `window.resize(height=500, width=500)`; `pane.resize(height='50')`; `window.select_layout('main-horizontal')`; assert `pane is not None`; `pane.pane_height is not None`; `pane.pane_width is not None`; `int(pane.pane_height) == 50`; `int(pane.pane_width) == 75`; `int(pane.pane_height) == 75`; `pane_height_before - pane_height_adjustment * 2 == int(pane.pane_height)`; `pane_height_before + pane_height_adjustment == int(pane.pane_height)`; `pane_height_before < pane_height_expanded`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_resize_7efb639998 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_respawn_pane_kill | Test Pane.respawn() with kill flag on active pane; fixtures `session`; execute `session.new_window(window_name='test_respawn')`; `pane.respawn(kill=True, shell='sh')`; `pane.refresh()`; assert `pane is not None`; `pane in window.panes`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_respawn_pane_kill_a42cf02479 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_select_pane_direction | Test Pane.select() with direction flags; fixtures `session`; execute `session.new_window(window_name='test_select_dir')`; `window.resize(height=40, width=80)`; `pane_top.split(direction=PaneDirection.Below)`; `pane_bottom.select()`; `pane_bottom.refresh()`; `pane_bottom.select(direction=ResizeAdjustmentDirection.Up)`; assert `pane_top is not None`; `pane_bottom.pane_active == '1'`; `pane_top.pane_active == '1'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_select_pane_direction_fb5f1d3c55 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_select_pane_disable_enable_input | Test Pane.select() with disable/enable input flags; fixtures `session`; execute `session.new_window(window_name='test_input_toggle', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.select(disable_input=True)`; `pane.send_keys('echo disabled_test', enter=False)`; `pane.select(enable_input=True)`; `pane.send_keys('echo enabled_ok', enter=True)`; assert `env is not None`; `pane is not None`; `'disabled_test' not in contents`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_select_pane_disable_enable_input_b4359f56b1 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_select_pane_last | Test Pane.select() with last flag; fixtures `session`; execute `session.new_window(window_name='test_select_last')`; `pane1.split()`; `pane2.select()`; `pane2.refresh()`; `pane1.select()`; `pane1.refresh()`; assert `pane1 is not None`; `pane2.pane_active == '1'`; `pane1.pane_active == '1'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_select_pane_last_94129b39c1 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_select_pane_mark | Test Pane.select() with mark/clear_mark flags; fixtures `session`; execute `session.new_window(window_name='test_select_mark')`; `pane.select(mark=True)`; `pane.select(clear_mark=True)`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_select_pane_mark_2345b5e28f | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_send_keys | Verify Pane.send_keys(); fixtures `session`; execute `pane.send_keys('c-c', literal=True)`; `pane.cmd('capture-pane', '-p')`; `pane.send_keys('c-a', literal=False)`; `'\n'.join(pane.cmd('capture-pane', '-p').stdout)`; assert `pane is not None`; `'c-c' in pane_contents`; `'c-a' not in pane_contents; message='should not print to pane'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_send_keys_13758b0615 | 3.3a through 3.7c | planned parity | | tests/test_pane.py::test_send_keys_flag_only_repeat_emits_dash_N | ``send_keys(repeat=3)`` flag-only emits ``send-keys -N 3`` only; fixtures `monkeypatch`, `session`; execute `pane.send_keys(repeat=3, reset=True)`; `real_cmd(cmd_name, *args, **kw)`; assert `pane is not None`; `send_keys_calls == [('send-keys', '-R', '-N', '3')]`. | unit test | PythonBehaviorParityTest#tests_test_pane_py_test_send_keys_flag_only_repeat_emits_dash_N_bad6b5f577 | not tmux-bound | planned parity | -| tests/test_pane.py::test_send_keys_flag_only_requires_a_flag | ``send_keys()`` with neither ``cmd`` nor a flag raises ValueError; fixtures `session`; execute `pane.send_keys()`; expect `pytest.raises(ValueError, match='requires at least one of')`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_send_keys_flag_only_requires_a_flag_13ca72a4e1 | 3.2a through 3.7b | planned parity | +| tests/test_pane.py::test_send_keys_flag_only_requires_a_flag | ``send_keys()`` with neither ``cmd`` nor a flag raises ValueError; fixtures `session`; execute `pane.send_keys()`; expect `pytest.raises(ValueError, match='requires at least one of')`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_send_keys_flag_only_requires_a_flag_13ca72a4e1 | 3.3a through 3.7c | planned parity | | tests/test_pane.py::test_send_keys_flag_only_reset_emits_clean_argv | ``send_keys(reset=True)`` (no positional) emits ``send-keys -R`` only; fixtures `monkeypatch`, `session`; execute `pane.send_keys(reset=True)`; `real_cmd(cmd_name, *args, **kw)`; assert `pane is not None`; `send_keys_calls == [('send-keys', '-R')]`. | unit test | PythonBehaviorParityTest#tests_test_pane_py_test_send_keys_flag_only_reset_emits_clean_argv_de59116e65 | not tmux-bound | planned parity | -| tests/test_pane.py::test_send_keys_flags[expand_formats] | Test send_keys() with various flag combinations; case inputs `expected_in_capture='aa'`, `key='a'`, `kwargs={'expand_formats': True, 'repeat': 2, 'enter': False}`, `min_tmux_version=None`, `not_expected_in_capture=None`, `test_id='expand_formats'`; fixtures `session`; execute `session.new_window(window_name=f'sk_{test_id[:15]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(key, **kwargs)`; `shutil.which('env')`; `retry_until(lambda: '$' in '\n'.join(pane.capture_pane()), 2, raises=True)`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_in_capture is not None`: execute `pane.capture_pane()`; `retry_until(lambda: expected_in_capture in '\n'.join(pane.capture_pane()), 3, raises=True)`; `'\n'.join(pane.capture_pane())`; when `not_expected_in_capture is not None`: execute `pane.capture_pane()`; `'\n'.join(pane.capture_pane())`; assert `not_expected_in_capture not in contents`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_send_keys_flags_expand_formats_26bfa49a92 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_send_keys_flags[hex_key_A] | Test send_keys() with various flag combinations; case inputs `expected_in_capture='A'`, `key='41'`, `kwargs={'hex_keys': True, 'enter': False}`, `min_tmux_version=None`, `not_expected_in_capture=None`, `test_id='hex_key_A'`; fixtures `session`; execute `session.new_window(window_name=f'sk_{test_id[:15]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(key, **kwargs)`; `shutil.which('env')`; `retry_until(lambda: '$' in '\n'.join(pane.capture_pane()), 2, raises=True)`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_in_capture is not None`: execute `pane.capture_pane()`; `retry_until(lambda: expected_in_capture in '\n'.join(pane.capture_pane()), 3, raises=True)`; `'\n'.join(pane.capture_pane())`; when `not_expected_in_capture is not None`: execute `pane.capture_pane()`; `'\n'.join(pane.capture_pane())`; assert `not_expected_in_capture not in contents`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_send_keys_flags_hex_key_A_c83696e0cb | 3.2a through 3.7b | planned parity | +| tests/test_pane.py::test_send_keys_flags[expand_formats] | Test send_keys() with various flag combinations; case inputs `expected_in_capture='aa'`, `key='a'`, `kwargs={'expand_formats': True, 'repeat': 2, 'enter': False}`, `min_tmux_version=None`, `not_expected_in_capture=None`, `test_id='expand_formats'`; fixtures `session`; execute `session.new_window(window_name=f'sk_{test_id[:15]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(key, **kwargs)`; `shutil.which('env')`; `retry_until(lambda: '$' in '\n'.join(pane.capture_pane()), 2, raises=True)`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_in_capture is not None`: execute `pane.capture_pane()`; `retry_until(lambda: expected_in_capture in '\n'.join(pane.capture_pane()), 3, raises=True)`; `'\n'.join(pane.capture_pane())`; when `not_expected_in_capture is not None`: execute `pane.capture_pane()`; `'\n'.join(pane.capture_pane())`; assert `not_expected_in_capture not in contents`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_send_keys_flags_expand_formats_26bfa49a92 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_send_keys_flags[hex_key_A] | Test send_keys() with various flag combinations; case inputs `expected_in_capture='A'`, `key='41'`, `kwargs={'hex_keys': True, 'enter': False}`, `min_tmux_version=None`, `not_expected_in_capture=None`, `test_id='hex_key_A'`; fixtures `session`; execute `session.new_window(window_name=f'sk_{test_id[:15]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(key, **kwargs)`; `shutil.which('env')`; `retry_until(lambda: '$' in '\n'.join(pane.capture_pane()), 2, raises=True)`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_in_capture is not None`: execute `pane.capture_pane()`; `retry_until(lambda: expected_in_capture in '\n'.join(pane.capture_pane()), 3, raises=True)`; `'\n'.join(pane.capture_pane())`; when `not_expected_in_capture is not None`: execute `pane.capture_pane()`; `'\n'.join(pane.capture_pane())`; assert `not_expected_in_capture not in contents`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_send_keys_flags_hex_key_A_c83696e0cb | 3.3a through 3.7c | planned parity | | tests/test_pane.py::test_send_keys_flags[key_name_flag] | Test send_keys() with various flag combinations; case inputs `expected_in_capture=None`, `key='a'`, `kwargs={'key_name': True, 'enter': False}`, `min_tmux_version='3.4'`, `not_expected_in_capture=None`, `test_id='key_name_flag'`; fixtures `session`; execute `session.new_window(window_name=f'sk_{test_id[:15]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(key, **kwargs)`; `shutil.which('env')`; `retry_until(lambda: '$' in '\n'.join(pane.capture_pane()), 2, raises=True)`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_in_capture is not None`: execute `pane.capture_pane()`; `retry_until(lambda: expected_in_capture in '\n'.join(pane.capture_pane()), 3, raises=True)`; `'\n'.join(pane.capture_pane())`; when `not_expected_in_capture is not None`: execute `pane.capture_pane()`; `'\n'.join(pane.capture_pane())`; assert `not_expected_in_capture not in contents`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_send_keys_flags_key_name_flag_5a362347ab | 3.4 through 3.7b | planned parity | -| tests/test_pane.py::test_send_keys_flags[repeat_count] | Test send_keys() with various flag combinations; case inputs `expected_in_capture='aaa'`, `key='a'`, `kwargs={'repeat': 3, 'literal': True, 'enter': False}`, `min_tmux_version=None`, `not_expected_in_capture=None`, `test_id='repeat_count'`; fixtures `session`; execute `session.new_window(window_name=f'sk_{test_id[:15]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(key, **kwargs)`; `shutil.which('env')`; `retry_until(lambda: '$' in '\n'.join(pane.capture_pane()), 2, raises=True)`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_in_capture is not None`: execute `pane.capture_pane()`; `retry_until(lambda: expected_in_capture in '\n'.join(pane.capture_pane()), 3, raises=True)`; `'\n'.join(pane.capture_pane())`; when `not_expected_in_capture is not None`: execute `pane.capture_pane()`; `'\n'.join(pane.capture_pane())`; assert `not_expected_in_capture not in contents`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_send_keys_flags_repeat_count_c3148b7d12 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_send_keys_flags[reset_terminal] | Test send_keys() with various flag combinations; case inputs `expected_in_capture=None`, `key=''`, `kwargs={'reset': True, 'enter': False}`, `min_tmux_version=None`, `not_expected_in_capture=None`, `test_id='reset_terminal'`; fixtures `session`; execute `session.new_window(window_name=f'sk_{test_id[:15]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(key, **kwargs)`; `shutil.which('env')`; `retry_until(lambda: '$' in '\n'.join(pane.capture_pane()), 2, raises=True)`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_in_capture is not None`: execute `pane.capture_pane()`; `retry_until(lambda: expected_in_capture in '\n'.join(pane.capture_pane()), 3, raises=True)`; `'\n'.join(pane.capture_pane())`; when `not_expected_in_capture is not None`: execute `pane.capture_pane()`; `'\n'.join(pane.capture_pane())`; assert `not_expected_in_capture not in contents`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_send_keys_flags_reset_terminal_20ca6419bf | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_send_prefix | Test Pane.send_prefix() sends prefix key without error; fixtures `session`; execute `pane.send_prefix()`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_send_prefix_6786722605 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_set_height | Verify Pane.set_height(); fixtures `session`; execute `session.new_window(window_name='test_set_height')`; `window.split()`; `pane1.set_height(4)`; assert `pane1 is not None`; `pane1.pane_height != pane1_height`; `pane1.pane_height is not None`; `int(pane1.pane_height) == 4`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_set_height_1a4e68d297 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_set_title | Test Pane.set_title() sets pane title; fixtures `session`; execute `pane.set_title('test-title')`; assert `pane is not None`; `result == pane`; `pane.pane_title == 'test-title'`; `pane.title == 'test-title'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_set_title_419f2ad1a8 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_set_title_special_characters | Test Pane.set_title() with spaces and unicode; fixtures `session`; execute `pane.set_title('my pane title')`; `pane.set_title('my π pane')`; assert `pane is not None`; `pane.pane_title == 'my pane title'`; `pane.pane_title == 'my π pane'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_set_title_special_characters_d0dffc4f6f | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_set_width | Verify Pane.set_width(); fixtures `session`; execute `session.new_window(window_name='test_set_width')`; `window.split()`; `window.select_layout('main-vertical')`; `pane1.set_width(10)`; `pane1.reset()`; assert `pane1 is not None`; `pane1.pane_width != pane1_width`; `pane1.pane_width is not None`; `int(pane1.pane_width) == 10`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_set_width_933585b385 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_split_3_7_flags[active_border_style] | Pane.split() tmux 3.7 styling/remain flags work on 3.7+; warn below; case inputs `kwargs={'active_border_style': 'fg=green'}`, `test_id='active_border_style'`; fixtures `session`; execute `session.new_window(window_name='split_37')`; `window.resize(height=40, width=80)`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.split(**kwargs)`; assert `new_pane in window.panes`; when `not (has_gte_version('3.7'))`: execute `pane.split(**kwargs)`; expect `pytest.warns(UserWarning, match='require tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_split_3_7_flags_active_border_style_2f0234c32f | 3.2a through 3.7b; conditional boundaries: 3.7 | planned parity | -| tests/test_pane.py::test_split_3_7_flags[inactive_border_style] | Pane.split() tmux 3.7 styling/remain flags work on 3.7+; warn below; case inputs `kwargs={'inactive_border_style': 'fg=blue'}`, `test_id='inactive_border_style'`; fixtures `session`; execute `session.new_window(window_name='split_37')`; `window.resize(height=40, width=80)`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.split(**kwargs)`; assert `new_pane in window.panes`; when `not (has_gte_version('3.7'))`: execute `pane.split(**kwargs)`; expect `pytest.warns(UserWarning, match='require tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_split_3_7_flags_inactive_border_style_8111dd8d5a | 3.2a through 3.7b; conditional boundaries: 3.7 | planned parity | -| tests/test_pane.py::test_split_3_7_flags[keep] | Pane.split() tmux 3.7 styling/remain flags work on 3.7+; warn below; case inputs `kwargs={'keep': True}`, `test_id='keep'`; fixtures `session`; execute `session.new_window(window_name='split_37')`; `window.resize(height=40, width=80)`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.split(**kwargs)`; assert `new_pane in window.panes`; when `not (has_gte_version('3.7'))`: execute `pane.split(**kwargs)`; expect `pytest.warns(UserWarning, match='require tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_split_3_7_flags_keep_6730641a46 | 3.2a through 3.7b; conditional boundaries: 3.7 | planned parity | -| tests/test_pane.py::test_split_3_7_flags[message] | Pane.split() tmux 3.7 styling/remain flags work on 3.7+; warn below; case inputs `kwargs={'message': 'bye'}`, `test_id='message'`; fixtures `session`; execute `session.new_window(window_name='split_37')`; `window.resize(height=40, width=80)`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.split(**kwargs)`; assert `new_pane in window.panes`; when `not (has_gte_version('3.7'))`: execute `pane.split(**kwargs)`; expect `pytest.warns(UserWarning, match='require tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_split_3_7_flags_message_200fa89609 | 3.2a through 3.7b; conditional boundaries: 3.7 | planned parity | -| tests/test_pane.py::test_split_3_7_flags[style] | Pane.split() tmux 3.7 styling/remain flags work on 3.7+; warn below; case inputs `kwargs={'style': 'bg=red'}`, `test_id='style'`; fixtures `session`; execute `session.new_window(window_name='split_37')`; `window.resize(height=40, width=80)`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.split(**kwargs)`; assert `new_pane in window.panes`; when `not (has_gte_version('3.7'))`: execute `pane.split(**kwargs)`; expect `pytest.warns(UserWarning, match='require tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_split_3_7_flags_style_b2764e49dd | 3.2a through 3.7b; conditional boundaries: 3.7 | planned parity | -| tests/test_pane.py::test_split_empty | Pane.split(empty=True) creates an empty pane on tmux 3.7+; fixtures `session`; execute `session.new_window(window_name='split_empty')`; `window.resize(height=40, width=80)`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.split(empty=True)`; assert `new_pane in window.panes`; `len(window.panes) == 2`; when `not (has_gte_version('3.7'))`: execute `pane.split(empty=True)`; expect `pytest.warns(UserWarning, match='empty requires tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_split_empty_a941382146 | 3.2a through 3.7b; conditional boundaries: 3.7 | planned parity | -| tests/test_pane.py::test_split_pane_size | Pane.split(); fixtures `session`; execute `session.new_window(window_name='split window size')`; `window.resize(height=100, width=100)`; `pane.split(size=10)`; `pane.split(direction=PaneDirection.Right, size=10)`; `pane.split(size='10%')`; `short_pane.split(direction=PaneDirection.Right, size='10%')`; assert `pane is not None`; `short_pane.pane_height == '10'`; `short_pane.at_left`; `short_pane.at_right`; `not short_pane.at_top`; `short_pane.at_bottom`; `narrow_pane.pane_width == '10'`; `not narrow_pane.at_left`; `narrow_pane.at_right`; `narrow_pane.at_top`; `not narrow_pane.at_bottom`; `new_pane.pane_height == '8'`; `new_pane.pane_width == '10'`; `not new_pane.at_left`; `new_pane.at_right`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_split_pane_size_7b189df9db | 3.2a through 3.7b | planned parity | +| tests/test_pane.py::test_send_keys_flags[repeat_count] | Test send_keys() with various flag combinations; case inputs `expected_in_capture='aaa'`, `key='a'`, `kwargs={'repeat': 3, 'literal': True, 'enter': False}`, `min_tmux_version=None`, `not_expected_in_capture=None`, `test_id='repeat_count'`; fixtures `session`; execute `session.new_window(window_name=f'sk_{test_id[:15]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(key, **kwargs)`; `shutil.which('env')`; `retry_until(lambda: '$' in '\n'.join(pane.capture_pane()), 2, raises=True)`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_in_capture is not None`: execute `pane.capture_pane()`; `retry_until(lambda: expected_in_capture in '\n'.join(pane.capture_pane()), 3, raises=True)`; `'\n'.join(pane.capture_pane())`; when `not_expected_in_capture is not None`: execute `pane.capture_pane()`; `'\n'.join(pane.capture_pane())`; assert `not_expected_in_capture not in contents`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_send_keys_flags_repeat_count_c3148b7d12 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_send_keys_flags[reset_terminal] | Test send_keys() with various flag combinations; case inputs `expected_in_capture=None`, `key=''`, `kwargs={'reset': True, 'enter': False}`, `min_tmux_version=None`, `not_expected_in_capture=None`, `test_id='reset_terminal'`; fixtures `session`; execute `session.new_window(window_name=f'sk_{test_id[:15]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(key, **kwargs)`; `shutil.which('env')`; `retry_until(lambda: '$' in '\n'.join(pane.capture_pane()), 2, raises=True)`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_in_capture is not None`: execute `pane.capture_pane()`; `retry_until(lambda: expected_in_capture in '\n'.join(pane.capture_pane()), 3, raises=True)`; `'\n'.join(pane.capture_pane())`; when `not_expected_in_capture is not None`: execute `pane.capture_pane()`; `'\n'.join(pane.capture_pane())`; assert `not_expected_in_capture not in contents`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_send_keys_flags_reset_terminal_20ca6419bf | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_send_prefix | Test Pane.send_prefix() sends prefix key without error; fixtures `session`; execute `pane.send_prefix()`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_send_prefix_6786722605 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_set_height | Verify Pane.set_height(); fixtures `session`; execute `session.new_window(window_name='test_set_height')`; `window.split()`; `pane1.set_height(4)`; assert `pane1 is not None`; `pane1.pane_height != pane1_height`; `pane1.pane_height is not None`; `int(pane1.pane_height) == 4`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_set_height_1a4e68d297 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_set_title | Test Pane.set_title() sets pane title; fixtures `session`; execute `pane.set_title('test-title')`; assert `pane is not None`; `result == pane`; `pane.pane_title == 'test-title'`; `pane.title == 'test-title'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_set_title_419f2ad1a8 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_set_title_special_characters | Test Pane.set_title() with spaces and unicode; fixtures `session`; execute `pane.set_title('my pane title')`; `pane.set_title('my π pane')`; assert `pane is not None`; `pane.pane_title == 'my pane title'`; `pane.pane_title == 'my π pane'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_set_title_special_characters_d0dffc4f6f | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_set_width | Verify Pane.set_width(); fixtures `session`; execute `session.new_window(window_name='test_set_width')`; `window.split()`; `window.select_layout('main-vertical')`; `pane1.set_width(10)`; `pane1.reset()`; assert `pane1 is not None`; `pane1.pane_width != pane1_width`; `pane1.pane_width is not None`; `int(pane1.pane_width) == 10`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_set_width_933585b385 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_split_3_7_flags[active_border_style] | Pane.split() tmux 3.7 styling/remain flags work on 3.7+; warn below; case inputs `kwargs={'active_border_style': 'fg=green'}`, `test_id='active_border_style'`; fixtures `session`; execute `session.new_window(window_name='split_37')`; `window.resize(height=40, width=80)`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.split(**kwargs)`; assert `new_pane in window.panes`; when `not (has_gte_version('3.7'))`: execute `pane.split(**kwargs)`; expect `pytest.warns(UserWarning, match='require tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_split_3_7_flags_active_border_style_2f0234c32f | 3.3a through 3.7c; conditional boundaries: 3.7 | planned parity | +| tests/test_pane.py::test_split_3_7_flags[inactive_border_style] | Pane.split() tmux 3.7 styling/remain flags work on 3.7+; warn below; case inputs `kwargs={'inactive_border_style': 'fg=blue'}`, `test_id='inactive_border_style'`; fixtures `session`; execute `session.new_window(window_name='split_37')`; `window.resize(height=40, width=80)`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.split(**kwargs)`; assert `new_pane in window.panes`; when `not (has_gte_version('3.7'))`: execute `pane.split(**kwargs)`; expect `pytest.warns(UserWarning, match='require tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_split_3_7_flags_inactive_border_style_8111dd8d5a | 3.3a through 3.7c; conditional boundaries: 3.7 | planned parity | +| tests/test_pane.py::test_split_3_7_flags[keep] | Pane.split() tmux 3.7 styling/remain flags work on 3.7+; warn below; case inputs `kwargs={'keep': True}`, `test_id='keep'`; fixtures `session`; execute `session.new_window(window_name='split_37')`; `window.resize(height=40, width=80)`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.split(**kwargs)`; assert `new_pane in window.panes`; when `not (has_gte_version('3.7'))`: execute `pane.split(**kwargs)`; expect `pytest.warns(UserWarning, match='require tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_split_3_7_flags_keep_6730641a46 | 3.3a through 3.7c; conditional boundaries: 3.7 | planned parity | +| tests/test_pane.py::test_split_3_7_flags[message] | Pane.split() tmux 3.7 styling/remain flags work on 3.7+; warn below; case inputs `kwargs={'message': 'bye'}`, `test_id='message'`; fixtures `session`; execute `session.new_window(window_name='split_37')`; `window.resize(height=40, width=80)`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.split(**kwargs)`; assert `new_pane in window.panes`; when `not (has_gte_version('3.7'))`: execute `pane.split(**kwargs)`; expect `pytest.warns(UserWarning, match='require tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_split_3_7_flags_message_200fa89609 | 3.3a through 3.7c; conditional boundaries: 3.7 | planned parity | +| tests/test_pane.py::test_split_3_7_flags[style] | Pane.split() tmux 3.7 styling/remain flags work on 3.7+; warn below; case inputs `kwargs={'style': 'bg=red'}`, `test_id='style'`; fixtures `session`; execute `session.new_window(window_name='split_37')`; `window.resize(height=40, width=80)`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.split(**kwargs)`; assert `new_pane in window.panes`; when `not (has_gte_version('3.7'))`: execute `pane.split(**kwargs)`; expect `pytest.warns(UserWarning, match='require tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_split_3_7_flags_style_b2764e49dd | 3.3a through 3.7c; conditional boundaries: 3.7 | planned parity | +| tests/test_pane.py::test_split_empty | Pane.split(empty=True) creates an empty pane on tmux 3.7+; fixtures `session`; execute `session.new_window(window_name='split_empty')`; `window.resize(height=40, width=80)`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.split(empty=True)`; assert `new_pane in window.panes`; `len(window.panes) == 2`; when `not (has_gte_version('3.7'))`: execute `pane.split(empty=True)`; expect `pytest.warns(UserWarning, match='empty requires tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_split_empty_a941382146 | 3.3a through 3.7c; conditional boundaries: 3.7 | planned parity | +| tests/test_pane.py::test_split_pane_size | Pane.split(); fixtures `session`; execute `session.new_window(window_name='split window size')`; `window.resize(height=100, width=100)`; `pane.split(size=10)`; `pane.split(direction=PaneDirection.Right, size=10)`; `pane.split(size='10%')`; `short_pane.split(direction=PaneDirection.Right, size='10%')`; assert `pane is not None`; `short_pane.pane_height == '10'`; `short_pane.at_left`; `short_pane.at_right`; `not short_pane.at_top`; `short_pane.at_bottom`; `narrow_pane.pane_width == '10'`; `not narrow_pane.at_left`; `narrow_pane.at_right`; `narrow_pane.at_top`; `not narrow_pane.at_bottom`; `new_pane.pane_height == '8'`; `new_pane.pane_width == '10'`; `not new_pane.at_left`; `new_pane.at_right`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_split_pane_size_7b189df9db | 3.3a through 3.7c | planned parity | | tests/test_pane.py::test_split_percentage | Test Pane.split() with percentage parameter; fixtures `session`; execute `session.new_window(window_name='test_split_pct')`; `window.resize(height=40, width=80)`; `pane.split(percentage=25)`; `new_pane.refresh()`; assert `pane is not None`; `new_pane in window.panes`; `len(window.panes) == 2`; `new_pane.pane_height is not None`; `int(new_pane.pane_height) <= 15`; when `not has_gte_version('3.5')`: skip via `pytest.skip('split-window -p was broken in tmux 3.4 (fixed in 3.5)')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_split_percentage_ed7d56084d | 3.5 through 3.7b | planned parity | -| tests/test_pane.py::test_split_percentage_size_mutual_exclusion | Test that size and percentage are mutually exclusive; fixtures `session`; execute `session.new_window(window_name='test_split_mutex')`; `pane.split(size=10, percentage=50)`; expect `pytest.raises(ValueError, match='Cannot specify both')`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_split_percentage_size_mutual_exclusion_5c93d7dcfe | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_split_preserves_empty_option_values[active_border_style] | Pane.split() emits empty value flags as a separate argv entry; case inputs `option_kwargs={'active_border_style': ''}`, `test_id='active_border_style'`; fixtures `session`; execute `session.new_window(window_name=f'split_empty_{test_id}')`; `window.resize(height=40, width=120)`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.split(**option_kwargs)`; assert `new_pane in window.panes`; when `not (has_gte_version('3.7'))`: execute `pane.split(**option_kwargs)`; expect `pytest.warns(UserWarning, match='require tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#est_pane_py_test_split_preserves_empty_option_values_active_border_style_8191759322 | 3.2a through 3.7b; conditional boundaries: 3.7 | planned parity | -| tests/test_pane.py::test_split_preserves_empty_option_values[inactive_border_style] | Pane.split() emits empty value flags as a separate argv entry; case inputs `option_kwargs={'inactive_border_style': ''}`, `test_id='inactive_border_style'`; fixtures `session`; execute `session.new_window(window_name=f'split_empty_{test_id}')`; `window.resize(height=40, width=120)`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.split(**option_kwargs)`; assert `new_pane in window.panes`; when `not (has_gte_version('3.7'))`: execute `pane.split(**option_kwargs)`; expect `pytest.warns(UserWarning, match='require tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#t_pane_py_test_split_preserves_empty_option_values_inactive_border_style_2b3f5b8d5a | 3.2a through 3.7b; conditional boundaries: 3.7 | planned parity | -| tests/test_pane.py::test_split_preserves_empty_option_values[message] | Pane.split() emits empty value flags as a separate argv entry; case inputs `option_kwargs={'message': ''}`, `test_id='message'`; fixtures `session`; execute `session.new_window(window_name=f'split_empty_{test_id}')`; `window.resize(height=40, width=120)`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.split(**option_kwargs)`; assert `new_pane in window.panes`; when `not (has_gte_version('3.7'))`: execute `pane.split(**option_kwargs)`; expect `pytest.warns(UserWarning, match='require tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_split_preserves_empty_option_values_message_751487eb9d | 3.2a through 3.7b; conditional boundaries: 3.7 | planned parity | -| tests/test_pane.py::test_split_preserves_empty_option_values[style] | Pane.split() emits empty value flags as a separate argv entry; case inputs `option_kwargs={'style': ''}`, `test_id='style'`; fixtures `session`; execute `session.new_window(window_name=f'split_empty_{test_id}')`; `window.resize(height=40, width=120)`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.split(**option_kwargs)`; assert `new_pane in window.panes`; when `not (has_gte_version('3.7'))`: execute `pane.split(**option_kwargs)`; expect `pytest.warns(UserWarning, match='require tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_split_preserves_empty_option_values_style_96cb897ab0 | 3.2a through 3.7b; conditional boundaries: 3.7 | planned parity | -| tests/test_pane.py::test_split_start_directory[empty_string] | Test Pane.split start_directory parameter handling; case inputs `description='Empty string should not add -c flag'`, `start_directory=''`, `test_id='empty_string'`; fixtures `session`, `monkeypatch`, `tmp_path`, `user_path`; execute `session.new_window(window_name=f'test_split_{test_id}')`; `pane.split(start_directory=actual_start_directory)`; assert `pane is not None`; `new_pane in window.panes`; `len(window.panes) == 2`; when `start_directory and str(start_directory) not in {'', 'None'} and not (f'{user_path}' in str(start_directory)) and str(start_directory).startswith('./')`: execute `temp_dir.mkdir(parents=True, exist_ok=True)`; `temp_dir.resolve()`; when `expected_path`: execute `new_pane.refresh()`; `pathlib.Path(new_pane.pane_current_path).resolve()`; `pathlib.Path(new_pane.pane_current_path)`; assert `new_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_split_start_directory_empty_string_1a004c8e32 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_split_start_directory[none_value] | Test Pane.split start_directory parameter handling; case inputs `description='None should not add -c flag'`, `start_directory=None`, `test_id='none_value'`; fixtures `session`, `monkeypatch`, `tmp_path`, `user_path`; execute `session.new_window(window_name=f'test_split_{test_id}')`; `pane.split(start_directory=actual_start_directory)`; assert `pane is not None`; `new_pane in window.panes`; `len(window.panes) == 2`; when `start_directory and str(start_directory) not in {'', 'None'} and not (f'{user_path}' in str(start_directory)) and str(start_directory).startswith('./')`: execute `temp_dir.mkdir(parents=True, exist_ok=True)`; `temp_dir.resolve()`; when `expected_path`: execute `new_pane.refresh()`; `pathlib.Path(new_pane.pane_current_path).resolve()`; `pathlib.Path(new_pane.pane_current_path)`; assert `new_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_split_start_directory_none_value_4f81075b4d | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_split_start_directory[relative_path] | Test Pane.split start_directory parameter handling; case inputs `description='Relative path should add -c flag'`, `start_directory='./relative/path'`, `test_id='relative_path'`; fixtures `session`, `monkeypatch`, `tmp_path`, `user_path`; execute `session.new_window(window_name=f'test_split_{test_id}')`; `pane.split(start_directory=actual_start_directory)`; assert `pane is not None`; `new_pane in window.panes`; `len(window.panes) == 2`; when `start_directory and str(start_directory) not in {'', 'None'} and not (f'{user_path}' in str(start_directory)) and str(start_directory).startswith('./')`: execute `temp_dir.mkdir(parents=True, exist_ok=True)`; `temp_dir.resolve()`; when `expected_path`: execute `new_pane.refresh()`; `pathlib.Path(new_pane.pane_current_path).resolve()`; `pathlib.Path(new_pane.pane_current_path)`; assert `new_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_split_start_directory_relative_path_56f7922798 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_split_start_directory[user_path] | Test Pane.split start_directory parameter handling; case inputs `description='User path should add -c flag'`, `start_directory='{user_path}'`, `test_id='user_path'`; fixtures `session`, `monkeypatch`, `tmp_path`, `user_path`; execute `session.new_window(window_name=f'test_split_{test_id}')`; `pane.split(start_directory=actual_start_directory)`; assert `pane is not None`; `new_pane in window.panes`; `len(window.panes) == 2`; when `start_directory and str(start_directory) not in {'', 'None'} and not (f'{user_path}' in str(start_directory)) and str(start_directory).startswith('./')`: execute `temp_dir.mkdir(parents=True, exist_ok=True)`; `temp_dir.resolve()`; when `expected_path`: execute `new_pane.refresh()`; `pathlib.Path(new_pane.pane_current_path).resolve()`; `pathlib.Path(new_pane.pane_current_path)`; assert `new_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_split_start_directory_user_path_c4843ae174 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_split_start_directory_pathlib | Test Pane.split accepts pathlib.Path for start_directory; fixtures `session`, `user_path`; execute `session.new_window(window_name='test_split_pathlib')`; `pane.split(start_directory=user_path)`; `user_path.resolve()`; `new_pane.refresh()`; `pathlib.Path(new_pane.pane_current_path).resolve()`; `pathlib.Path(new_pane.pane_current_path)`; assert `pane is not None`; `new_pane in window.panes`; `len(window.panes) == 2`; `new_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_split_start_directory_pathlib_ebe061c9ff | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_swap_pane | Test Pane.swap() swaps two panes; fixtures `session`; execute `session.new_window(window_name='test_swap_pane')`; `window.resize(height=40, width=80)`; `pane1.split()`; `pane1.refresh()`; `pane2.refresh()`; `pane1.swap(pane2)`; assert `pane1 is not None`; `pane1.pane_index == pane2_idx`; `pane2.pane_index == pane1_idx`; `pane1.pane_id == pane1_id`; `pane2.pane_id == pane2_id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_swap_pane_2a51c3efd2 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_swap_pane_invalid_args[kwargs0-target or move_up] | Pane.swap() rejects missing or conflicting arguments; case inputs `kwargs={}`, `match='target or move_up'`; fixtures `session`; execute `pane.swap(**kwargs)`; expect `pytest.raises(exc.LibTmuxException, match=match)`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_swap_pane_invalid_args_kwargs0_target_or_move_up_59c9ccf38e | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_swap_pane_invalid_args[kwargs1-mutually exclusive] | Pane.swap() rejects missing or conflicting arguments; case inputs `kwargs={'move_up': True, 'move_down': True}`, `match='mutually exclusive'`; fixtures `session`; execute `pane.swap(**kwargs)`; expect `pytest.raises(exc.LibTmuxException, match=match)`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_pane_py_test_swap_pane_invalid_args_kwargs1_mutually_exclusive_0ce8e5bc8c | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_swap_pane_invalid_args[kwargs2-mutually exclusive] | Pane.swap() rejects missing or conflicting arguments; case inputs `kwargs={'move_up': True, 'target': '%0'}`, `match='mutually exclusive'`; fixtures `session`; execute `pane.swap(**kwargs)`; expect `pytest.raises(exc.LibTmuxException, match=match)`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_pane_py_test_swap_pane_invalid_args_kwargs2_mutually_exclusive_b2cf295c41 | 3.2a through 3.7b | planned parity | -| tests/test_pane.py::test_swap_pane_move_up_down | Pane.swap(move_up=True) / (move_down=True) work without a target; fixtures `session`; execute `session.new_window(window_name='test_swap_move')`; `window.resize(height=40, width=80)`; `pane1.split()`; `pane1.refresh()`; `pane2.refresh()`; `pane1.swap(move_down=True)`; assert `pane1 is not None`; `pane1.pane_index == pane2_idx`; `pane2.pane_index == pane1_idx`; `pane1.pane_index == pane1_idx`; `pane2.pane_index == pane2_idx`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_swap_pane_move_up_down_2aa558c99e | 3.2a through 3.7b | planned parity | -| tests/test_pane_capture_pane.py::test_capture_pane_backward_compatible | Test that capture_pane() works without any new parameters; fixtures `session`; execute `session.active_window.split(shell='sh')`; `pane.capture_pane()`; `pane.send_keys('echo "backward compat test"', enter=True)`; `'\n'.join(pane.capture_pane())`; `retry_until(prompt_ready, 2, raises=True)`; `retry_until(output_ready, 2, raises=True)`; assert `isinstance(output, list)`; `any(('backward compat test' in line for line in output))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_capture_pane_py_test_capture_pane_backward_compatible_dadcc36ef4 | 3.2a through 3.7b | planned parity | -| tests/test_pane_capture_pane.py::test_capture_pane_flag_smoke[alternate_screen] | Smoke-test capture_pane flags that aren't tied to output content; case inputs `kwargs={'alternate_screen': True}`, `min_tmux_version=None`; fixtures `session`; execute `pane.capture_pane(**kwargs)`; assert `pane is not None`; `isinstance(result, list)`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#_test_pane_capture_pane_py_test_capture_pane_flag_smoke_alternate_screen_779ce81a87 | 3.2a through 3.7b | planned parity | +| tests/test_pane.py::test_split_percentage_size_mutual_exclusion | Test that size and percentage are mutually exclusive; fixtures `session`; execute `session.new_window(window_name='test_split_mutex')`; `pane.split(size=10, percentage=50)`; expect `pytest.raises(ValueError, match='Cannot specify both')`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_split_percentage_size_mutual_exclusion_5c93d7dcfe | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_split_preserves_empty_option_values[active_border_style] | Pane.split() emits empty value flags as a separate argv entry; case inputs `option_kwargs={'active_border_style': ''}`, `test_id='active_border_style'`; fixtures `session`; execute `session.new_window(window_name=f'split_empty_{test_id}')`; `window.resize(height=40, width=120)`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.split(**option_kwargs)`; assert `new_pane in window.panes`; when `not (has_gte_version('3.7'))`: execute `pane.split(**option_kwargs)`; expect `pytest.warns(UserWarning, match='require tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#est_pane_py_test_split_preserves_empty_option_values_active_border_style_8191759322 | 3.3a through 3.7c; conditional boundaries: 3.7 | planned parity | +| tests/test_pane.py::test_split_preserves_empty_option_values[inactive_border_style] | Pane.split() emits empty value flags as a separate argv entry; case inputs `option_kwargs={'inactive_border_style': ''}`, `test_id='inactive_border_style'`; fixtures `session`; execute `session.new_window(window_name=f'split_empty_{test_id}')`; `window.resize(height=40, width=120)`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.split(**option_kwargs)`; assert `new_pane in window.panes`; when `not (has_gte_version('3.7'))`: execute `pane.split(**option_kwargs)`; expect `pytest.warns(UserWarning, match='require tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#t_pane_py_test_split_preserves_empty_option_values_inactive_border_style_2b3f5b8d5a | 3.3a through 3.7c; conditional boundaries: 3.7 | planned parity | +| tests/test_pane.py::test_split_preserves_empty_option_values[message] | Pane.split() emits empty value flags as a separate argv entry; case inputs `option_kwargs={'message': ''}`, `test_id='message'`; fixtures `session`; execute `session.new_window(window_name=f'split_empty_{test_id}')`; `window.resize(height=40, width=120)`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.split(**option_kwargs)`; assert `new_pane in window.panes`; when `not (has_gte_version('3.7'))`: execute `pane.split(**option_kwargs)`; expect `pytest.warns(UserWarning, match='require tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_split_preserves_empty_option_values_message_751487eb9d | 3.3a through 3.7c; conditional boundaries: 3.7 | planned parity | +| tests/test_pane.py::test_split_preserves_empty_option_values[style] | Pane.split() emits empty value flags as a separate argv entry; case inputs `option_kwargs={'style': ''}`, `test_id='style'`; fixtures `session`; execute `session.new_window(window_name=f'split_empty_{test_id}')`; `window.resize(height=40, width=120)`; assert `pane is not None`; when `has_gte_version('3.7')`: execute `pane.split(**option_kwargs)`; assert `new_pane in window.panes`; when `not (has_gte_version('3.7'))`: execute `pane.split(**option_kwargs)`; expect `pytest.warns(UserWarning, match='require tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_split_preserves_empty_option_values_style_96cb897ab0 | 3.3a through 3.7c; conditional boundaries: 3.7 | planned parity | +| tests/test_pane.py::test_split_start_directory[empty_string] | Test Pane.split start_directory parameter handling; case inputs `description='Empty string should not add -c flag'`, `start_directory=''`, `test_id='empty_string'`; fixtures `session`, `monkeypatch`, `tmp_path`, `user_path`; execute `session.new_window(window_name=f'test_split_{test_id}')`; `pane.split(start_directory=actual_start_directory)`; assert `pane is not None`; `new_pane in window.panes`; `len(window.panes) == 2`; when `start_directory and str(start_directory) not in {'', 'None'} and not (f'{user_path}' in str(start_directory)) and str(start_directory).startswith('./')`: execute `temp_dir.mkdir(parents=True, exist_ok=True)`; `temp_dir.resolve()`; when `expected_path`: execute `new_pane.refresh()`; `pathlib.Path(new_pane.pane_current_path).resolve()`; `pathlib.Path(new_pane.pane_current_path)`; assert `new_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_split_start_directory_empty_string_1a004c8e32 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_split_start_directory[none_value] | Test Pane.split start_directory parameter handling; case inputs `description='None should not add -c flag'`, `start_directory=None`, `test_id='none_value'`; fixtures `session`, `monkeypatch`, `tmp_path`, `user_path`; execute `session.new_window(window_name=f'test_split_{test_id}')`; `pane.split(start_directory=actual_start_directory)`; assert `pane is not None`; `new_pane in window.panes`; `len(window.panes) == 2`; when `start_directory and str(start_directory) not in {'', 'None'} and not (f'{user_path}' in str(start_directory)) and str(start_directory).startswith('./')`: execute `temp_dir.mkdir(parents=True, exist_ok=True)`; `temp_dir.resolve()`; when `expected_path`: execute `new_pane.refresh()`; `pathlib.Path(new_pane.pane_current_path).resolve()`; `pathlib.Path(new_pane.pane_current_path)`; assert `new_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_split_start_directory_none_value_4f81075b4d | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_split_start_directory[relative_path] | Test Pane.split start_directory parameter handling; case inputs `description='Relative path should add -c flag'`, `start_directory='./relative/path'`, `test_id='relative_path'`; fixtures `session`, `monkeypatch`, `tmp_path`, `user_path`; execute `session.new_window(window_name=f'test_split_{test_id}')`; `pane.split(start_directory=actual_start_directory)`; assert `pane is not None`; `new_pane in window.panes`; `len(window.panes) == 2`; when `start_directory and str(start_directory) not in {'', 'None'} and not (f'{user_path}' in str(start_directory)) and str(start_directory).startswith('./')`: execute `temp_dir.mkdir(parents=True, exist_ok=True)`; `temp_dir.resolve()`; when `expected_path`: execute `new_pane.refresh()`; `pathlib.Path(new_pane.pane_current_path).resolve()`; `pathlib.Path(new_pane.pane_current_path)`; assert `new_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_split_start_directory_relative_path_56f7922798 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_split_start_directory[user_path] | Test Pane.split start_directory parameter handling; case inputs `description='User path should add -c flag'`, `start_directory='{user_path}'`, `test_id='user_path'`; fixtures `session`, `monkeypatch`, `tmp_path`, `user_path`; execute `session.new_window(window_name=f'test_split_{test_id}')`; `pane.split(start_directory=actual_start_directory)`; assert `pane is not None`; `new_pane in window.panes`; `len(window.panes) == 2`; when `start_directory and str(start_directory) not in {'', 'None'} and not (f'{user_path}' in str(start_directory)) and str(start_directory).startswith('./')`: execute `temp_dir.mkdir(parents=True, exist_ok=True)`; `temp_dir.resolve()`; when `expected_path`: execute `new_pane.refresh()`; `pathlib.Path(new_pane.pane_current_path).resolve()`; `pathlib.Path(new_pane.pane_current_path)`; assert `new_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_split_start_directory_user_path_c4843ae174 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_split_start_directory_pathlib | Test Pane.split accepts pathlib.Path for start_directory; fixtures `session`, `user_path`; execute `session.new_window(window_name='test_split_pathlib')`; `pane.split(start_directory=user_path)`; `user_path.resolve()`; `new_pane.refresh()`; `pathlib.Path(new_pane.pane_current_path).resolve()`; `pathlib.Path(new_pane.pane_current_path)`; assert `pane is not None`; `new_pane in window.panes`; `len(window.panes) == 2`; `new_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_split_start_directory_pathlib_ebe061c9ff | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_swap_pane | Test Pane.swap() swaps two panes; fixtures `session`; execute `session.new_window(window_name='test_swap_pane')`; `window.resize(height=40, width=80)`; `pane1.split()`; `pane1.refresh()`; `pane2.refresh()`; `pane1.swap(pane2)`; assert `pane1 is not None`; `pane1.pane_index == pane2_idx`; `pane2.pane_index == pane1_idx`; `pane1.pane_id == pane1_id`; `pane2.pane_id == pane2_id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_swap_pane_2a51c3efd2 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_swap_pane_invalid_args[kwargs0-target or move_up] | Pane.swap() rejects missing or conflicting arguments; case inputs `kwargs={}`, `match='target or move_up'`; fixtures `session`; execute `pane.swap(**kwargs)`; expect `pytest.raises(exc.LibTmuxException, match=match)`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_swap_pane_invalid_args_kwargs0_target_or_move_up_59c9ccf38e | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_swap_pane_invalid_args[kwargs1-mutually exclusive] | Pane.swap() rejects missing or conflicting arguments; case inputs `kwargs={'move_up': True, 'move_down': True}`, `match='mutually exclusive'`; fixtures `session`; execute `pane.swap(**kwargs)`; expect `pytest.raises(exc.LibTmuxException, match=match)`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_pane_py_test_swap_pane_invalid_args_kwargs1_mutually_exclusive_0ce8e5bc8c | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_swap_pane_invalid_args[kwargs2-mutually exclusive] | Pane.swap() rejects missing or conflicting arguments; case inputs `kwargs={'move_up': True, 'target': '%0'}`, `match='mutually exclusive'`; fixtures `session`; execute `pane.swap(**kwargs)`; expect `pytest.raises(exc.LibTmuxException, match=match)`; assert `pane is not None`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_pane_py_test_swap_pane_invalid_args_kwargs2_mutually_exclusive_b2cf295c41 | 3.3a through 3.7c | planned parity | +| tests/test_pane.py::test_swap_pane_move_up_down | Pane.swap(move_up=True) / (move_down=True) work without a target; fixtures `session`; execute `session.new_window(window_name='test_swap_move')`; `window.resize(height=40, width=80)`; `pane1.split()`; `pane1.refresh()`; `pane2.refresh()`; `pane1.swap(move_down=True)`; assert `pane1 is not None`; `pane1.pane_index == pane2_idx`; `pane2.pane_index == pane1_idx`; `pane1.pane_index == pane1_idx`; `pane2.pane_index == pane2_idx`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_py_test_swap_pane_move_up_down_2aa558c99e | 3.3a through 3.7c | planned parity | +| tests/test_pane_capture_pane.py::test_capture_pane_backward_compatible | Test that capture_pane() works without any new parameters; fixtures `session`; execute `session.active_window.split(shell='sh')`; `pane.capture_pane()`; `pane.send_keys('echo "backward compat test"', enter=True)`; `'\n'.join(pane.capture_pane())`; `retry_until(prompt_ready, 2, raises=True)`; `retry_until(output_ready, 2, raises=True)`; assert `isinstance(output, list)`; `any(('backward compat test' in line for line in output))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_capture_pane_py_test_capture_pane_backward_compatible_dadcc36ef4 | 3.3a through 3.7c | planned parity | +| tests/test_pane_capture_pane.py::test_capture_pane_flag_smoke[alternate_screen] | Smoke-test capture_pane flags that aren't tied to output content; case inputs `kwargs={'alternate_screen': True}`, `min_tmux_version=None`; fixtures `session`; execute `pane.capture_pane(**kwargs)`; assert `pane is not None`; `isinstance(result, list)`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#_test_pane_capture_pane_py_test_capture_pane_flag_smoke_alternate_screen_779ce81a87 | 3.3a through 3.7c | planned parity | | tests/test_pane_capture_pane.py::test_capture_pane_flag_smoke[mode_screen_v36] | Smoke-test capture_pane flags that aren't tied to output content; case inputs `kwargs={'mode_screen': True}`, `min_tmux_version='3.6'`; fixtures `session`; execute `pane.capture_pane(**kwargs)`; assert `pane is not None`; `isinstance(result, list)`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#s_test_pane_capture_pane_py_test_capture_pane_flag_smoke_mode_screen_v36_5627da7a9c | 3.6 through 3.7b | planned parity | -| tests/test_pane_capture_pane.py::test_capture_pane_flag_smoke[quiet] | Smoke-test capture_pane flags that aren't tied to output content; case inputs `kwargs={'quiet': True}`, `min_tmux_version=None`; fixtures `session`; execute `pane.capture_pane(**kwargs)`; assert `pane is not None`; `isinstance(result, list)`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_capture_pane_py_test_capture_pane_flag_smoke_quiet_6f1c8513d6 | 3.2a through 3.7b | planned parity | -| tests/test_pane_capture_pane.py::test_capture_pane_flags[all_flags_except_trim] | Test capture_pane() with various flag combinations; case inputs `command='printf "\\033[34mBLUE: %s \\033[0m" "test"'`, `escape_non_printable=True`, `escape_sequences=True`, `expected_pattern='\\x1b\\[34m|BLUE'`, `join_wrapped=True`, `min_tmux_version=None`, `not_expected_pattern=None`, `preserve_trailing=True`, `test_id='all_flags_except_trim'`, `trim_trailing=False`; fixtures `session`; execute `session.new_window(attach=True, window_name=f'cap_{test_id[:10]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(full_command, literal=False, suppress_history=False)`; `pane.capture_pane(escape_sequences=escape_sequences, escape_non_printable=escape_non_printable, join_wrapped=join_wrapped, preserve_trailing=preserve_trailing, trim_trailing=trim_trailing)`; `shutil.which('env')`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_pattern`: execute `re.search(expected_pattern, output_str, re.DOTALL)`; assert `re.search(expected_pattern, output_str, re.DOTALL); message=f"Expected pattern '{expected_pattern}' not found in output:\n{output_str}"`; when `not_expected_pattern`: execute `re.search(not_expected_pattern, output_str, re.DOTALL)`; assert `not re.search(not_expected_pattern, output_str, re.DOTALL); message=f"Unexpected pattern '{not_expected_pattern}' found in output"`. | real-tmux integration test | PythonBehaviorParityTest#_test_pane_capture_pane_py_test_capture_pane_flags_all_flags_except_trim_922d38ac76 | 3.2a through 3.7b | planned parity | -| tests/test_pane_capture_pane.py::test_capture_pane_flags[basic_capture] | Test capture_pane() with various flag combinations; case inputs `command='echo "hello world"'`, `escape_non_printable=False`, `escape_sequences=False`, `expected_pattern='hello world'`, `join_wrapped=False`, `min_tmux_version=None`, `not_expected_pattern=None`, `preserve_trailing=False`, `test_id='basic_capture'`, `trim_trailing=False`; fixtures `session`; execute `session.new_window(attach=True, window_name=f'cap_{test_id[:10]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(full_command, literal=False, suppress_history=False)`; `pane.capture_pane(escape_sequences=escape_sequences, escape_non_printable=escape_non_printable, join_wrapped=join_wrapped, preserve_trailing=preserve_trailing, trim_trailing=trim_trailing)`; `shutil.which('env')`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_pattern`: execute `re.search(expected_pattern, output_str, re.DOTALL)`; assert `re.search(expected_pattern, output_str, re.DOTALL); message=f"Expected pattern '{expected_pattern}' not found in output:\n{output_str}"`; when `not_expected_pattern`: execute `re.search(not_expected_pattern, output_str, re.DOTALL)`; assert `not re.search(not_expected_pattern, output_str, re.DOTALL); message=f"Unexpected pattern '{not_expected_pattern}' found in output"`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_capture_pane_py_test_capture_pane_flags_basic_capture_c13b79057e | 3.2a through 3.7b | planned parity | -| tests/test_pane_capture_pane.py::test_capture_pane_flags[basic_multiline] | Test capture_pane() with various flag combinations; case inputs `command='printf "line1\\nline2\\nline3\\n"'`, `escape_non_printable=False`, `escape_sequences=False`, `expected_pattern='line1.*line2.*line3'`, `join_wrapped=False`, `min_tmux_version=None`, `not_expected_pattern=None`, `preserve_trailing=False`, `test_id='basic_multiline'`, `trim_trailing=False`; fixtures `session`; execute `session.new_window(attach=True, window_name=f'cap_{test_id[:10]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(full_command, literal=False, suppress_history=False)`; `pane.capture_pane(escape_sequences=escape_sequences, escape_non_printable=escape_non_printable, join_wrapped=join_wrapped, preserve_trailing=preserve_trailing, trim_trailing=trim_trailing)`; `shutil.which('env')`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_pattern`: execute `re.search(expected_pattern, output_str, re.DOTALL)`; assert `re.search(expected_pattern, output_str, re.DOTALL); message=f"Expected pattern '{expected_pattern}' not found in output:\n{output_str}"`; when `not_expected_pattern`: execute `re.search(not_expected_pattern, output_str, re.DOTALL)`; assert `not re.search(not_expected_pattern, output_str, re.DOTALL); message=f"Unexpected pattern '{not_expected_pattern}' found in output"`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_capture_pane_py_test_capture_pane_flags_basic_multiline_faf8bbdc1a | 3.2a through 3.7b | planned parity | -| tests/test_pane_capture_pane.py::test_capture_pane_flags[escape_non_printable_basic] | Test capture_pane() with various flag combinations; case inputs `command='printf "\\001\\002\\003"'`, `escape_non_printable=True`, `escape_sequences=False`, `expected_pattern='\\\\00[123]'`, `join_wrapped=False`, `min_tmux_version=None`, `not_expected_pattern=None`, `preserve_trailing=False`, `test_id='escape_non_printable_basic'`, `trim_trailing=False`; fixtures `session`; execute `session.new_window(attach=True, window_name=f'cap_{test_id[:10]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(full_command, literal=False, suppress_history=False)`; `pane.capture_pane(escape_sequences=escape_sequences, escape_non_printable=escape_non_printable, join_wrapped=join_wrapped, preserve_trailing=preserve_trailing, trim_trailing=trim_trailing)`; `shutil.which('env')`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_pattern`: execute `re.search(expected_pattern, output_str, re.DOTALL)`; assert `re.search(expected_pattern, output_str, re.DOTALL); message=f"Expected pattern '{expected_pattern}' not found in output:\n{output_str}"`; when `not_expected_pattern`: execute `re.search(not_expected_pattern, output_str, re.DOTALL)`; assert `not re.search(not_expected_pattern, output_str, re.DOTALL); message=f"Unexpected pattern '{not_expected_pattern}' found in output"`. | real-tmux integration test | PythonBehaviorParityTest#_pane_capture_pane_py_test_capture_pane_flags_escape_non_printable_basic_469ed4ffd7 | 3.2a through 3.7b | planned parity | -| tests/test_pane_capture_pane.py::test_capture_pane_flags[escape_non_printable_tab] | Test capture_pane() with various flag combinations; case inputs `command='printf "a\\tb"'`, `escape_non_printable=True`, `escape_sequences=False`, `expected_pattern='a.*b'`, `join_wrapped=False`, `min_tmux_version=None`, `not_expected_pattern=None`, `preserve_trailing=False`, `test_id='escape_non_printable_tab'`, `trim_trailing=False`; fixtures `session`; execute `session.new_window(attach=True, window_name=f'cap_{test_id[:10]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(full_command, literal=False, suppress_history=False)`; `pane.capture_pane(escape_sequences=escape_sequences, escape_non_printable=escape_non_printable, join_wrapped=join_wrapped, preserve_trailing=preserve_trailing, trim_trailing=trim_trailing)`; `shutil.which('env')`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_pattern`: execute `re.search(expected_pattern, output_str, re.DOTALL)`; assert `re.search(expected_pattern, output_str, re.DOTALL); message=f"Expected pattern '{expected_pattern}' not found in output:\n{output_str}"`; when `not_expected_pattern`: execute `re.search(not_expected_pattern, output_str, re.DOTALL)`; assert `not re.search(not_expected_pattern, output_str, re.DOTALL); message=f"Unexpected pattern '{not_expected_pattern}' found in output"`. | real-tmux integration test | PythonBehaviorParityTest#st_pane_capture_pane_py_test_capture_pane_flags_escape_non_printable_tab_5898542c8c | 3.2a through 3.7b | planned parity | -| tests/test_pane_capture_pane.py::test_capture_pane_flags[escape_sequences_bold] | Test capture_pane() with various flag combinations; case inputs `command='printf "\\033[1mBOLD\\033[0m"'`, `escape_non_printable=False`, `escape_sequences=True`, `expected_pattern='\\x1b\\[1m'`, `join_wrapped=False`, `min_tmux_version=None`, `not_expected_pattern=None`, `preserve_trailing=False`, `test_id='escape_sequences_bold'`, `trim_trailing=False`; fixtures `session`; execute `session.new_window(attach=True, window_name=f'cap_{test_id[:10]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(full_command, literal=False, suppress_history=False)`; `pane.capture_pane(escape_sequences=escape_sequences, escape_non_printable=escape_non_printable, join_wrapped=join_wrapped, preserve_trailing=preserve_trailing, trim_trailing=trim_trailing)`; `shutil.which('env')`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_pattern`: execute `re.search(expected_pattern, output_str, re.DOTALL)`; assert `re.search(expected_pattern, output_str, re.DOTALL); message=f"Expected pattern '{expected_pattern}' not found in output:\n{output_str}"`; when `not_expected_pattern`: execute `re.search(not_expected_pattern, output_str, re.DOTALL)`; assert `not re.search(not_expected_pattern, output_str, re.DOTALL); message=f"Unexpected pattern '{not_expected_pattern}' found in output"`. | real-tmux integration test | PythonBehaviorParityTest#_test_pane_capture_pane_py_test_capture_pane_flags_escape_sequences_bold_da0623516d | 3.2a through 3.7b | planned parity | -| tests/test_pane_capture_pane.py::test_capture_pane_flags[escape_sequences_green] | Test capture_pane() with various flag combinations; case inputs `command='printf "\\033[32mGREEN\\033[0m"'`, `escape_non_printable=False`, `escape_sequences=True`, `expected_pattern='\\x1b\\[32m'`, `join_wrapped=False`, `min_tmux_version=None`, `not_expected_pattern=None`, `preserve_trailing=False`, `test_id='escape_sequences_green'`, `trim_trailing=False`; fixtures `session`; execute `session.new_window(attach=True, window_name=f'cap_{test_id[:10]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(full_command, literal=False, suppress_history=False)`; `pane.capture_pane(escape_sequences=escape_sequences, escape_non_printable=escape_non_printable, join_wrapped=join_wrapped, preserve_trailing=preserve_trailing, trim_trailing=trim_trailing)`; `shutil.which('env')`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_pattern`: execute `re.search(expected_pattern, output_str, re.DOTALL)`; assert `re.search(expected_pattern, output_str, re.DOTALL); message=f"Expected pattern '{expected_pattern}' not found in output:\n{output_str}"`; when `not_expected_pattern`: execute `re.search(not_expected_pattern, output_str, re.DOTALL)`; assert `not re.search(not_expected_pattern, output_str, re.DOTALL); message=f"Unexpected pattern '{not_expected_pattern}' found in output"`. | real-tmux integration test | PythonBehaviorParityTest#test_pane_capture_pane_py_test_capture_pane_flags_escape_sequences_green_cad850baea | 3.2a through 3.7b | planned parity | -| tests/test_pane_capture_pane.py::test_capture_pane_flags[escape_sequences_red] | Test capture_pane() with various flag combinations; case inputs `command='printf "\\033[31mRED\\033[0m"'`, `escape_non_printable=False`, `escape_sequences=True`, `expected_pattern='\\x1b\\[31m'`, `join_wrapped=False`, `min_tmux_version=None`, `not_expected_pattern=None`, `preserve_trailing=False`, `test_id='escape_sequences_red'`, `trim_trailing=False`; fixtures `session`; execute `session.new_window(attach=True, window_name=f'cap_{test_id[:10]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(full_command, literal=False, suppress_history=False)`; `pane.capture_pane(escape_sequences=escape_sequences, escape_non_printable=escape_non_printable, join_wrapped=join_wrapped, preserve_trailing=preserve_trailing, trim_trailing=trim_trailing)`; `shutil.which('env')`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_pattern`: execute `re.search(expected_pattern, output_str, re.DOTALL)`; assert `re.search(expected_pattern, output_str, re.DOTALL); message=f"Expected pattern '{expected_pattern}' not found in output:\n{output_str}"`; when `not_expected_pattern`: execute `re.search(not_expected_pattern, output_str, re.DOTALL)`; assert `not re.search(not_expected_pattern, output_str, re.DOTALL); message=f"Unexpected pattern '{not_expected_pattern}' found in output"`. | real-tmux integration test | PythonBehaviorParityTest#s_test_pane_capture_pane_py_test_capture_pane_flags_escape_sequences_red_c8e93b8afa | 3.2a through 3.7b | planned parity | -| tests/test_pane_capture_pane.py::test_capture_pane_flags[escape_sequences_with_join] | Test capture_pane() with various flag combinations; case inputs `command='printf "\\033[32mGREEN TEXT\\033[0m"'`, `escape_non_printable=False`, `escape_sequences=True`, `expected_pattern='\\x1b\\[32m'`, `join_wrapped=True`, `min_tmux_version=None`, `not_expected_pattern=None`, `preserve_trailing=False`, `test_id='escape_sequences_with_join'`, `trim_trailing=False`; fixtures `session`; execute `session.new_window(attach=True, window_name=f'cap_{test_id[:10]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(full_command, literal=False, suppress_history=False)`; `pane.capture_pane(escape_sequences=escape_sequences, escape_non_printable=escape_non_printable, join_wrapped=join_wrapped, preserve_trailing=preserve_trailing, trim_trailing=trim_trailing)`; `shutil.which('env')`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_pattern`: execute `re.search(expected_pattern, output_str, re.DOTALL)`; assert `re.search(expected_pattern, output_str, re.DOTALL); message=f"Expected pattern '{expected_pattern}' not found in output:\n{output_str}"`; when `not_expected_pattern`: execute `re.search(not_expected_pattern, output_str, re.DOTALL)`; assert `not re.search(not_expected_pattern, output_str, re.DOTALL); message=f"Unexpected pattern '{not_expected_pattern}' found in output"`. | real-tmux integration test | PythonBehaviorParityTest#_pane_capture_pane_py_test_capture_pane_flags_escape_sequences_with_join_9a8e67967f | 3.2a through 3.7b | planned parity | -| tests/test_pane_capture_pane.py::test_capture_pane_flags[join_with_preserve_trailing] | Test capture_pane() with various flag combinations; case inputs `command='printf "%s " "$(python3 -c \'print("z" * 100)\')"'`, `escape_non_printable=False`, `escape_sequences=False`, `expected_pattern='z{50,}'`, `join_wrapped=True`, `min_tmux_version=None`, `not_expected_pattern=None`, `preserve_trailing=True`, `test_id='join_with_preserve_trailing'`, `trim_trailing=False`; fixtures `session`; execute `session.new_window(attach=True, window_name=f'cap_{test_id[:10]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(full_command, literal=False, suppress_history=False)`; `pane.capture_pane(escape_sequences=escape_sequences, escape_non_printable=escape_non_printable, join_wrapped=join_wrapped, preserve_trailing=preserve_trailing, trim_trailing=trim_trailing)`; `shutil.which('env')`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_pattern`: execute `re.search(expected_pattern, output_str, re.DOTALL)`; assert `re.search(expected_pattern, output_str, re.DOTALL); message=f"Expected pattern '{expected_pattern}' not found in output:\n{output_str}"`; when `not_expected_pattern`: execute `re.search(not_expected_pattern, output_str, re.DOTALL)`; assert `not re.search(not_expected_pattern, output_str, re.DOTALL); message=f"Unexpected pattern '{not_expected_pattern}' found in output"`. | real-tmux integration test | PythonBehaviorParityTest#pane_capture_pane_py_test_capture_pane_flags_join_with_preserve_trailing_a5c4ff8bed | 3.2a through 3.7b | planned parity | -| tests/test_pane_capture_pane.py::test_capture_pane_flags[join_wrapped_long_line] | Test capture_pane() with various flag combinations; case inputs `command='printf "%s" "$(python3 -c \'print("x" * 200)\')"'`, `escape_non_printable=False`, `escape_sequences=False`, `expected_pattern='x{100,}'`, `join_wrapped=True`, `min_tmux_version=None`, `not_expected_pattern=None`, `preserve_trailing=False`, `test_id='join_wrapped_long_line'`, `trim_trailing=False`; fixtures `session`; execute `session.new_window(attach=True, window_name=f'cap_{test_id[:10]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(full_command, literal=False, suppress_history=False)`; `pane.capture_pane(escape_sequences=escape_sequences, escape_non_printable=escape_non_printable, join_wrapped=join_wrapped, preserve_trailing=preserve_trailing, trim_trailing=trim_trailing)`; `shutil.which('env')`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_pattern`: execute `re.search(expected_pattern, output_str, re.DOTALL)`; assert `re.search(expected_pattern, output_str, re.DOTALL); message=f"Expected pattern '{expected_pattern}' not found in output:\n{output_str}"`; when `not_expected_pattern`: execute `re.search(not_expected_pattern, output_str, re.DOTALL)`; assert `not re.search(not_expected_pattern, output_str, re.DOTALL); message=f"Unexpected pattern '{not_expected_pattern}' found in output"`. | real-tmux integration test | PythonBehaviorParityTest#test_pane_capture_pane_py_test_capture_pane_flags_join_wrapped_long_line_c03ac2c0ae | 3.2a through 3.7b | planned parity | -| tests/test_pane_capture_pane.py::test_capture_pane_flags[join_wrapped_numbers] | Test capture_pane() with various flag combinations; case inputs `command='printf "%s" "$(seq -s "" 1 100)"'`, `escape_non_printable=False`, `escape_sequences=False`, `expected_pattern='123.*99100'`, `join_wrapped=True`, `min_tmux_version=None`, `not_expected_pattern=None`, `preserve_trailing=False`, `test_id='join_wrapped_numbers'`, `trim_trailing=False`; fixtures `session`; execute `session.new_window(attach=True, window_name=f'cap_{test_id[:10]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(full_command, literal=False, suppress_history=False)`; `pane.capture_pane(escape_sequences=escape_sequences, escape_non_printable=escape_non_printable, join_wrapped=join_wrapped, preserve_trailing=preserve_trailing, trim_trailing=trim_trailing)`; `shutil.which('env')`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_pattern`: execute `re.search(expected_pattern, output_str, re.DOTALL)`; assert `re.search(expected_pattern, output_str, re.DOTALL); message=f"Expected pattern '{expected_pattern}' not found in output:\n{output_str}"`; when `not_expected_pattern`: execute `re.search(not_expected_pattern, output_str, re.DOTALL)`; assert `not re.search(not_expected_pattern, output_str, re.DOTALL); message=f"Unexpected pattern '{not_expected_pattern}' found in output"`. | real-tmux integration test | PythonBehaviorParityTest#s_test_pane_capture_pane_py_test_capture_pane_flags_join_wrapped_numbers_72182cbd56 | 3.2a through 3.7b | planned parity | -| tests/test_pane_capture_pane.py::test_capture_pane_flags[no_escape_sequences] | Test capture_pane() with various flag combinations; case inputs `command='printf "\\033[31mRED\\033[0m"'`, `escape_non_printable=False`, `escape_sequences=False`, `expected_pattern='RED'`, `join_wrapped=False`, `min_tmux_version=None`, `not_expected_pattern='\\x1b'`, `preserve_trailing=False`, `test_id='no_escape_sequences'`, `trim_trailing=False`; fixtures `session`; execute `session.new_window(attach=True, window_name=f'cap_{test_id[:10]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(full_command, literal=False, suppress_history=False)`; `pane.capture_pane(escape_sequences=escape_sequences, escape_non_printable=escape_non_printable, join_wrapped=join_wrapped, preserve_trailing=preserve_trailing, trim_trailing=trim_trailing)`; `shutil.which('env')`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_pattern`: execute `re.search(expected_pattern, output_str, re.DOTALL)`; assert `re.search(expected_pattern, output_str, re.DOTALL); message=f"Expected pattern '{expected_pattern}' not found in output:\n{output_str}"`; when `not_expected_pattern`: execute `re.search(not_expected_pattern, output_str, re.DOTALL)`; assert `not re.search(not_expected_pattern, output_str, re.DOTALL); message=f"Unexpected pattern '{not_expected_pattern}' found in output"`. | real-tmux integration test | PythonBehaviorParityTest#ts_test_pane_capture_pane_py_test_capture_pane_flags_no_escape_sequences_355c0c30a4 | 3.2a through 3.7b | planned parity | -| tests/test_pane_capture_pane.py::test_capture_pane_flags[no_preserve_trailing] | Test capture_pane() with various flag combinations; case inputs `command='printf "text \\n"'`, `escape_non_printable=False`, `escape_sequences=False`, `expected_pattern='text'`, `join_wrapped=False`, `min_tmux_version=None`, `not_expected_pattern=None`, `preserve_trailing=False`, `test_id='no_preserve_trailing'`, `trim_trailing=False`; fixtures `session`; execute `session.new_window(attach=True, window_name=f'cap_{test_id[:10]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(full_command, literal=False, suppress_history=False)`; `pane.capture_pane(escape_sequences=escape_sequences, escape_non_printable=escape_non_printable, join_wrapped=join_wrapped, preserve_trailing=preserve_trailing, trim_trailing=trim_trailing)`; `shutil.which('env')`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_pattern`: execute `re.search(expected_pattern, output_str, re.DOTALL)`; assert `re.search(expected_pattern, output_str, re.DOTALL); message=f"Expected pattern '{expected_pattern}' not found in output:\n{output_str}"`; when `not_expected_pattern`: execute `re.search(not_expected_pattern, output_str, re.DOTALL)`; assert `not re.search(not_expected_pattern, output_str, re.DOTALL); message=f"Unexpected pattern '{not_expected_pattern}' found in output"`. | real-tmux integration test | PythonBehaviorParityTest#s_test_pane_capture_pane_py_test_capture_pane_flags_no_preserve_trailing_7bfa13ace2 | 3.2a through 3.7b | planned parity | -| tests/test_pane_capture_pane.py::test_capture_pane_flags[preserve_trailing_spaces] | Test capture_pane() with various flag combinations; case inputs `command='printf "text \\n"'`, `escape_non_printable=False`, `escape_sequences=False`, `expected_pattern='text '`, `join_wrapped=False`, `min_tmux_version=None`, `not_expected_pattern=None`, `preserve_trailing=True`, `test_id='preserve_trailing_spaces'`, `trim_trailing=False`; fixtures `session`; execute `session.new_window(attach=True, window_name=f'cap_{test_id[:10]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(full_command, literal=False, suppress_history=False)`; `pane.capture_pane(escape_sequences=escape_sequences, escape_non_printable=escape_non_printable, join_wrapped=join_wrapped, preserve_trailing=preserve_trailing, trim_trailing=trim_trailing)`; `shutil.which('env')`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_pattern`: execute `re.search(expected_pattern, output_str, re.DOTALL)`; assert `re.search(expected_pattern, output_str, re.DOTALL); message=f"Expected pattern '{expected_pattern}' not found in output:\n{output_str}"`; when `not_expected_pattern`: execute `re.search(not_expected_pattern, output_str, re.DOTALL)`; assert `not re.search(not_expected_pattern, output_str, re.DOTALL); message=f"Unexpected pattern '{not_expected_pattern}' found in output"`. | real-tmux integration test | PythonBehaviorParityTest#st_pane_capture_pane_py_test_capture_pane_flags_preserve_trailing_spaces_fe6fd70cf1 | 3.2a through 3.7b | planned parity | +| tests/test_pane_capture_pane.py::test_capture_pane_flag_smoke[quiet] | Smoke-test capture_pane flags that aren't tied to output content; case inputs `kwargs={'quiet': True}`, `min_tmux_version=None`; fixtures `session`; execute `pane.capture_pane(**kwargs)`; assert `pane is not None`; `isinstance(result, list)`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_capture_pane_py_test_capture_pane_flag_smoke_quiet_6f1c8513d6 | 3.3a through 3.7c | planned parity | +| tests/test_pane_capture_pane.py::test_capture_pane_flags[all_flags_except_trim] | Test capture_pane() with various flag combinations; case inputs `command='printf "\\033[34mBLUE: %s \\033[0m" "test"'`, `escape_non_printable=True`, `escape_sequences=True`, `expected_pattern='\\x1b\\[34m|BLUE'`, `join_wrapped=True`, `min_tmux_version=None`, `not_expected_pattern=None`, `preserve_trailing=True`, `test_id='all_flags_except_trim'`, `trim_trailing=False`; fixtures `session`; execute `session.new_window(attach=True, window_name=f'cap_{test_id[:10]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(full_command, literal=False, suppress_history=False)`; `pane.capture_pane(escape_sequences=escape_sequences, escape_non_printable=escape_non_printable, join_wrapped=join_wrapped, preserve_trailing=preserve_trailing, trim_trailing=trim_trailing)`; `shutil.which('env')`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_pattern`: execute `re.search(expected_pattern, output_str, re.DOTALL)`; assert `re.search(expected_pattern, output_str, re.DOTALL); message=f"Expected pattern '{expected_pattern}' not found in output:\n{output_str}"`; when `not_expected_pattern`: execute `re.search(not_expected_pattern, output_str, re.DOTALL)`; assert `not re.search(not_expected_pattern, output_str, re.DOTALL); message=f"Unexpected pattern '{not_expected_pattern}' found in output"`. | real-tmux integration test | PythonBehaviorParityTest#_test_pane_capture_pane_py_test_capture_pane_flags_all_flags_except_trim_922d38ac76 | 3.3a through 3.7c | planned parity | +| tests/test_pane_capture_pane.py::test_capture_pane_flags[basic_capture] | Test capture_pane() with various flag combinations; case inputs `command='echo "hello world"'`, `escape_non_printable=False`, `escape_sequences=False`, `expected_pattern='hello world'`, `join_wrapped=False`, `min_tmux_version=None`, `not_expected_pattern=None`, `preserve_trailing=False`, `test_id='basic_capture'`, `trim_trailing=False`; fixtures `session`; execute `session.new_window(attach=True, window_name=f'cap_{test_id[:10]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(full_command, literal=False, suppress_history=False)`; `pane.capture_pane(escape_sequences=escape_sequences, escape_non_printable=escape_non_printable, join_wrapped=join_wrapped, preserve_trailing=preserve_trailing, trim_trailing=trim_trailing)`; `shutil.which('env')`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_pattern`: execute `re.search(expected_pattern, output_str, re.DOTALL)`; assert `re.search(expected_pattern, output_str, re.DOTALL); message=f"Expected pattern '{expected_pattern}' not found in output:\n{output_str}"`; when `not_expected_pattern`: execute `re.search(not_expected_pattern, output_str, re.DOTALL)`; assert `not re.search(not_expected_pattern, output_str, re.DOTALL); message=f"Unexpected pattern '{not_expected_pattern}' found in output"`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_capture_pane_py_test_capture_pane_flags_basic_capture_c13b79057e | 3.3a through 3.7c | planned parity | +| tests/test_pane_capture_pane.py::test_capture_pane_flags[basic_multiline] | Test capture_pane() with various flag combinations; case inputs `command='printf "line1\\nline2\\nline3\\n"'`, `escape_non_printable=False`, `escape_sequences=False`, `expected_pattern='line1.*line2.*line3'`, `join_wrapped=False`, `min_tmux_version=None`, `not_expected_pattern=None`, `preserve_trailing=False`, `test_id='basic_multiline'`, `trim_trailing=False`; fixtures `session`; execute `session.new_window(attach=True, window_name=f'cap_{test_id[:10]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(full_command, literal=False, suppress_history=False)`; `pane.capture_pane(escape_sequences=escape_sequences, escape_non_printable=escape_non_printable, join_wrapped=join_wrapped, preserve_trailing=preserve_trailing, trim_trailing=trim_trailing)`; `shutil.which('env')`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_pattern`: execute `re.search(expected_pattern, output_str, re.DOTALL)`; assert `re.search(expected_pattern, output_str, re.DOTALL); message=f"Expected pattern '{expected_pattern}' not found in output:\n{output_str}"`; when `not_expected_pattern`: execute `re.search(not_expected_pattern, output_str, re.DOTALL)`; assert `not re.search(not_expected_pattern, output_str, re.DOTALL); message=f"Unexpected pattern '{not_expected_pattern}' found in output"`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_capture_pane_py_test_capture_pane_flags_basic_multiline_faf8bbdc1a | 3.3a through 3.7c | planned parity | +| tests/test_pane_capture_pane.py::test_capture_pane_flags[escape_non_printable_basic] | Test capture_pane() with various flag combinations; case inputs `command='printf "\\001\\002\\003"'`, `escape_non_printable=True`, `escape_sequences=False`, `expected_pattern='\\\\00[123]'`, `join_wrapped=False`, `min_tmux_version=None`, `not_expected_pattern=None`, `preserve_trailing=False`, `test_id='escape_non_printable_basic'`, `trim_trailing=False`; fixtures `session`; execute `session.new_window(attach=True, window_name=f'cap_{test_id[:10]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(full_command, literal=False, suppress_history=False)`; `pane.capture_pane(escape_sequences=escape_sequences, escape_non_printable=escape_non_printable, join_wrapped=join_wrapped, preserve_trailing=preserve_trailing, trim_trailing=trim_trailing)`; `shutil.which('env')`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_pattern`: execute `re.search(expected_pattern, output_str, re.DOTALL)`; assert `re.search(expected_pattern, output_str, re.DOTALL); message=f"Expected pattern '{expected_pattern}' not found in output:\n{output_str}"`; when `not_expected_pattern`: execute `re.search(not_expected_pattern, output_str, re.DOTALL)`; assert `not re.search(not_expected_pattern, output_str, re.DOTALL); message=f"Unexpected pattern '{not_expected_pattern}' found in output"`. | real-tmux integration test | PythonBehaviorParityTest#_pane_capture_pane_py_test_capture_pane_flags_escape_non_printable_basic_469ed4ffd7 | 3.3a through 3.7c | planned parity | +| tests/test_pane_capture_pane.py::test_capture_pane_flags[escape_non_printable_tab] | Test capture_pane() with various flag combinations; case inputs `command='printf "a\\tb"'`, `escape_non_printable=True`, `escape_sequences=False`, `expected_pattern='a.*b'`, `join_wrapped=False`, `min_tmux_version=None`, `not_expected_pattern=None`, `preserve_trailing=False`, `test_id='escape_non_printable_tab'`, `trim_trailing=False`; fixtures `session`; execute `session.new_window(attach=True, window_name=f'cap_{test_id[:10]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(full_command, literal=False, suppress_history=False)`; `pane.capture_pane(escape_sequences=escape_sequences, escape_non_printable=escape_non_printable, join_wrapped=join_wrapped, preserve_trailing=preserve_trailing, trim_trailing=trim_trailing)`; `shutil.which('env')`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_pattern`: execute `re.search(expected_pattern, output_str, re.DOTALL)`; assert `re.search(expected_pattern, output_str, re.DOTALL); message=f"Expected pattern '{expected_pattern}' not found in output:\n{output_str}"`; when `not_expected_pattern`: execute `re.search(not_expected_pattern, output_str, re.DOTALL)`; assert `not re.search(not_expected_pattern, output_str, re.DOTALL); message=f"Unexpected pattern '{not_expected_pattern}' found in output"`. | real-tmux integration test | PythonBehaviorParityTest#st_pane_capture_pane_py_test_capture_pane_flags_escape_non_printable_tab_5898542c8c | 3.3a through 3.7c | planned parity | +| tests/test_pane_capture_pane.py::test_capture_pane_flags[escape_sequences_bold] | Test capture_pane() with various flag combinations; case inputs `command='printf "\\033[1mBOLD\\033[0m"'`, `escape_non_printable=False`, `escape_sequences=True`, `expected_pattern='\\x1b\\[1m'`, `join_wrapped=False`, `min_tmux_version=None`, `not_expected_pattern=None`, `preserve_trailing=False`, `test_id='escape_sequences_bold'`, `trim_trailing=False`; fixtures `session`; execute `session.new_window(attach=True, window_name=f'cap_{test_id[:10]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(full_command, literal=False, suppress_history=False)`; `pane.capture_pane(escape_sequences=escape_sequences, escape_non_printable=escape_non_printable, join_wrapped=join_wrapped, preserve_trailing=preserve_trailing, trim_trailing=trim_trailing)`; `shutil.which('env')`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_pattern`: execute `re.search(expected_pattern, output_str, re.DOTALL)`; assert `re.search(expected_pattern, output_str, re.DOTALL); message=f"Expected pattern '{expected_pattern}' not found in output:\n{output_str}"`; when `not_expected_pattern`: execute `re.search(not_expected_pattern, output_str, re.DOTALL)`; assert `not re.search(not_expected_pattern, output_str, re.DOTALL); message=f"Unexpected pattern '{not_expected_pattern}' found in output"`. | real-tmux integration test | PythonBehaviorParityTest#_test_pane_capture_pane_py_test_capture_pane_flags_escape_sequences_bold_da0623516d | 3.3a through 3.7c | planned parity | +| tests/test_pane_capture_pane.py::test_capture_pane_flags[escape_sequences_green] | Test capture_pane() with various flag combinations; case inputs `command='printf "\\033[32mGREEN\\033[0m"'`, `escape_non_printable=False`, `escape_sequences=True`, `expected_pattern='\\x1b\\[32m'`, `join_wrapped=False`, `min_tmux_version=None`, `not_expected_pattern=None`, `preserve_trailing=False`, `test_id='escape_sequences_green'`, `trim_trailing=False`; fixtures `session`; execute `session.new_window(attach=True, window_name=f'cap_{test_id[:10]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(full_command, literal=False, suppress_history=False)`; `pane.capture_pane(escape_sequences=escape_sequences, escape_non_printable=escape_non_printable, join_wrapped=join_wrapped, preserve_trailing=preserve_trailing, trim_trailing=trim_trailing)`; `shutil.which('env')`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_pattern`: execute `re.search(expected_pattern, output_str, re.DOTALL)`; assert `re.search(expected_pattern, output_str, re.DOTALL); message=f"Expected pattern '{expected_pattern}' not found in output:\n{output_str}"`; when `not_expected_pattern`: execute `re.search(not_expected_pattern, output_str, re.DOTALL)`; assert `not re.search(not_expected_pattern, output_str, re.DOTALL); message=f"Unexpected pattern '{not_expected_pattern}' found in output"`. | real-tmux integration test | PythonBehaviorParityTest#test_pane_capture_pane_py_test_capture_pane_flags_escape_sequences_green_cad850baea | 3.3a through 3.7c | planned parity | +| tests/test_pane_capture_pane.py::test_capture_pane_flags[escape_sequences_red] | Test capture_pane() with various flag combinations; case inputs `command='printf "\\033[31mRED\\033[0m"'`, `escape_non_printable=False`, `escape_sequences=True`, `expected_pattern='\\x1b\\[31m'`, `join_wrapped=False`, `min_tmux_version=None`, `not_expected_pattern=None`, `preserve_trailing=False`, `test_id='escape_sequences_red'`, `trim_trailing=False`; fixtures `session`; execute `session.new_window(attach=True, window_name=f'cap_{test_id[:10]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(full_command, literal=False, suppress_history=False)`; `pane.capture_pane(escape_sequences=escape_sequences, escape_non_printable=escape_non_printable, join_wrapped=join_wrapped, preserve_trailing=preserve_trailing, trim_trailing=trim_trailing)`; `shutil.which('env')`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_pattern`: execute `re.search(expected_pattern, output_str, re.DOTALL)`; assert `re.search(expected_pattern, output_str, re.DOTALL); message=f"Expected pattern '{expected_pattern}' not found in output:\n{output_str}"`; when `not_expected_pattern`: execute `re.search(not_expected_pattern, output_str, re.DOTALL)`; assert `not re.search(not_expected_pattern, output_str, re.DOTALL); message=f"Unexpected pattern '{not_expected_pattern}' found in output"`. | real-tmux integration test | PythonBehaviorParityTest#s_test_pane_capture_pane_py_test_capture_pane_flags_escape_sequences_red_c8e93b8afa | 3.3a through 3.7c | planned parity | +| tests/test_pane_capture_pane.py::test_capture_pane_flags[escape_sequences_with_join] | Test capture_pane() with various flag combinations; case inputs `command='printf "\\033[32mGREEN TEXT\\033[0m"'`, `escape_non_printable=False`, `escape_sequences=True`, `expected_pattern='\\x1b\\[32m'`, `join_wrapped=True`, `min_tmux_version=None`, `not_expected_pattern=None`, `preserve_trailing=False`, `test_id='escape_sequences_with_join'`, `trim_trailing=False`; fixtures `session`; execute `session.new_window(attach=True, window_name=f'cap_{test_id[:10]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(full_command, literal=False, suppress_history=False)`; `pane.capture_pane(escape_sequences=escape_sequences, escape_non_printable=escape_non_printable, join_wrapped=join_wrapped, preserve_trailing=preserve_trailing, trim_trailing=trim_trailing)`; `shutil.which('env')`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_pattern`: execute `re.search(expected_pattern, output_str, re.DOTALL)`; assert `re.search(expected_pattern, output_str, re.DOTALL); message=f"Expected pattern '{expected_pattern}' not found in output:\n{output_str}"`; when `not_expected_pattern`: execute `re.search(not_expected_pattern, output_str, re.DOTALL)`; assert `not re.search(not_expected_pattern, output_str, re.DOTALL); message=f"Unexpected pattern '{not_expected_pattern}' found in output"`. | real-tmux integration test | PythonBehaviorParityTest#_pane_capture_pane_py_test_capture_pane_flags_escape_sequences_with_join_9a8e67967f | 3.3a through 3.7c | planned parity | +| tests/test_pane_capture_pane.py::test_capture_pane_flags[join_with_preserve_trailing] | Test capture_pane() with various flag combinations; case inputs `command='printf "%s " "$(python3 -c \'print("z" * 100)\')"'`, `escape_non_printable=False`, `escape_sequences=False`, `expected_pattern='z{50,}'`, `join_wrapped=True`, `min_tmux_version=None`, `not_expected_pattern=None`, `preserve_trailing=True`, `test_id='join_with_preserve_trailing'`, `trim_trailing=False`; fixtures `session`; execute `session.new_window(attach=True, window_name=f'cap_{test_id[:10]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(full_command, literal=False, suppress_history=False)`; `pane.capture_pane(escape_sequences=escape_sequences, escape_non_printable=escape_non_printable, join_wrapped=join_wrapped, preserve_trailing=preserve_trailing, trim_trailing=trim_trailing)`; `shutil.which('env')`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_pattern`: execute `re.search(expected_pattern, output_str, re.DOTALL)`; assert `re.search(expected_pattern, output_str, re.DOTALL); message=f"Expected pattern '{expected_pattern}' not found in output:\n{output_str}"`; when `not_expected_pattern`: execute `re.search(not_expected_pattern, output_str, re.DOTALL)`; assert `not re.search(not_expected_pattern, output_str, re.DOTALL); message=f"Unexpected pattern '{not_expected_pattern}' found in output"`. | real-tmux integration test | PythonBehaviorParityTest#pane_capture_pane_py_test_capture_pane_flags_join_with_preserve_trailing_a5c4ff8bed | 3.3a through 3.7c | planned parity | +| tests/test_pane_capture_pane.py::test_capture_pane_flags[join_wrapped_long_line] | Test capture_pane() with various flag combinations; case inputs `command='printf "%s" "$(python3 -c \'print("x" * 200)\')"'`, `escape_non_printable=False`, `escape_sequences=False`, `expected_pattern='x{100,}'`, `join_wrapped=True`, `min_tmux_version=None`, `not_expected_pattern=None`, `preserve_trailing=False`, `test_id='join_wrapped_long_line'`, `trim_trailing=False`; fixtures `session`; execute `session.new_window(attach=True, window_name=f'cap_{test_id[:10]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(full_command, literal=False, suppress_history=False)`; `pane.capture_pane(escape_sequences=escape_sequences, escape_non_printable=escape_non_printable, join_wrapped=join_wrapped, preserve_trailing=preserve_trailing, trim_trailing=trim_trailing)`; `shutil.which('env')`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_pattern`: execute `re.search(expected_pattern, output_str, re.DOTALL)`; assert `re.search(expected_pattern, output_str, re.DOTALL); message=f"Expected pattern '{expected_pattern}' not found in output:\n{output_str}"`; when `not_expected_pattern`: execute `re.search(not_expected_pattern, output_str, re.DOTALL)`; assert `not re.search(not_expected_pattern, output_str, re.DOTALL); message=f"Unexpected pattern '{not_expected_pattern}' found in output"`. | real-tmux integration test | PythonBehaviorParityTest#test_pane_capture_pane_py_test_capture_pane_flags_join_wrapped_long_line_c03ac2c0ae | 3.3a through 3.7c | planned parity | +| tests/test_pane_capture_pane.py::test_capture_pane_flags[join_wrapped_numbers] | Test capture_pane() with various flag combinations; case inputs `command='printf "%s" "$(seq -s "" 1 100)"'`, `escape_non_printable=False`, `escape_sequences=False`, `expected_pattern='123.*99100'`, `join_wrapped=True`, `min_tmux_version=None`, `not_expected_pattern=None`, `preserve_trailing=False`, `test_id='join_wrapped_numbers'`, `trim_trailing=False`; fixtures `session`; execute `session.new_window(attach=True, window_name=f'cap_{test_id[:10]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(full_command, literal=False, suppress_history=False)`; `pane.capture_pane(escape_sequences=escape_sequences, escape_non_printable=escape_non_printable, join_wrapped=join_wrapped, preserve_trailing=preserve_trailing, trim_trailing=trim_trailing)`; `shutil.which('env')`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_pattern`: execute `re.search(expected_pattern, output_str, re.DOTALL)`; assert `re.search(expected_pattern, output_str, re.DOTALL); message=f"Expected pattern '{expected_pattern}' not found in output:\n{output_str}"`; when `not_expected_pattern`: execute `re.search(not_expected_pattern, output_str, re.DOTALL)`; assert `not re.search(not_expected_pattern, output_str, re.DOTALL); message=f"Unexpected pattern '{not_expected_pattern}' found in output"`. | real-tmux integration test | PythonBehaviorParityTest#s_test_pane_capture_pane_py_test_capture_pane_flags_join_wrapped_numbers_72182cbd56 | 3.3a through 3.7c | planned parity | +| tests/test_pane_capture_pane.py::test_capture_pane_flags[no_escape_sequences] | Test capture_pane() with various flag combinations; case inputs `command='printf "\\033[31mRED\\033[0m"'`, `escape_non_printable=False`, `escape_sequences=False`, `expected_pattern='RED'`, `join_wrapped=False`, `min_tmux_version=None`, `not_expected_pattern='\\x1b'`, `preserve_trailing=False`, `test_id='no_escape_sequences'`, `trim_trailing=False`; fixtures `session`; execute `session.new_window(attach=True, window_name=f'cap_{test_id[:10]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(full_command, literal=False, suppress_history=False)`; `pane.capture_pane(escape_sequences=escape_sequences, escape_non_printable=escape_non_printable, join_wrapped=join_wrapped, preserve_trailing=preserve_trailing, trim_trailing=trim_trailing)`; `shutil.which('env')`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_pattern`: execute `re.search(expected_pattern, output_str, re.DOTALL)`; assert `re.search(expected_pattern, output_str, re.DOTALL); message=f"Expected pattern '{expected_pattern}' not found in output:\n{output_str}"`; when `not_expected_pattern`: execute `re.search(not_expected_pattern, output_str, re.DOTALL)`; assert `not re.search(not_expected_pattern, output_str, re.DOTALL); message=f"Unexpected pattern '{not_expected_pattern}' found in output"`. | real-tmux integration test | PythonBehaviorParityTest#ts_test_pane_capture_pane_py_test_capture_pane_flags_no_escape_sequences_355c0c30a4 | 3.3a through 3.7c | planned parity | +| tests/test_pane_capture_pane.py::test_capture_pane_flags[no_preserve_trailing] | Test capture_pane() with various flag combinations; case inputs `command='printf "text \\n"'`, `escape_non_printable=False`, `escape_sequences=False`, `expected_pattern='text'`, `join_wrapped=False`, `min_tmux_version=None`, `not_expected_pattern=None`, `preserve_trailing=False`, `test_id='no_preserve_trailing'`, `trim_trailing=False`; fixtures `session`; execute `session.new_window(attach=True, window_name=f'cap_{test_id[:10]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(full_command, literal=False, suppress_history=False)`; `pane.capture_pane(escape_sequences=escape_sequences, escape_non_printable=escape_non_printable, join_wrapped=join_wrapped, preserve_trailing=preserve_trailing, trim_trailing=trim_trailing)`; `shutil.which('env')`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_pattern`: execute `re.search(expected_pattern, output_str, re.DOTALL)`; assert `re.search(expected_pattern, output_str, re.DOTALL); message=f"Expected pattern '{expected_pattern}' not found in output:\n{output_str}"`; when `not_expected_pattern`: execute `re.search(not_expected_pattern, output_str, re.DOTALL)`; assert `not re.search(not_expected_pattern, output_str, re.DOTALL); message=f"Unexpected pattern '{not_expected_pattern}' found in output"`. | real-tmux integration test | PythonBehaviorParityTest#s_test_pane_capture_pane_py_test_capture_pane_flags_no_preserve_trailing_7bfa13ace2 | 3.3a through 3.7c | planned parity | +| tests/test_pane_capture_pane.py::test_capture_pane_flags[preserve_trailing_spaces] | Test capture_pane() with various flag combinations; case inputs `command='printf "text \\n"'`, `escape_non_printable=False`, `escape_sequences=False`, `expected_pattern='text '`, `join_wrapped=False`, `min_tmux_version=None`, `not_expected_pattern=None`, `preserve_trailing=True`, `test_id='preserve_trailing_spaces'`, `trim_trailing=False`; fixtures `session`; execute `session.new_window(attach=True, window_name=f'cap_{test_id[:10]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(full_command, literal=False, suppress_history=False)`; `pane.capture_pane(escape_sequences=escape_sequences, escape_non_printable=escape_non_printable, join_wrapped=join_wrapped, preserve_trailing=preserve_trailing, trim_trailing=trim_trailing)`; `shutil.which('env')`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_pattern`: execute `re.search(expected_pattern, output_str, re.DOTALL)`; assert `re.search(expected_pattern, output_str, re.DOTALL); message=f"Expected pattern '{expected_pattern}' not found in output:\n{output_str}"`; when `not_expected_pattern`: execute `re.search(not_expected_pattern, output_str, re.DOTALL)`; assert `not re.search(not_expected_pattern, output_str, re.DOTALL); message=f"Unexpected pattern '{not_expected_pattern}' found in output"`. | real-tmux integration test | PythonBehaviorParityTest#st_pane_capture_pane_py_test_capture_pane_flags_preserve_trailing_spaces_fe6fd70cf1 | 3.3a through 3.7c | planned parity | | tests/test_pane_capture_pane.py::test_capture_pane_flags[trim_trailing_basic] | Test capture_pane() with various flag combinations; case inputs `command='echo "short"'`, `escape_non_printable=False`, `escape_sequences=False`, `expected_pattern='short'`, `join_wrapped=False`, `min_tmux_version='3.4'`, `not_expected_pattern=None`, `preserve_trailing=False`, `test_id='trim_trailing_basic'`, `trim_trailing=True`; fixtures `session`; execute `session.new_window(attach=True, window_name=f'cap_{test_id[:10]}', window_shell=f"{env} PS1='$ ' sh")`; `pane.capture_pane()`; `pane.send_keys(full_command, literal=False, suppress_history=False)`; `pane.capture_pane(escape_sequences=escape_sequences, escape_non_printable=escape_non_printable, join_wrapped=join_wrapped, preserve_trailing=preserve_trailing, trim_trailing=trim_trailing)`; `shutil.which('env')`; `'\n'.join(pane.capture_pane())`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_pattern`: execute `re.search(expected_pattern, output_str, re.DOTALL)`; assert `re.search(expected_pattern, output_str, re.DOTALL); message=f"Expected pattern '{expected_pattern}' not found in output:\n{output_str}"`; when `not_expected_pattern`: execute `re.search(not_expected_pattern, output_str, re.DOTALL)`; assert `not re.search(not_expected_pattern, output_str, re.DOTALL); message=f"Unexpected pattern '{not_expected_pattern}' found in output"`. | real-tmux integration test | PythonBehaviorParityTest#ts_test_pane_capture_pane_py_test_capture_pane_flags_trim_trailing_basic_3684833d13 | 3.4 through 3.7b | planned parity | | tests/test_pane_capture_pane.py::test_capture_pane_pending_emits_dash_P | ``capture_pane(pending=True)`` emits the tmux ``-P`` flag; fixtures `monkeypatch`, `session`; execute `pane.capture_pane(pending=True)`; `real_cmd(cmd_name, *args, **kw)`; assert `pane is not None`; `capture_calls`; `'-P' in capture_calls[0]`; `'-p' in capture_calls[0]`. | unit test | PythonBehaviorParityTest#tests_test_pane_capture_pane_py_test_capture_pane_pending_emits_dash_P_676ede056f | not tmux-bound | planned parity | -| tests/test_pane_capture_pane.py::test_capture_pane_pending_returns_list | ``capture_pane(pending=True)`` returns a list (possibly empty); fixtures `session`; execute `pane.capture_pane(pending=True)`; assert `pane is not None`; `isinstance(result, list)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_capture_pane_py_test_capture_pane_pending_returns_list_9fdd656d0b | 3.2a through 3.7b | planned parity | -| tests/test_pane_capture_pane.py::test_capture_pane_start_end_with_flags | Test that start/end parameters work with new flags; fixtures `session`; execute `session.active_window.split(shell='sh')`; `pane.capture_pane()`; `pane.send_keys('echo "line1"; echo "line2"; echo "line3"', enter=True)`; `pane.capture_pane(start=0, end='-', preserve_trailing=True)`; `'\n'.join(pane.capture_pane())`; `retry_until(prompt_ready, 2, raises=True)`; assert `isinstance(output, list)`; `len(output) > 0`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_capture_pane_py_test_capture_pane_start_end_with_flags_cef758a00c | 3.2a through 3.7b | planned parity | -| tests/test_pane_capture_pane.py::test_capture_pane_to_buffer | Test capture_pane(to_buffer=...) writes to a tmux buffer; fixtures `session`; execute `pane.send_keys('echo "BUFFER_CAPTURE_MARKER"', enter=True)`; `pane.capture_pane()`; `pane.capture_pane(to_buffer='cap_test_buf')`; `session.server.cmd('show-buffer', '-b', 'cap_test_buf')`; `session.server.cmd('delete-buffer', '-b', 'cap_test_buf')`; `retry_until(lambda: 'BUFFER_CAPTURE_MARKER' in '\n'.join(pane.capture_pane()), 2, raises=True)`; assert `pane is not None`; `result is None`; `any(('BUFFER_CAPTURE_MARKER' in line for line in contents))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_capture_pane_py_test_capture_pane_to_buffer_357297abc8 | 3.2a through 3.7b | planned parity | -| tests/test_pane_capture_pane.py::test_capture_pane_trim_trailing_warning | Test that trim_trailing issues a warning on tmux < 3.4; fixtures `session`, `monkeypatch`; execute `session.active_window.split(shell='sh')`; `pane.capture_pane()`; `pane.capture_pane(trim_trailing=True)`; `'\n'.join(pane.capture_pane())`; `retry_until(prompt_ready, 2, raises=True)`; assert `len(w) == 1`; `'trim_trailing requires tmux 3.4+' in str(w[0].message)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_capture_pane_py_test_capture_pane_trim_trailing_warning_106c8f632e | 3.2a through 3.7b | planned parity | -| tests/test_pytest_plugin.py::test_plugin | Test libtmux pytest plugin; fixtures `pytester`, `monkeypatch`; execute `pytester.makefile('.ini', pytest=textwrap.dedent('\n[pytest]\naddopts=-vv\n '.strip()))`; `pytester.makeconftest(textwrap.dedent('\nimport pathlib\nimport pytest\n\n@pytest.fixture(autouse=True)\ndef setup(\n request: pytest.FixtureRequest,\n) -> None:\n pass\n '))`; `pytester.runpytest(str(first_test_filename))`; `textwrap.dedent('\n[pytest]\naddopts=-vv\n '.strip())`; `'\n[pytest]\naddopts=-vv\n '.strip()`; `textwrap.dedent('\nimport pathlib\nimport pytest\n\n@pytest.fixture(autouse=True)\ndef setup(\n request: pytest.FixtureRequest,\n) -> None:\n pass\n ')`. | JUnit extension integration test | PythonBehaviorParityTest#tests_test_pytest_plugin_py_test_plugin_ca9cfd8dd4 | 3.2a through 3.7b | planned parity | -| tests/test_pytest_plugin.py::test_reap_test_server_is_noop_when_socket_missing | Reaping a non-existent socket succeeds silently; execute `_libtmux_socket_dir()`; `socket_path.exists()`; `_reap_test_server(bogus_name)`; assert `not socket_path.exists()`. | JUnit extension integration test | PythonBehaviorParityTest#_test_pytest_plugin_py_test_reap_test_server_is_noop_when_socket_missing_3ad24e7f13 | 3.2a through 3.7b | planned parity | -| tests/test_pytest_plugin.py::test_reap_test_server_tolerates_none | ``_reap_test_server(None)`` is a no-op, not a crash; execute `_reap_test_server(None)`. | JUnit extension integration test | PythonBehaviorParityTest#tests_test_pytest_plugin_py_test_reap_test_server_tolerates_none_0b7646b9f3 | 3.2a through 3.7b | planned parity | -| tests/test_pytest_plugin.py::test_reap_test_server_unlinks_socket_file | ``_reap_test_server`` kills the daemon *and* unlinks the socket; execute `server.new_session(session_name='reap_probe')`; `Server(socket_name='libtmux_test_reap_unlink')`; `_libtmux_socket_dir()`; `socket_path.exists()`; `_reap_test_server('libtmux_test_reap_unlink')`; `socket_path.unlink(missing_ok=True)`; assert `socket_path.exists(); message=f'expected tmux to have created {socket_path}, but it is missing'`; `not socket_path.exists(); message=f'_reap_test_server should have unlinked {socket_path}'`. | JUnit extension integration test | PythonBehaviorParityTest#tests_test_pytest_plugin_py_test_reap_test_server_unlinks_socket_file_ca0bef2b4d | 3.2a through 3.7b | planned parity | -| tests/test_pytest_plugin.py::test_test_server | Test TestServer creates and cleans up server; fixtures `TestServer`; execute `TestServer()`; `server.is_alive()`; `server.new_session()`; `server.socket_name.startswith('libtmux_test')`; `server2.socket_name.startswith('libtmux_test')`; assert `server.is_alive() is False`; `server.is_alive() is True`; `len(server.sessions) == 1`; `session.session_name is not None`; `server.socket_name is not None`; `server.socket_name.startswith('libtmux_test')`; `server2.socket_name is not None`; `server2.socket_name.startswith('libtmux_test')`; `server2.socket_name != server.socket_name`. | JUnit extension integration test | PythonBehaviorParityTest#tests_test_pytest_plugin_py_test_test_server_77772185e3 | 3.2a through 3.7b | planned parity | -| tests/test_pytest_plugin.py::test_test_server_cleanup | Test TestServer properly cleans up after itself; fixtures `TestServer`; execute `TestServer()`; `server.new_session(session_name='test1')`; `server.new_session(session_name='test2')`; `server.is_alive()`; `server.kill()`; `time.sleep(0.1)`; assert `socket_name is not None`; `len(server.sessions) == 2`; `server.is_alive() is True`; `new_server.is_alive() is False`; `new_server.is_alive() is True`. | JUnit extension integration test | PythonBehaviorParityTest#tests_test_pytest_plugin_py_test_test_server_cleanup_89e16902ec | 3.2a through 3.7b | planned parity | -| tests/test_pytest_plugin.py::test_test_server_multiple | Test multiple TestServer instances can coexist; fixtures `TestServer`; execute `TestServer()`; `server1.new_session(session_name='test1')`; `server2.new_session(session_name='test2')`; `any((s.session_name == 'test1' for s in server1.sessions))`; `any((s.session_name == 'test2' for s in server2.sessions))`; `any((s.session_name == 'test1' for s in server2.sessions))`; assert `server1.socket_name != server2.socket_name`; `any((s.session_name == 'test1' for s in server1.sessions))`; `any((s.session_name == 'test2' for s in server2.sessions))`; `not any((s.session_name == 'test1' for s in server2.sessions))`; `not any((s.session_name == 'test2' for s in server1.sessions))`. | JUnit extension integration test | PythonBehaviorParityTest#tests_test_pytest_plugin_py_test_test_server_multiple_c049bb6ff0 | 3.2a through 3.7b | planned parity | -| tests/test_pytest_plugin.py::test_test_server_with_config | Test TestServer with config file; fixtures `TestServer`, `tmp_path`; execute `TestServer(config_file=str(config_file))`; `server.new_session()`; `session.cmd('show-options', '-g', 'status')`; `config_file.write_text('set -g status off', encoding='utf-8')`; assert `session.cmd('show-options', '-g', 'status').stdout[0] == 'status off'`. | JUnit extension integration test | PythonBehaviorParityTest#tests_test_pytest_plugin_py_test_test_server_with_config_6f5a08fc97 | 3.2a through 3.7b | planned parity | -| tests/test_resolution.py::test_ambiguous_point_lookup_is_catchable_and_legible[cross_session] | An ambiguous ``get()`` explains itself, and ``except LibTmuxException`` sees it; case inputs `expected_holders=2`, `select=None`, `shape='cross-session'`, `test_id='cross_session'`; fixtures `server`; execute `build_winlink_shape(server, shape)`; expect `pytest.raises(exc.LibTmuxException)`; assert `pane is not None`; `pane.pane_id is not None`; `isinstance(excinfo.value, exc.MultipleObjectsReturned)`; `str(excinfo.value) == f'Multiple objects returned (2): pane_id={pane.pane_id!r}'`. | real-tmux integration test | PythonBehaviorParityTest#on_py_test_ambiguous_point_lookup_is_catchable_and_legible_cross_session_60f6b26db9 | 3.2a through 3.7b | planned parity | -| tests/test_resolution.py::test_ambiguous_point_lookup_is_catchable_and_legible[grouped_session] | An ambiguous ``get()`` explains itself, and ``except LibTmuxException`` sees it; case inputs `expected_holders=2`, `select=None`, `shape='grouped'`, `test_id='grouped_session'`; fixtures `server`; execute `build_winlink_shape(server, shape)`; expect `pytest.raises(exc.LibTmuxException)`; assert `pane is not None`; `pane.pane_id is not None`; `isinstance(excinfo.value, exc.MultipleObjectsReturned)`; `str(excinfo.value) == f'Multiple objects returned (2): pane_id={pane.pane_id!r}'`. | real-tmux integration test | PythonBehaviorParityTest#_py_test_ambiguous_point_lookup_is_catchable_and_legible_grouped_session_1b1f3ea355 | 3.2a through 3.7b | planned parity | -| tests/test_resolution.py::test_ambiguous_point_lookup_is_catchable_and_legible[same_session_high_link_current] | An ambiguous ``get()`` explains itself, and ``except LibTmuxException`` sees it; case inputs `expected_holders=1`, `select='dup:5'`, `shape='same-session'`, `test_id='same_session_high_link_current'`; fixtures `server`; execute `build_winlink_shape(server, shape)`; expect `pytest.raises(exc.LibTmuxException)`; assert `pane is not None`; `pane.pane_id is not None`; `isinstance(excinfo.value, exc.MultipleObjectsReturned)`; `str(excinfo.value) == f'Multiple objects returned (2): pane_id={pane.pane_id!r}'`. | real-tmux integration test | PythonBehaviorParityTest#ous_point_lookup_is_catchable_and_legible_same_session_high_link_current_a3e42a3a89 | 3.2a through 3.7b | planned parity | -| tests/test_resolution.py::test_ambiguous_point_lookup_is_catchable_and_legible[same_session_low_link_current] | An ambiguous ``get()`` explains itself, and ``except LibTmuxException`` sees it; case inputs `expected_holders=1`, `select='dup:1'`, `shape='same-session'`, `test_id='same_session_low_link_current'`; fixtures `server`; execute `build_winlink_shape(server, shape)`; expect `pytest.raises(exc.LibTmuxException)`; assert `pane is not None`; `pane.pane_id is not None`; `isinstance(excinfo.value, exc.MultipleObjectsReturned)`; `str(excinfo.value) == f'Multiple objects returned (2): pane_id={pane.pane_id!r}'`. | real-tmux integration test | PythonBehaviorParityTest#uous_point_lookup_is_catchable_and_legible_same_session_low_link_current_59dd6a8fab | 3.2a through 3.7b | planned parity | -| tests/test_resolution.py::test_ambiguous_point_lookup_is_catchable_and_legible[same_session_other_window_current] | An ambiguous ``get()`` explains itself, and ``except LibTmuxException`` sees it; case inputs `expected_holders=1`, `select='dup:2'`, `shape='same-session'`, `test_id='same_session_other_window_current'`; fixtures `server`; execute `build_winlink_shape(server, shape)`; expect `pytest.raises(exc.LibTmuxException)`; assert `pane is not None`; `pane.pane_id is not None`; `isinstance(excinfo.value, exc.MultipleObjectsReturned)`; `str(excinfo.value) == f'Multiple objects returned (2): pane_id={pane.pane_id!r}'`. | real-tmux integration test | PythonBehaviorParityTest#_point_lookup_is_catchable_and_legible_same_session_other_window_current_5cb04f11a0 | 3.2a through 3.7b | planned parity | -| tests/test_resolution.py::test_dead_server_raises_libtmux_exception | A dead server is a different failure from a missing object; fixtures `server`, `session`; execute `server.kill()`; `Pane.from_pane_id(server=server, pane_id=pane_id)`; expect `pytest.raises(exc.LibTmuxException)`; assert `pane is not None`; `pane.pane_id is not None`; `not isinstance(excinfo.value, ObjectDoesNotExist)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_resolution_py_test_dead_server_raises_libtmux_exception_75fb5851db | 3.2a through 3.7b | planned parity | -| tests/test_resolution.py::test_linked_pane_resolves_like_tmux | :meth:`Pane.from_pane_id` answers with tmux's own session; fixtures `server`; execute `link_window_into_second_session(server)`; `tmux_resolves_to(server, pane.pane_id)`; `Pane.from_pane_id(server=server, pane_id=pane.pane_id)`; assert `pane is not None`; `pane.pane_id is not None`; `resolved.session_id == canonical`; `resolved.session_id == recent.session_id`; `resolved.session_id != old.session_id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_resolution_py_test_linked_pane_resolves_like_tmux_d48c10e1cd | 3.2a through 3.7b | planned parity | -| tests/test_resolution.py::test_linked_sessions_lists_each_holder_once | :attr:`Window.linked_sessions` dedupes winlinks down to sessions; fixtures `server`; execute `server.new_session(session_name='guest')`; `server.cmd('link-window', '-d', '-s', window.window_id, '-t', f'{guest.session_id}:')`; `link_window_twice_into_one_session(server)`; assert `[s.session_id for s in window.linked_sessions] == [dup.session_id]`; `holders == {dup.session_id, guest.session_id}`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_resolution_py_test_linked_sessions_lists_each_holder_once_ffca9a885b | 3.2a through 3.7b | planned parity | -| tests/test_resolution.py::test_linked_sessions_returns_empty_after_server_kill | A retained window has no knowable holders after its server dies; fixtures `server`, `session`; execute `server.kill()`; assert `list(window.linked_sessions) == []`. | real-tmux integration test | PythonBehaviorParityTest#_test_resolution_py_test_linked_sessions_returns_empty_after_server_kill_68504b2cb6 | 3.2a through 3.7b | planned parity | +| tests/test_pane_capture_pane.py::test_capture_pane_pending_returns_list | ``capture_pane(pending=True)`` returns a list (possibly empty); fixtures `session`; execute `pane.capture_pane(pending=True)`; assert `pane is not None`; `isinstance(result, list)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_capture_pane_py_test_capture_pane_pending_returns_list_9fdd656d0b | 3.3a through 3.7c | planned parity | +| tests/test_pane_capture_pane.py::test_capture_pane_start_end_with_flags | Test that start/end parameters work with new flags; fixtures `session`; execute `session.active_window.split(shell='sh')`; `pane.capture_pane()`; `pane.send_keys('echo "line1"; echo "line2"; echo "line3"', enter=True)`; `pane.capture_pane(start=0, end='-', preserve_trailing=True)`; `'\n'.join(pane.capture_pane())`; `retry_until(prompt_ready, 2, raises=True)`; assert `isinstance(output, list)`; `len(output) > 0`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_capture_pane_py_test_capture_pane_start_end_with_flags_cef758a00c | 3.3a through 3.7c | planned parity | +| tests/test_pane_capture_pane.py::test_capture_pane_to_buffer | Test capture_pane(to_buffer=...) writes to a tmux buffer; fixtures `session`; execute `pane.send_keys('echo "BUFFER_CAPTURE_MARKER"', enter=True)`; `pane.capture_pane()`; `pane.capture_pane(to_buffer='cap_test_buf')`; `session.server.cmd('show-buffer', '-b', 'cap_test_buf')`; `session.server.cmd('delete-buffer', '-b', 'cap_test_buf')`; `retry_until(lambda: 'BUFFER_CAPTURE_MARKER' in '\n'.join(pane.capture_pane()), 2, raises=True)`; assert `pane is not None`; `result is None`; `any(('BUFFER_CAPTURE_MARKER' in line for line in contents))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_capture_pane_py_test_capture_pane_to_buffer_357297abc8 | 3.3a through 3.7c | planned parity | +| tests/test_pane_capture_pane.py::test_capture_pane_trim_trailing_warning | Test that trim_trailing issues a warning on tmux < 3.4; fixtures `session`, `monkeypatch`; execute `session.active_window.split(shell='sh')`; `pane.capture_pane()`; `pane.capture_pane(trim_trailing=True)`; `'\n'.join(pane.capture_pane())`; `retry_until(prompt_ready, 2, raises=True)`; assert `len(w) == 1`; `'trim_trailing requires tmux 3.4+' in str(w[0].message)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_pane_capture_pane_py_test_capture_pane_trim_trailing_warning_106c8f632e | 3.3a through 3.7c | planned parity | +| tests/test_pytest_plugin.py::test_plugin | Test libtmux pytest plugin; fixtures `pytester`, `monkeypatch`; execute `pytester.makefile('.ini', pytest=textwrap.dedent('\n[pytest]\naddopts=-vv\n '.strip()))`; `pytester.makeconftest(textwrap.dedent('\nimport pathlib\nimport pytest\n\n@pytest.fixture(autouse=True)\ndef setup(\n request: pytest.FixtureRequest,\n) -> None:\n pass\n '))`; `pytester.runpytest(str(first_test_filename))`; `textwrap.dedent('\n[pytest]\naddopts=-vv\n '.strip())`; `'\n[pytest]\naddopts=-vv\n '.strip()`; `textwrap.dedent('\nimport pathlib\nimport pytest\n\n@pytest.fixture(autouse=True)\ndef setup(\n request: pytest.FixtureRequest,\n) -> None:\n pass\n ')`. | JUnit extension integration test | PythonBehaviorParityTest#tests_test_pytest_plugin_py_test_plugin_ca9cfd8dd4 | 3.3a through 3.7c | planned parity | +| tests/test_pytest_plugin.py::test_reap_test_server_is_noop_when_socket_missing | Reaping a non-existent socket succeeds silently; execute `_libtmux_socket_dir()`; `socket_path.exists()`; `_reap_test_server(bogus_name)`; assert `not socket_path.exists()`. | JUnit extension integration test | PythonBehaviorParityTest#_test_pytest_plugin_py_test_reap_test_server_is_noop_when_socket_missing_3ad24e7f13 | 3.3a through 3.7c | planned parity | +| tests/test_pytest_plugin.py::test_reap_test_server_tolerates_none | ``_reap_test_server(None)`` is a no-op, not a crash; execute `_reap_test_server(None)`. | JUnit extension integration test | PythonBehaviorParityTest#tests_test_pytest_plugin_py_test_reap_test_server_tolerates_none_0b7646b9f3 | 3.3a through 3.7c | planned parity | +| tests/test_pytest_plugin.py::test_reap_test_server_unlinks_socket_file | ``_reap_test_server`` kills the daemon *and* unlinks the socket; execute `server.new_session(session_name='reap_probe')`; `Server(socket_name='libtmux_test_reap_unlink')`; `_libtmux_socket_dir()`; `socket_path.exists()`; `_reap_test_server('libtmux_test_reap_unlink')`; `socket_path.unlink(missing_ok=True)`; assert `socket_path.exists(); message=f'expected tmux to have created {socket_path}, but it is missing'`; `not socket_path.exists(); message=f'_reap_test_server should have unlinked {socket_path}'`. | JUnit extension integration test | PythonBehaviorParityTest#tests_test_pytest_plugin_py_test_reap_test_server_unlinks_socket_file_ca0bef2b4d | 3.3a through 3.7c | planned parity | +| tests/test_pytest_plugin.py::test_test_server | Test TestServer creates and cleans up server; fixtures `TestServer`; execute `TestServer()`; `server.is_alive()`; `server.new_session()`; `server.socket_name.startswith('libtmux_test')`; `server2.socket_name.startswith('libtmux_test')`; assert `server.is_alive() is False`; `server.is_alive() is True`; `len(server.sessions) == 1`; `session.session_name is not None`; `server.socket_name is not None`; `server.socket_name.startswith('libtmux_test')`; `server2.socket_name is not None`; `server2.socket_name.startswith('libtmux_test')`; `server2.socket_name != server.socket_name`. | JUnit extension integration test | PythonBehaviorParityTest#tests_test_pytest_plugin_py_test_test_server_77772185e3 | 3.3a through 3.7c | planned parity | +| tests/test_pytest_plugin.py::test_test_server_cleanup | Test TestServer properly cleans up after itself; fixtures `TestServer`; execute `TestServer()`; `server.new_session(session_name='test1')`; `server.new_session(session_name='test2')`; `server.is_alive()`; `server.kill()`; `time.sleep(0.1)`; assert `socket_name is not None`; `len(server.sessions) == 2`; `server.is_alive() is True`; `new_server.is_alive() is False`; `new_server.is_alive() is True`. | JUnit extension integration test | PythonBehaviorParityTest#tests_test_pytest_plugin_py_test_test_server_cleanup_89e16902ec | 3.3a through 3.7c | planned parity | +| tests/test_pytest_plugin.py::test_test_server_multiple | Test multiple TestServer instances can coexist; fixtures `TestServer`; execute `TestServer()`; `server1.new_session(session_name='test1')`; `server2.new_session(session_name='test2')`; `any((s.session_name == 'test1' for s in server1.sessions))`; `any((s.session_name == 'test2' for s in server2.sessions))`; `any((s.session_name == 'test1' for s in server2.sessions))`; assert `server1.socket_name != server2.socket_name`; `any((s.session_name == 'test1' for s in server1.sessions))`; `any((s.session_name == 'test2' for s in server2.sessions))`; `not any((s.session_name == 'test1' for s in server2.sessions))`; `not any((s.session_name == 'test2' for s in server1.sessions))`. | JUnit extension integration test | PythonBehaviorParityTest#tests_test_pytest_plugin_py_test_test_server_multiple_c049bb6ff0 | 3.3a through 3.7c | planned parity | +| tests/test_pytest_plugin.py::test_test_server_with_config | Test TestServer with config file; fixtures `TestServer`, `tmp_path`; execute `TestServer(config_file=str(config_file))`; `server.new_session()`; `session.cmd('show-options', '-g', 'status')`; `config_file.write_text('set -g status off', encoding='utf-8')`; assert `session.cmd('show-options', '-g', 'status').stdout[0] == 'status off'`. | JUnit extension integration test | PythonBehaviorParityTest#tests_test_pytest_plugin_py_test_test_server_with_config_6f5a08fc97 | 3.3a through 3.7c | planned parity | +| tests/test_resolution.py::test_ambiguous_point_lookup_is_catchable_and_legible[cross_session] | An ambiguous ``get()`` explains itself, and ``except LibTmuxException`` sees it; case inputs `expected_holders=2`, `select=None`, `shape='cross-session'`, `test_id='cross_session'`; fixtures `server`; execute `build_winlink_shape(server, shape)`; expect `pytest.raises(exc.LibTmuxException)`; assert `pane is not None`; `pane.pane_id is not None`; `isinstance(excinfo.value, exc.MultipleObjectsReturned)`; `str(excinfo.value) == f'Multiple objects returned (2): pane_id={pane.pane_id!r}'`. | real-tmux integration test | PythonBehaviorParityTest#on_py_test_ambiguous_point_lookup_is_catchable_and_legible_cross_session_60f6b26db9 | 3.3a through 3.7c | planned parity | +| tests/test_resolution.py::test_ambiguous_point_lookup_is_catchable_and_legible[grouped_session] | An ambiguous ``get()`` explains itself, and ``except LibTmuxException`` sees it; case inputs `expected_holders=2`, `select=None`, `shape='grouped'`, `test_id='grouped_session'`; fixtures `server`; execute `build_winlink_shape(server, shape)`; expect `pytest.raises(exc.LibTmuxException)`; assert `pane is not None`; `pane.pane_id is not None`; `isinstance(excinfo.value, exc.MultipleObjectsReturned)`; `str(excinfo.value) == f'Multiple objects returned (2): pane_id={pane.pane_id!r}'`. | real-tmux integration test | PythonBehaviorParityTest#_py_test_ambiguous_point_lookup_is_catchable_and_legible_grouped_session_1b1f3ea355 | 3.3a through 3.7c | planned parity | +| tests/test_resolution.py::test_ambiguous_point_lookup_is_catchable_and_legible[same_session_high_link_current] | An ambiguous ``get()`` explains itself, and ``except LibTmuxException`` sees it; case inputs `expected_holders=1`, `select='dup:5'`, `shape='same-session'`, `test_id='same_session_high_link_current'`; fixtures `server`; execute `build_winlink_shape(server, shape)`; expect `pytest.raises(exc.LibTmuxException)`; assert `pane is not None`; `pane.pane_id is not None`; `isinstance(excinfo.value, exc.MultipleObjectsReturned)`; `str(excinfo.value) == f'Multiple objects returned (2): pane_id={pane.pane_id!r}'`. | real-tmux integration test | PythonBehaviorParityTest#ous_point_lookup_is_catchable_and_legible_same_session_high_link_current_a3e42a3a89 | 3.3a through 3.7c | planned parity | +| tests/test_resolution.py::test_ambiguous_point_lookup_is_catchable_and_legible[same_session_low_link_current] | An ambiguous ``get()`` explains itself, and ``except LibTmuxException`` sees it; case inputs `expected_holders=1`, `select='dup:1'`, `shape='same-session'`, `test_id='same_session_low_link_current'`; fixtures `server`; execute `build_winlink_shape(server, shape)`; expect `pytest.raises(exc.LibTmuxException)`; assert `pane is not None`; `pane.pane_id is not None`; `isinstance(excinfo.value, exc.MultipleObjectsReturned)`; `str(excinfo.value) == f'Multiple objects returned (2): pane_id={pane.pane_id!r}'`. | real-tmux integration test | PythonBehaviorParityTest#uous_point_lookup_is_catchable_and_legible_same_session_low_link_current_59dd6a8fab | 3.3a through 3.7c | planned parity | +| tests/test_resolution.py::test_ambiguous_point_lookup_is_catchable_and_legible[same_session_other_window_current] | An ambiguous ``get()`` explains itself, and ``except LibTmuxException`` sees it; case inputs `expected_holders=1`, `select='dup:2'`, `shape='same-session'`, `test_id='same_session_other_window_current'`; fixtures `server`; execute `build_winlink_shape(server, shape)`; expect `pytest.raises(exc.LibTmuxException)`; assert `pane is not None`; `pane.pane_id is not None`; `isinstance(excinfo.value, exc.MultipleObjectsReturned)`; `str(excinfo.value) == f'Multiple objects returned (2): pane_id={pane.pane_id!r}'`. | real-tmux integration test | PythonBehaviorParityTest#_point_lookup_is_catchable_and_legible_same_session_other_window_current_5cb04f11a0 | 3.3a through 3.7c | planned parity | +| tests/test_resolution.py::test_dead_server_raises_libtmux_exception | A dead server is a different failure from a missing object; fixtures `server`, `session`; execute `server.kill()`; `Pane.from_pane_id(server=server, pane_id=pane_id)`; expect `pytest.raises(exc.LibTmuxException)`; assert `pane is not None`; `pane.pane_id is not None`; `not isinstance(excinfo.value, ObjectDoesNotExist)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_resolution_py_test_dead_server_raises_libtmux_exception_75fb5851db | 3.3a through 3.7c | planned parity | +| tests/test_resolution.py::test_linked_pane_resolves_like_tmux | :meth:`Pane.from_pane_id` answers with tmux's own session; fixtures `server`; execute `link_window_into_second_session(server)`; `tmux_resolves_to(server, pane.pane_id)`; `Pane.from_pane_id(server=server, pane_id=pane.pane_id)`; assert `pane is not None`; `pane.pane_id is not None`; `resolved.session_id == canonical`; `resolved.session_id == recent.session_id`; `resolved.session_id != old.session_id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_resolution_py_test_linked_pane_resolves_like_tmux_d48c10e1cd | 3.3a through 3.7c | planned parity | +| tests/test_resolution.py::test_linked_sessions_lists_each_holder_once | :attr:`Window.linked_sessions` dedupes winlinks down to sessions; fixtures `server`; execute `server.new_session(session_name='guest')`; `server.cmd('link-window', '-d', '-s', window.window_id, '-t', f'{guest.session_id}:')`; `link_window_twice_into_one_session(server)`; assert `[s.session_id for s in window.linked_sessions] == [dup.session_id]`; `holders == {dup.session_id, guest.session_id}`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_resolution_py_test_linked_sessions_lists_each_holder_once_ffca9a885b | 3.3a through 3.7c | planned parity | +| tests/test_resolution.py::test_linked_sessions_returns_empty_after_server_kill | A retained window has no knowable holders after its server dies; fixtures `server`, `session`; execute `server.kill()`; assert `list(window.linked_sessions) == []`. | real-tmux integration test | PythonBehaviorParityTest#_test_resolution_py_test_linked_sessions_returns_empty_after_server_kill_68504b2cb6 | 3.3a through 3.7c | planned parity | | tests/test_resolution.py::test_linked_sessions_returns_empty_when_session_snapshot_fails | The second listing shares the accessor's lenient error boundary; fixtures `server`, `monkeypatch`; execute `server.new_session(session_name='holder')`; `real_fetch_objs(**kwargs)`; assert `list(window.linked_sessions) == []`; when `kwargs['list_cmd'] == 'list-sessions'`: execute `exc.LibTmuxException(msg)`; raise `raise exc.LibTmuxException(msg)`. | unit test | PythonBehaviorParityTest#lution_py_test_linked_sessions_returns_empty_when_session_snapshot_fails_27d863cb7b | not tmux-bound | planned parity | | tests/test_resolution.py::test_linked_sessions_skips_holder_lost_between_snapshots | A holder disappearing between real snapshots is skipped safely; fixtures `server`, `monkeypatch`; execute `server.new_session(session_name='home')`; `server.new_session(session_name='guest')`; `server.cmd('link-window', '-d', '-s', window.window_id, '-t', f'{guest.session_id}:')`; `real_fetch_objs(**kwargs)`; assert `[holder.session_id for holder in window.linked_sessions] == [home.session_id]`; when `kwargs['list_cmd'] == 'list-windows'`: execute `guest.kill()`. | unit test | PythonBehaviorParityTest#t_resolution_py_test_linked_sessions_skips_holder_lost_between_snapshots_2a31da8767 | not tmux-bound | planned parity | | tests/test_resolution.py::test_linked_sessions_uses_two_list_calls | Resolving several holders uses two real tmux listings in total; fixtures `server`, `monkeypatch`; execute `server.new_session(session_name=name)`; `server.cmd('link-window', '-d', '-s', window.window_id, '-t', f'{guest.session_id}:')`; `link_window_twice_into_one_session(server)`; `real_fetch_objs(server=server, list_cmd='list-windows', list_extra_args=('-a',))`; `dict.fromkeys((row['session_id'] for row in rows if row.get('window_id') == window.window_id and row.get('session_id')))`; `real_fetch_objs(**kwargs)`; assert `[item.session_id for item in window.linked_sessions] == expected_session_ids`; `list_calls == ['list-windows', 'list-sessions']`. | unit test | PythonBehaviorParityTest#tests_test_resolution_py_test_linked_sessions_uses_two_list_calls_bd8e13fd14 | not tmux-bound | planned parity | -| tests/test_resolution.py::test_linked_window_resolves_like_tmux | :meth:`Window.from_window_id` answers with tmux's own session; fixtures `server`; execute `link_window_into_second_session(server)`; `tmux_resolves_to(server, window.window_id)`; `Window.from_window_id(server=server, window_id=window.window_id)`; assert `window.window_id is not None`; `resolved.session_id == canonical`; `resolved.session_id == recent.session_id`; `resolved.session_id != old.session_id; message="'zzz-old' sorts last by name -- resolving from a `list-windows -a` listing would answer with it"`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_resolution_py_test_linked_window_resolves_like_tmux_4761a30783 | 3.2a through 3.7b | planned parity | -| tests/test_resolution.py::test_missing_pane_on_live_server_raises_object_does_not_exist | A pane id that never existed is a missing object, not a broken server; fixtures `server`, `session`; execute `Pane.from_pane_id(server=server, pane_id='%99999')`; expect `pytest.raises(exc.TmuxObjectDoesNotExist)`. | real-tmux integration test | PythonBehaviorParityTest#olution_py_test_missing_pane_on_live_server_raises_object_does_not_exist_dd9758e520 | 3.2a through 3.7b | planned parity | -| tests/test_resolution.py::test_multiply_linked_window_resolves_like_tmux[cross_session] | A window at several winlinks resolves to the one tmux would act on; case inputs `expected_holders=2`, `select=None`, `shape='cross-session'`, `test_id='cross_session'`; fixtures `server`; execute `window.refresh()`; `build_winlink_shape(server, shape)`; `tmux_resolves_to(server, window.window_id)`; `tmux_resolves_index_to(server, window.window_id)`; `Window.from_window_id(server=server, window_id=window.window_id)`; assert `window.window_id is not None`; `resolved.window_index == canonical_index`; `resolved.session_id == canonical_session`; `window.window_index == canonical_index`; `window.session_id == canonical_session`; `len(window.linked_sessions) == expected_holders`; when `select is not None`: execute `server.cmd('select-window', '-t', select)`. | real-tmux integration test | PythonBehaviorParityTest#solution_py_test_multiply_linked_window_resolves_like_tmux_cross_session_d0c070463f | 3.2a through 3.7b | planned parity | -| tests/test_resolution.py::test_multiply_linked_window_resolves_like_tmux[grouped_session] | A window at several winlinks resolves to the one tmux would act on; case inputs `expected_holders=2`, `select=None`, `shape='grouped'`, `test_id='grouped_session'`; fixtures `server`; execute `window.refresh()`; `build_winlink_shape(server, shape)`; `tmux_resolves_to(server, window.window_id)`; `tmux_resolves_index_to(server, window.window_id)`; `Window.from_window_id(server=server, window_id=window.window_id)`; assert `window.window_id is not None`; `resolved.window_index == canonical_index`; `resolved.session_id == canonical_session`; `window.window_index == canonical_index`; `window.session_id == canonical_session`; `len(window.linked_sessions) == expected_holders`; when `select is not None`: execute `server.cmd('select-window', '-t', select)`. | real-tmux integration test | PythonBehaviorParityTest#lution_py_test_multiply_linked_window_resolves_like_tmux_grouped_session_bbe7f1e42b | 3.2a through 3.7b | planned parity | -| tests/test_resolution.py::test_multiply_linked_window_resolves_like_tmux[same_session_high_link_current] | A window at several winlinks resolves to the one tmux would act on; case inputs `expected_holders=1`, `select='dup:5'`, `shape='same-session'`, `test_id='same_session_high_link_current'`; fixtures `server`; execute `window.refresh()`; `build_winlink_shape(server, shape)`; `tmux_resolves_to(server, window.window_id)`; `tmux_resolves_index_to(server, window.window_id)`; `Window.from_window_id(server=server, window_id=window.window_id)`; assert `window.window_id is not None`; `resolved.window_index == canonical_index`; `resolved.session_id == canonical_session`; `window.window_index == canonical_index`; `window.session_id == canonical_session`; `len(window.linked_sessions) == expected_holders`; when `select is not None`: execute `server.cmd('select-window', '-t', select)`. | real-tmux integration test | PythonBehaviorParityTest#multiply_linked_window_resolves_like_tmux_same_session_high_link_current_b625449083 | 3.2a through 3.7b | planned parity | -| tests/test_resolution.py::test_multiply_linked_window_resolves_like_tmux[same_session_low_link_current] | A window at several winlinks resolves to the one tmux would act on; case inputs `expected_holders=1`, `select='dup:1'`, `shape='same-session'`, `test_id='same_session_low_link_current'`; fixtures `server`; execute `window.refresh()`; `build_winlink_shape(server, shape)`; `tmux_resolves_to(server, window.window_id)`; `tmux_resolves_index_to(server, window.window_id)`; `Window.from_window_id(server=server, window_id=window.window_id)`; assert `window.window_id is not None`; `resolved.window_index == canonical_index`; `resolved.session_id == canonical_session`; `window.window_index == canonical_index`; `window.session_id == canonical_session`; `len(window.linked_sessions) == expected_holders`; when `select is not None`: execute `server.cmd('select-window', '-t', select)`. | real-tmux integration test | PythonBehaviorParityTest#_multiply_linked_window_resolves_like_tmux_same_session_low_link_current_e4c66875a3 | 3.2a through 3.7b | planned parity | -| tests/test_resolution.py::test_multiply_linked_window_resolves_like_tmux[same_session_other_window_current] | A window at several winlinks resolves to the one tmux would act on; case inputs `expected_holders=1`, `select='dup:2'`, `shape='same-session'`, `test_id='same_session_other_window_current'`; fixtures `server`; execute `window.refresh()`; `build_winlink_shape(server, shape)`; `tmux_resolves_to(server, window.window_id)`; `tmux_resolves_index_to(server, window.window_id)`; `Window.from_window_id(server=server, window_id=window.window_id)`; assert `window.window_id is not None`; `resolved.window_index == canonical_index`; `resolved.session_id == canonical_session`; `window.window_index == canonical_index`; `window.session_id == canonical_session`; `len(window.linked_sessions) == expected_holders`; when `select is not None`: execute `server.cmd('select-window', '-t', select)`. | real-tmux integration test | PythonBehaviorParityTest#tiply_linked_window_resolves_like_tmux_same_session_other_window_current_3c8159b733 | 3.2a through 3.7b | planned parity | -| tests/test_resolution.py::test_pane_of_multiply_linked_window_agrees_with_its_window | :meth:`Pane.from_pane_id` and :meth:`Window.from_window_id` do not disagree; fixtures `server`; execute `server.cmd('select-window', '-t', 'dup:1')`; `link_window_twice_into_one_session(server)`; `Pane.from_pane_id(server=server, pane_id=pane.pane_id)`; `Window.from_window_id(server=server, window_id=window.window_id)`; `tmux_resolves_index_to(server, pane.pane_id)`; assert `pane is not None`; `pane.pane_id is not None`; `window.window_id is not None`; `resolved_pane.window_index == resolved_window.window_index`; `resolved_pane.window_index == tmux_resolves_index_to(server, pane.pane_id)`. | real-tmux integration test | PythonBehaviorParityTest#resolution_py_test_pane_of_multiply_linked_window_agrees_with_its_window_21df30be0c | 3.2a through 3.7b | planned parity | -| tests/test_resolution.py::test_pane_session_follows_move_window | A :class:`Pane` held across a ``move-window`` still finds its session; fixtures `server`; execute `server.new_session(session_name='origin')`; `server.new_session(session_name='destination')`; `window.move_window(destination='99', session=destination.session_id)`; assert `pane is not None`; `pane.session.session_id == destination.session_id`; `pane.window.session_id == destination.session_id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_resolution_py_test_pane_session_follows_move_window_eda41d9e14 | 3.2a through 3.7b | planned parity | -| tests/test_resolution.py::test_point_lookup_by_id_is_the_way_out | The documented escape from an ambiguous scan: name the id with ``-t``; fixtures `server`; execute `link_window_into_second_session(server)`; `Pane.from_pane_id(server, pane.pane_id)`; `Window.from_window_id(server, window.window_id)`; assert `pane is not None`; `pane.pane_id is not None`; `window.window_id is not None`; `Pane.from_pane_id(server, pane.pane_id).pane_id == pane.pane_id`; `Window.from_window_id(server, window.window_id).window_id == window.window_id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_resolution_py_test_point_lookup_by_id_is_the_way_out_50e77c5830 | 3.2a through 3.7b | planned parity | -| tests/test_resolution.py::test_refresh_after_kill_raises_object_does_not_exist[pane] | A gone object still raises :exc:`~libtmux.exc.TmuxObjectDoesNotExist`; case inputs `kind='pane'`, `test_id='pane'`; fixtures `session`; execute `session.new_window(window_name='doomed')`; `target.refresh()`; expect `pytest.raises(ObjectDoesNotExist)`; when `not (kind == 'window')`: execute `window.split()`; `pane.kill()`; when `kind == 'window'`: execute `window.kill()`. | real-tmux integration test | PythonBehaviorParityTest#_resolution_py_test_refresh_after_kill_raises_object_does_not_exist_pane_cf722778bb | 3.2a through 3.7b | planned parity | -| tests/test_resolution.py::test_refresh_after_kill_raises_object_does_not_exist[window] | A gone object still raises :exc:`~libtmux.exc.TmuxObjectDoesNotExist`; case inputs `kind='window'`, `test_id='window'`; fixtures `session`; execute `session.new_window(window_name='doomed')`; `target.refresh()`; expect `pytest.raises(ObjectDoesNotExist)`; when `not (kind == 'window')`: execute `window.split()`; `pane.kill()`; when `kind == 'window'`: execute `window.kill()`. | real-tmux integration test | PythonBehaviorParityTest#esolution_py_test_refresh_after_kill_raises_object_does_not_exist_window_4d7db37bb1 | 3.2a through 3.7b | planned parity | -| tests/test_resolution.py::test_same_session_double_link_answers_the_low_index | The regression in its narrowest form: tmux says 1, libtmux said 5; fixtures `server`; execute `server.cmd('select-window', '-t', 'dup:1')`; `link_window_twice_into_one_session(server)`; `tmux_resolves_index_to(server, window.window_id)`; `Window.from_window_id(server, window.window_id)`; assert `window.window_id is not None`; `tmux_resolves_index_to(server, window.window_id) == '1'`; `Window.from_window_id(server, window.window_id).window_index == '1'`. | real-tmux integration test | PythonBehaviorParityTest#s_test_resolution_py_test_same_session_double_link_answers_the_low_index_d2dbe2c977 | 3.2a through 3.7b | planned parity | -| tests/test_resolution.py::test_server_collections_enumerate_winlinks[cross_session] | A server-wide listing yields one row per winlink, and that is the truth; case inputs `expected_holders=2`, `select=None`, `shape='cross-session'`, `test_id='cross_session'`; fixtures `server`; execute `server.windows.filter(window_id=window.window_id)`; `server.panes.filter(pane_id=pane.pane_id)`; `build_winlink_shape(server, shape)`; assert `window.window_id is not None`; `pane is not None`; `pane.pane_id is not None`; `len(window_rows) == 2`; `len(pane_rows) == 2`; `{row.window_id for row in window_rows} == {window.window_id}`; `len(winlinks) == 2`. | real-tmux integration test | PythonBehaviorParityTest#t_resolution_py_test_server_collections_enumerate_winlinks_cross_session_b36c1af7f4 | 3.2a through 3.7b | planned parity | -| tests/test_resolution.py::test_server_collections_enumerate_winlinks[grouped_session] | A server-wide listing yields one row per winlink, and that is the truth; case inputs `expected_holders=2`, `select=None`, `shape='grouped'`, `test_id='grouped_session'`; fixtures `server`; execute `server.windows.filter(window_id=window.window_id)`; `server.panes.filter(pane_id=pane.pane_id)`; `build_winlink_shape(server, shape)`; assert `window.window_id is not None`; `pane is not None`; `pane.pane_id is not None`; `len(window_rows) == 2`; `len(pane_rows) == 2`; `{row.window_id for row in window_rows} == {window.window_id}`; `len(winlinks) == 2`. | real-tmux integration test | PythonBehaviorParityTest#resolution_py_test_server_collections_enumerate_winlinks_grouped_session_f890f4c69f | 3.2a through 3.7b | planned parity | -| tests/test_resolution.py::test_server_collections_enumerate_winlinks[same_session_high_link_current] | A server-wide listing yields one row per winlink, and that is the truth; case inputs `expected_holders=1`, `select='dup:5'`, `shape='same-session'`, `test_id='same_session_high_link_current'`; fixtures `server`; execute `server.windows.filter(window_id=window.window_id)`; `server.panes.filter(pane_id=pane.pane_id)`; `build_winlink_shape(server, shape)`; assert `window.window_id is not None`; `pane is not None`; `pane.pane_id is not None`; `len(window_rows) == 2`; `len(pane_rows) == 2`; `{row.window_id for row in window_rows} == {window.window_id}`; `len(winlinks) == 2`. | real-tmux integration test | PythonBehaviorParityTest#est_server_collections_enumerate_winlinks_same_session_high_link_current_3ccca980a1 | 3.2a through 3.7b | planned parity | -| tests/test_resolution.py::test_server_collections_enumerate_winlinks[same_session_low_link_current] | A server-wide listing yields one row per winlink, and that is the truth; case inputs `expected_holders=1`, `select='dup:1'`, `shape='same-session'`, `test_id='same_session_low_link_current'`; fixtures `server`; execute `server.windows.filter(window_id=window.window_id)`; `server.panes.filter(pane_id=pane.pane_id)`; `build_winlink_shape(server, shape)`; assert `window.window_id is not None`; `pane is not None`; `pane.pane_id is not None`; `len(window_rows) == 2`; `len(pane_rows) == 2`; `{row.window_id for row in window_rows} == {window.window_id}`; `len(winlinks) == 2`. | real-tmux integration test | PythonBehaviorParityTest#test_server_collections_enumerate_winlinks_same_session_low_link_current_67f97dd930 | 3.2a through 3.7b | planned parity | -| tests/test_resolution.py::test_server_collections_enumerate_winlinks[same_session_other_window_current] | A server-wide listing yields one row per winlink, and that is the truth; case inputs `expected_holders=1`, `select='dup:2'`, `shape='same-session'`, `test_id='same_session_other_window_current'`; fixtures `server`; execute `server.windows.filter(window_id=window.window_id)`; `server.panes.filter(pane_id=pane.pane_id)`; `build_winlink_shape(server, shape)`; assert `window.window_id is not None`; `pane is not None`; `pane.pane_id is not None`; `len(window_rows) == 2`; `len(pane_rows) == 2`; `{row.window_id for row in window_rows} == {window.window_id}`; `len(winlinks) == 2`. | real-tmux integration test | PythonBehaviorParityTest#_server_collections_enumerate_winlinks_same_session_other_window_current_17447cf546 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_256_colors | Assert Server respects ``colors=256``; fixtures `server`; execute `Server(colors=256)`; `myserver.cmd('list-sessions')`; assert `myserver.colors == 256`; `'-2' in proc.cmd`; `'-8' not in proc.cmd`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_256_colors_1af3e21b1c | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_88_colors | Assert Server respects ``colors=88``; fixtures `server`; execute `Server(colors=88)`; `myserver.cmd('list-sessions')`; assert `myserver.colors == 88`; `'-8' in proc.cmd`; `'-2' not in proc.cmd`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_88_colors_a24b676a0a | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_bind_unbind_key | Test Server.bind_key() and unbind_key() cycle; fixtures `server`; execute `server.new_session(session_name='bind_test')`; `server.bind_key('F12', 'display-message bound', key_table='root')`; `server.list_keys(key_table='root')`; `server.unbind_key('F12', key_table='root')`; `any(('F12' in line for line in keys))`; `any(('F12' in line and 'display-message' in line for line in keys))`; assert `any(('F12' in line for line in keys))`; `not any(('F12' in line and 'display-message' in line for line in keys))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_bind_unbind_key_d77633139e | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_buffer_delete | Test Server.delete_buffer(); fixtures `server`; execute `server.new_session(session_name='buf_delete')`; `server.set_buffer('to_delete', buffer_name='del_buf')`; `server.show_buffer(buffer_name='del_buf')`; `server.delete_buffer(buffer_name='del_buf')`; expect `pytest.raises(exc.LibTmuxException)`; assert `server.show_buffer(buffer_name='del_buf') == 'to_delete'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_buffer_delete_76248eed4e | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_buffer_save_append | Test Server.save_buffer() with append flag; fixtures `server`, `tmp_path`; execute `server.new_session(session_name='buf_saveappend')`; `server.set_buffer('first_line', buffer_name='app1')`; `server.save_buffer(buf_file, buffer_name='app1')`; `server.set_buffer('second_line', buffer_name='app2')`; `server.save_buffer(buf_file, buffer_name='app2', append=True)`; `buf_file.read_text()`; assert `'first_line' in content`; `'second_line' in content`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_buffer_save_append_803c177e17 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_buffer_save_load | Test Server.save_buffer() and load_buffer() cycle; fixtures `server`, `tmp_path`; execute `server.new_session(session_name='buf_saveload')`; `server.set_buffer('save_test_data')`; `server.save_buffer(buf_file)`; `server.load_buffer(buf_file, buffer_name='loaded_buf')`; `server.show_buffer(buffer_name='loaded_buf')`; `buf_file.read_text()`; assert `buf_file.read_text() == 'save_test_data'`; `server.show_buffer(buffer_name='loaded_buf') == 'save_test_data'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_buffer_save_load_f2d73900f7 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_buffer_set_show[set_show_append] | Test Server.set_buffer() and show_buffer() cycle; case inputs `append=True`, `buffer_name='append_test'`, `data='_second'`, `expected_content='first_second'`, `test_id='set_show_append'`; fixtures `server`; execute `server.new_session(session_name=f'buf_{test_id}')`; `server.set_buffer(data, **kwargs)`; `server.show_buffer(buffer_name=buffer_name)`; assert `result == expected_content`; when `append`: execute `server.set_buffer('first', buffer_name=buffer_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_buffer_set_show_set_show_append_123d743887 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_buffer_set_show[set_show_default] | Test Server.set_buffer() and show_buffer() cycle; case inputs `append=None`, `buffer_name=None`, `data='hello_buf'`, `expected_content='hello_buf'`, `test_id='set_show_default'`; fixtures `server`; execute `server.new_session(session_name=f'buf_{test_id}')`; `server.set_buffer(data, **kwargs)`; `server.show_buffer(buffer_name=buffer_name)`; assert `result == expected_content`; when `append`: execute `server.set_buffer('first', buffer_name=buffer_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_buffer_set_show_set_show_default_a2f68ba5f6 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_buffer_set_show[set_show_named] | Test Server.set_buffer() and show_buffer() cycle; case inputs `append=None`, `buffer_name='mybuf'`, `data='named_data'`, `expected_content='named_data'`, `test_id='set_show_named'`; fixtures `server`; execute `server.new_session(session_name=f'buf_{test_id}')`; `server.set_buffer(data, **kwargs)`; `server.show_buffer(buffer_name=buffer_name)`; assert `result == expected_content`; when `append`: execute `server.set_buffer('first', buffer_name=buffer_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_buffer_set_show_set_show_named_502e44630a | 3.2a through 3.7b | planned parity | +| tests/test_resolution.py::test_linked_window_resolves_like_tmux | :meth:`Window.from_window_id` answers with tmux's own session; fixtures `server`; execute `link_window_into_second_session(server)`; `tmux_resolves_to(server, window.window_id)`; `Window.from_window_id(server=server, window_id=window.window_id)`; assert `window.window_id is not None`; `resolved.session_id == canonical`; `resolved.session_id == recent.session_id`; `resolved.session_id != old.session_id; message="'zzz-old' sorts last by name -- resolving from a `list-windows -a` listing would answer with it"`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_resolution_py_test_linked_window_resolves_like_tmux_4761a30783 | 3.3a through 3.7c | planned parity | +| tests/test_resolution.py::test_missing_pane_on_live_server_raises_object_does_not_exist | A pane id that never existed is a missing object, not a broken server; fixtures `server`, `session`; execute `Pane.from_pane_id(server=server, pane_id='%99999')`; expect `pytest.raises(exc.TmuxObjectDoesNotExist)`. | real-tmux integration test | PythonBehaviorParityTest#olution_py_test_missing_pane_on_live_server_raises_object_does_not_exist_dd9758e520 | 3.3a through 3.7c | planned parity | +| tests/test_resolution.py::test_multiply_linked_window_resolves_like_tmux[cross_session] | A window at several winlinks resolves to the one tmux would act on; case inputs `expected_holders=2`, `select=None`, `shape='cross-session'`, `test_id='cross_session'`; fixtures `server`; execute `window.refresh()`; `build_winlink_shape(server, shape)`; `tmux_resolves_to(server, window.window_id)`; `tmux_resolves_index_to(server, window.window_id)`; `Window.from_window_id(server=server, window_id=window.window_id)`; assert `window.window_id is not None`; `resolved.window_index == canonical_index`; `resolved.session_id == canonical_session`; `window.window_index == canonical_index`; `window.session_id == canonical_session`; `len(window.linked_sessions) == expected_holders`; when `select is not None`: execute `server.cmd('select-window', '-t', select)`. | real-tmux integration test | PythonBehaviorParityTest#solution_py_test_multiply_linked_window_resolves_like_tmux_cross_session_d0c070463f | 3.3a through 3.7c | planned parity | +| tests/test_resolution.py::test_multiply_linked_window_resolves_like_tmux[grouped_session] | A window at several winlinks resolves to the one tmux would act on; case inputs `expected_holders=2`, `select=None`, `shape='grouped'`, `test_id='grouped_session'`; fixtures `server`; execute `window.refresh()`; `build_winlink_shape(server, shape)`; `tmux_resolves_to(server, window.window_id)`; `tmux_resolves_index_to(server, window.window_id)`; `Window.from_window_id(server=server, window_id=window.window_id)`; assert `window.window_id is not None`; `resolved.window_index == canonical_index`; `resolved.session_id == canonical_session`; `window.window_index == canonical_index`; `window.session_id == canonical_session`; `len(window.linked_sessions) == expected_holders`; when `select is not None`: execute `server.cmd('select-window', '-t', select)`. | real-tmux integration test | PythonBehaviorParityTest#lution_py_test_multiply_linked_window_resolves_like_tmux_grouped_session_bbe7f1e42b | 3.3a through 3.7c | planned parity | +| tests/test_resolution.py::test_multiply_linked_window_resolves_like_tmux[same_session_high_link_current] | A window at several winlinks resolves to the one tmux would act on; case inputs `expected_holders=1`, `select='dup:5'`, `shape='same-session'`, `test_id='same_session_high_link_current'`; fixtures `server`; execute `window.refresh()`; `build_winlink_shape(server, shape)`; `tmux_resolves_to(server, window.window_id)`; `tmux_resolves_index_to(server, window.window_id)`; `Window.from_window_id(server=server, window_id=window.window_id)`; assert `window.window_id is not None`; `resolved.window_index == canonical_index`; `resolved.session_id == canonical_session`; `window.window_index == canonical_index`; `window.session_id == canonical_session`; `len(window.linked_sessions) == expected_holders`; when `select is not None`: execute `server.cmd('select-window', '-t', select)`. | real-tmux integration test | PythonBehaviorParityTest#multiply_linked_window_resolves_like_tmux_same_session_high_link_current_b625449083 | 3.3a through 3.7c | planned parity | +| tests/test_resolution.py::test_multiply_linked_window_resolves_like_tmux[same_session_low_link_current] | A window at several winlinks resolves to the one tmux would act on; case inputs `expected_holders=1`, `select='dup:1'`, `shape='same-session'`, `test_id='same_session_low_link_current'`; fixtures `server`; execute `window.refresh()`; `build_winlink_shape(server, shape)`; `tmux_resolves_to(server, window.window_id)`; `tmux_resolves_index_to(server, window.window_id)`; `Window.from_window_id(server=server, window_id=window.window_id)`; assert `window.window_id is not None`; `resolved.window_index == canonical_index`; `resolved.session_id == canonical_session`; `window.window_index == canonical_index`; `window.session_id == canonical_session`; `len(window.linked_sessions) == expected_holders`; when `select is not None`: execute `server.cmd('select-window', '-t', select)`. | real-tmux integration test | PythonBehaviorParityTest#_multiply_linked_window_resolves_like_tmux_same_session_low_link_current_e4c66875a3 | 3.3a through 3.7c | planned parity | +| tests/test_resolution.py::test_multiply_linked_window_resolves_like_tmux[same_session_other_window_current] | A window at several winlinks resolves to the one tmux would act on; case inputs `expected_holders=1`, `select='dup:2'`, `shape='same-session'`, `test_id='same_session_other_window_current'`; fixtures `server`; execute `window.refresh()`; `build_winlink_shape(server, shape)`; `tmux_resolves_to(server, window.window_id)`; `tmux_resolves_index_to(server, window.window_id)`; `Window.from_window_id(server=server, window_id=window.window_id)`; assert `window.window_id is not None`; `resolved.window_index == canonical_index`; `resolved.session_id == canonical_session`; `window.window_index == canonical_index`; `window.session_id == canonical_session`; `len(window.linked_sessions) == expected_holders`; when `select is not None`: execute `server.cmd('select-window', '-t', select)`. | real-tmux integration test | PythonBehaviorParityTest#tiply_linked_window_resolves_like_tmux_same_session_other_window_current_3c8159b733 | 3.3a through 3.7c | planned parity | +| tests/test_resolution.py::test_pane_of_multiply_linked_window_agrees_with_its_window | :meth:`Pane.from_pane_id` and :meth:`Window.from_window_id` do not disagree; fixtures `server`; execute `server.cmd('select-window', '-t', 'dup:1')`; `link_window_twice_into_one_session(server)`; `Pane.from_pane_id(server=server, pane_id=pane.pane_id)`; `Window.from_window_id(server=server, window_id=window.window_id)`; `tmux_resolves_index_to(server, pane.pane_id)`; assert `pane is not None`; `pane.pane_id is not None`; `window.window_id is not None`; `resolved_pane.window_index == resolved_window.window_index`; `resolved_pane.window_index == tmux_resolves_index_to(server, pane.pane_id)`. | real-tmux integration test | PythonBehaviorParityTest#resolution_py_test_pane_of_multiply_linked_window_agrees_with_its_window_21df30be0c | 3.3a through 3.7c | planned parity | +| tests/test_resolution.py::test_pane_session_follows_move_window | A :class:`Pane` held across a ``move-window`` still finds its session; fixtures `server`; execute `server.new_session(session_name='origin')`; `server.new_session(session_name='destination')`; `window.move_window(destination='99', session=destination.session_id)`; assert `pane is not None`; `pane.session.session_id == destination.session_id`; `pane.window.session_id == destination.session_id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_resolution_py_test_pane_session_follows_move_window_eda41d9e14 | 3.3a through 3.7c | planned parity | +| tests/test_resolution.py::test_point_lookup_by_id_is_the_way_out | The documented escape from an ambiguous scan: name the id with ``-t``; fixtures `server`; execute `link_window_into_second_session(server)`; `Pane.from_pane_id(server, pane.pane_id)`; `Window.from_window_id(server, window.window_id)`; assert `pane is not None`; `pane.pane_id is not None`; `window.window_id is not None`; `Pane.from_pane_id(server, pane.pane_id).pane_id == pane.pane_id`; `Window.from_window_id(server, window.window_id).window_id == window.window_id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_resolution_py_test_point_lookup_by_id_is_the_way_out_50e77c5830 | 3.3a through 3.7c | planned parity | +| tests/test_resolution.py::test_refresh_after_kill_raises_object_does_not_exist[pane] | A gone object still raises :exc:`~libtmux.exc.TmuxObjectDoesNotExist`; case inputs `kind='pane'`, `test_id='pane'`; fixtures `session`; execute `session.new_window(window_name='doomed')`; `target.refresh()`; expect `pytest.raises(ObjectDoesNotExist)`; when `not (kind == 'window')`: execute `window.split()`; `pane.kill()`; when `kind == 'window'`: execute `window.kill()`. | real-tmux integration test | PythonBehaviorParityTest#_resolution_py_test_refresh_after_kill_raises_object_does_not_exist_pane_cf722778bb | 3.3a through 3.7c | planned parity | +| tests/test_resolution.py::test_refresh_after_kill_raises_object_does_not_exist[window] | A gone object still raises :exc:`~libtmux.exc.TmuxObjectDoesNotExist`; case inputs `kind='window'`, `test_id='window'`; fixtures `session`; execute `session.new_window(window_name='doomed')`; `target.refresh()`; expect `pytest.raises(ObjectDoesNotExist)`; when `not (kind == 'window')`: execute `window.split()`; `pane.kill()`; when `kind == 'window'`: execute `window.kill()`. | real-tmux integration test | PythonBehaviorParityTest#esolution_py_test_refresh_after_kill_raises_object_does_not_exist_window_4d7db37bb1 | 3.3a through 3.7c | planned parity | +| tests/test_resolution.py::test_same_session_double_link_answers_the_low_index | The regression in its narrowest form: tmux says 1, libtmux said 5; fixtures `server`; execute `server.cmd('select-window', '-t', 'dup:1')`; `link_window_twice_into_one_session(server)`; `tmux_resolves_index_to(server, window.window_id)`; `Window.from_window_id(server, window.window_id)`; assert `window.window_id is not None`; `tmux_resolves_index_to(server, window.window_id) == '1'`; `Window.from_window_id(server, window.window_id).window_index == '1'`. | real-tmux integration test | PythonBehaviorParityTest#s_test_resolution_py_test_same_session_double_link_answers_the_low_index_d2dbe2c977 | 3.3a through 3.7c | planned parity | +| tests/test_resolution.py::test_server_collections_enumerate_winlinks[cross_session] | A server-wide listing yields one row per winlink, and that is the truth; case inputs `expected_holders=2`, `select=None`, `shape='cross-session'`, `test_id='cross_session'`; fixtures `server`; execute `server.windows.filter(window_id=window.window_id)`; `server.panes.filter(pane_id=pane.pane_id)`; `build_winlink_shape(server, shape)`; assert `window.window_id is not None`; `pane is not None`; `pane.pane_id is not None`; `len(window_rows) == 2`; `len(pane_rows) == 2`; `{row.window_id for row in window_rows} == {window.window_id}`; `len(winlinks) == 2`. | real-tmux integration test | PythonBehaviorParityTest#t_resolution_py_test_server_collections_enumerate_winlinks_cross_session_b36c1af7f4 | 3.3a through 3.7c | planned parity | +| tests/test_resolution.py::test_server_collections_enumerate_winlinks[grouped_session] | A server-wide listing yields one row per winlink, and that is the truth; case inputs `expected_holders=2`, `select=None`, `shape='grouped'`, `test_id='grouped_session'`; fixtures `server`; execute `server.windows.filter(window_id=window.window_id)`; `server.panes.filter(pane_id=pane.pane_id)`; `build_winlink_shape(server, shape)`; assert `window.window_id is not None`; `pane is not None`; `pane.pane_id is not None`; `len(window_rows) == 2`; `len(pane_rows) == 2`; `{row.window_id for row in window_rows} == {window.window_id}`; `len(winlinks) == 2`. | real-tmux integration test | PythonBehaviorParityTest#resolution_py_test_server_collections_enumerate_winlinks_grouped_session_f890f4c69f | 3.3a through 3.7c | planned parity | +| tests/test_resolution.py::test_server_collections_enumerate_winlinks[same_session_high_link_current] | A server-wide listing yields one row per winlink, and that is the truth; case inputs `expected_holders=1`, `select='dup:5'`, `shape='same-session'`, `test_id='same_session_high_link_current'`; fixtures `server`; execute `server.windows.filter(window_id=window.window_id)`; `server.panes.filter(pane_id=pane.pane_id)`; `build_winlink_shape(server, shape)`; assert `window.window_id is not None`; `pane is not None`; `pane.pane_id is not None`; `len(window_rows) == 2`; `len(pane_rows) == 2`; `{row.window_id for row in window_rows} == {window.window_id}`; `len(winlinks) == 2`. | real-tmux integration test | PythonBehaviorParityTest#est_server_collections_enumerate_winlinks_same_session_high_link_current_3ccca980a1 | 3.3a through 3.7c | planned parity | +| tests/test_resolution.py::test_server_collections_enumerate_winlinks[same_session_low_link_current] | A server-wide listing yields one row per winlink, and that is the truth; case inputs `expected_holders=1`, `select='dup:1'`, `shape='same-session'`, `test_id='same_session_low_link_current'`; fixtures `server`; execute `server.windows.filter(window_id=window.window_id)`; `server.panes.filter(pane_id=pane.pane_id)`; `build_winlink_shape(server, shape)`; assert `window.window_id is not None`; `pane is not None`; `pane.pane_id is not None`; `len(window_rows) == 2`; `len(pane_rows) == 2`; `{row.window_id for row in window_rows} == {window.window_id}`; `len(winlinks) == 2`. | real-tmux integration test | PythonBehaviorParityTest#test_server_collections_enumerate_winlinks_same_session_low_link_current_67f97dd930 | 3.3a through 3.7c | planned parity | +| tests/test_resolution.py::test_server_collections_enumerate_winlinks[same_session_other_window_current] | A server-wide listing yields one row per winlink, and that is the truth; case inputs `expected_holders=1`, `select='dup:2'`, `shape='same-session'`, `test_id='same_session_other_window_current'`; fixtures `server`; execute `server.windows.filter(window_id=window.window_id)`; `server.panes.filter(pane_id=pane.pane_id)`; `build_winlink_shape(server, shape)`; assert `window.window_id is not None`; `pane is not None`; `pane.pane_id is not None`; `len(window_rows) == 2`; `len(pane_rows) == 2`; `{row.window_id for row in window_rows} == {window.window_id}`; `len(winlinks) == 2`. | real-tmux integration test | PythonBehaviorParityTest#_server_collections_enumerate_winlinks_same_session_other_window_current_17447cf546 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_256_colors | Assert Server respects ``colors=256``; fixtures `server`; execute `Server(colors=256)`; `myserver.cmd('list-sessions')`; assert `myserver.colors == 256`; `'-2' in proc.cmd`; `'-8' not in proc.cmd`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_256_colors_1af3e21b1c | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_88_colors | Assert Server respects ``colors=88``; fixtures `server`; execute `Server(colors=88)`; `myserver.cmd('list-sessions')`; assert `myserver.colors == 88`; `'-8' in proc.cmd`; `'-2' not in proc.cmd`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_88_colors_a24b676a0a | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_bind_unbind_key | Test Server.bind_key() and unbind_key() cycle; fixtures `server`; execute `server.new_session(session_name='bind_test')`; `server.bind_key('F12', 'display-message bound', key_table='root')`; `server.list_keys(key_table='root')`; `server.unbind_key('F12', key_table='root')`; `any(('F12' in line for line in keys))`; `any(('F12' in line and 'display-message' in line for line in keys))`; assert `any(('F12' in line for line in keys))`; `not any(('F12' in line and 'display-message' in line for line in keys))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_bind_unbind_key_d77633139e | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_buffer_delete | Test Server.delete_buffer(); fixtures `server`; execute `server.new_session(session_name='buf_delete')`; `server.set_buffer('to_delete', buffer_name='del_buf')`; `server.show_buffer(buffer_name='del_buf')`; `server.delete_buffer(buffer_name='del_buf')`; expect `pytest.raises(exc.LibTmuxException)`; assert `server.show_buffer(buffer_name='del_buf') == 'to_delete'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_buffer_delete_76248eed4e | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_buffer_save_append | Test Server.save_buffer() with append flag; fixtures `server`, `tmp_path`; execute `server.new_session(session_name='buf_saveappend')`; `server.set_buffer('first_line', buffer_name='app1')`; `server.save_buffer(buf_file, buffer_name='app1')`; `server.set_buffer('second_line', buffer_name='app2')`; `server.save_buffer(buf_file, buffer_name='app2', append=True)`; `buf_file.read_text()`; assert `'first_line' in content`; `'second_line' in content`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_buffer_save_append_803c177e17 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_buffer_save_load | Test Server.save_buffer() and load_buffer() cycle; fixtures `server`, `tmp_path`; execute `server.new_session(session_name='buf_saveload')`; `server.set_buffer('save_test_data')`; `server.save_buffer(buf_file)`; `server.load_buffer(buf_file, buffer_name='loaded_buf')`; `server.show_buffer(buffer_name='loaded_buf')`; `buf_file.read_text()`; assert `buf_file.read_text() == 'save_test_data'`; `server.show_buffer(buffer_name='loaded_buf') == 'save_test_data'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_buffer_save_load_f2d73900f7 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_buffer_set_show[set_show_append] | Test Server.set_buffer() and show_buffer() cycle; case inputs `append=True`, `buffer_name='append_test'`, `data='_second'`, `expected_content='first_second'`, `test_id='set_show_append'`; fixtures `server`; execute `server.new_session(session_name=f'buf_{test_id}')`; `server.set_buffer(data, **kwargs)`; `server.show_buffer(buffer_name=buffer_name)`; assert `result == expected_content`; when `append`: execute `server.set_buffer('first', buffer_name=buffer_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_buffer_set_show_set_show_append_123d743887 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_buffer_set_show[set_show_default] | Test Server.set_buffer() and show_buffer() cycle; case inputs `append=None`, `buffer_name=None`, `data='hello_buf'`, `expected_content='hello_buf'`, `test_id='set_show_default'`; fixtures `server`; execute `server.new_session(session_name=f'buf_{test_id}')`; `server.set_buffer(data, **kwargs)`; `server.show_buffer(buffer_name=buffer_name)`; assert `result == expected_content`; when `append`: execute `server.set_buffer('first', buffer_name=buffer_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_buffer_set_show_set_show_default_a2f68ba5f6 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_buffer_set_show[set_show_named] | Test Server.set_buffer() and show_buffer() cycle; case inputs `append=None`, `buffer_name='mybuf'`, `data='named_data'`, `expected_content='named_data'`, `test_id='set_show_named'`; fixtures `server`; execute `server.new_session(session_name=f'buf_{test_id}')`; `server.set_buffer(data, **kwargs)`; `server.show_buffer(buffer_name=buffer_name)`; assert `result == expected_content`; when `append`: execute `server.set_buffer('first', buffer_name=buffer_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_buffer_set_show_set_show_named_502e44630a | 3.3a through 3.7c | planned parity | | tests/test_server.py::test_clear_prompt_history | Test Server.clear_prompt_history() clears history; fixtures `server`; execute `server.new_session(session_name='clearph_test')`; `server.clear_prompt_history()`; `server.clear_prompt_history(prompt_type='command')`; when `not has_gte_version('3.3')`: skip via `pytest.skip('clear-prompt-history added in tmux 3.3')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_clear_prompt_history_c0a285e790 | 3.3 through 3.7b | planned parity | | tests/test_server.py::test_command_prompt[prefill_and_submit] | Test Server.command_prompt() with send-keys -K input; case inputs `expected_value='prefilled'`, `inputs='prefilled'`, `keys=['Enter']`, `min_tmux_version='3.4'`, `option_name='@cp_prefill'`, `template="set -g @cp_prefill '%1'"`, `test_id='prefill_and_submit'`; fixtures `control_mode`, `server`; execute `control_mode()`; `server.command_prompt(template, **kwargs)`; `server.cmd('send-keys', '-K', '-c', ctl.client_name, key)`; `server.cmd('show-options', '-gv', option_name)`; assert `result.stdout[0] == expected_value`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_command_prompt_prefill_and_submit_0008ebf0d3 | 3.4 through 3.7b | planned parity | | tests/test_server.py::test_command_prompt[type_and_submit] | Test Server.command_prompt() with send-keys -K input; case inputs `expected_value='hello'`, `inputs=None`, `keys=['h', 'e', 'l', 'l', 'o', 'Enter']`, `min_tmux_version='3.4'`, `option_name='@cp_typed'`, `template="set -g @cp_typed '%1'"`, `test_id='type_and_submit'`; fixtures `control_mode`, `server`; execute `control_mode()`; `server.command_prompt(template, **kwargs)`; `server.cmd('send-keys', '-K', '-c', ctl.client_name, key)`; `server.cmd('show-options', '-gv', option_name)`; assert `result.stdout[0] == expected_value`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_command_prompt_type_and_submit_d76de2d176 | 3.4 through 3.7b | planned parity | @@ -1246,13 +1246,13 @@ under [`tests/`](https://github.com/tmux-python/libtmux/blob/c4a980b/tests) and | tests/test_server.py::test_command_prompt_extra_flags[expand_format_v33] | ``command_prompt`` exposes -F/-l/-e flags; case inputs `expected_flag='-F'`, `kwargs={'expand_format': True}`, `min_tmux_version='3.3'`; fixtures `monkeypatch`, `server`; execute `server.command_prompt("set -g @x '%1'", **kwargs)`; `real_cmd(cmd, *args, **_kw)`; assert `captured; message='Server.cmd was not invoked'`; `expected_flag in flags`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `cmd == 'command-prompt'`: execute `_StubResult()`. | unit test | PythonBehaviorParityTest#tests_test_server_py_test_command_prompt_extra_flags_expand_format_v33_5eab7eff82 | 3.3 through 3.7b | planned parity | | tests/test_server.py::test_command_prompt_extra_flags[literal_v36] | ``command_prompt`` exposes -F/-l/-e flags; case inputs `expected_flag='-l'`, `kwargs={'literal': True}`, `min_tmux_version='3.6'`; fixtures `monkeypatch`, `server`; execute `server.command_prompt("set -g @x '%1'", **kwargs)`; `real_cmd(cmd, *args, **_kw)`; assert `captured; message='Server.cmd was not invoked'`; `expected_flag in flags`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `cmd == 'command-prompt'`: execute `_StubResult()`. | unit test | PythonBehaviorParityTest#tests_test_server_py_test_command_prompt_extra_flags_literal_v36_f428a207a5 | 3.6 through 3.7b | planned parity | | tests/test_server.py::test_command_prompt_extra_flags[no_freeze] | ``command_prompt`` exposes -F/-l/-e flags; case inputs `expected_flag='-C'`, `kwargs={'no_freeze': True}`, `min_tmux_version='3.7'`; fixtures `monkeypatch`, `server`; execute `server.command_prompt("set -g @x '%1'", **kwargs)`; `real_cmd(cmd, *args, **_kw)`; assert `captured; message='Server.cmd was not invoked'`; `expected_flag in flags`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `cmd == 'command-prompt'`: execute `_StubResult()`. | unit test | PythonBehaviorParityTest#tests_test_server_py_test_command_prompt_extra_flags_no_freeze_a0fbee6fd7 | 3.7 through 3.7b | planned parity | -| tests/test_server.py::test_config | ``-f`` file for tmux(1) configuration; fixtures `server`; execute `Server(config_file='test')`; assert `myserver.config_file == 'test'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_config_25243d066f | 3.2a through 3.7b | planned parity | +| tests/test_server.py::test_config | ``-f`` file for tmux(1) configuration; fixtures `server`; execute `Server(config_file='test')`; assert `myserver.config_file == 'test'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_config_25243d066f | 3.3a through 3.7c | planned parity | | tests/test_server.py::test_confirm_before[confirm_y] | Test Server.confirm_before() with send-keys -K confirmation; case inputs `confirm_key='y'`, `custom_confirm_key=None`, `expected_value='yes'`, `min_tmux_version='3.4'`, `option_name='@cf_test_y'`, `test_id='confirm_y'`, `use_default_yes=False`; fixtures `control_mode`, `server`; execute `control_mode()`; `server.confirm_before(f'set -g {option_name} {expected_value}', **kwargs)`; `server.cmd('send-keys', '-K', '-c', ctl.client_name, confirm_key)`; `server.cmd('show-options', '-gv', option_name)`; assert `result.stdout[0] == expected_value`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_confirm_before_confirm_y_9679d25e31 | 3.4 through 3.7b | planned parity | | tests/test_server.py::test_confirm_before[default_yes_enter] | Test Server.confirm_before() with send-keys -K confirmation; case inputs `confirm_key='Enter'`, `custom_confirm_key=None`, `expected_value='yes'`, `min_tmux_version='3.4'`, `option_name='@cf_test_enter'`, `test_id='default_yes_enter'`, `use_default_yes=True`; fixtures `control_mode`, `server`; execute `control_mode()`; `server.confirm_before(f'set -g {option_name} {expected_value}', **kwargs)`; `server.cmd('send-keys', '-K', '-c', ctl.client_name, confirm_key)`; `server.cmd('show-options', '-gv', option_name)`; assert `result.stdout[0] == expected_value`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_confirm_before_default_yes_enter_7a009dbd49 | 3.4 through 3.7b | planned parity | -| tests/test_server.py::test_detach_all_clients_keep_client_spans_sessions | ``Server.detach_all_clients(keep_client=...)`` is server-wide; fixtures `control_mode`, `server`, `session`; execute `server.new_session(session_name='detach_all_other')`; `control_mode()`; `server.cmd('list-clients', '-F', '#{client_name}')`; `server.detach_all_clients(keep_client=keep.client_name)`; `functools.partial(ControlMode, server=server, session=other_session)`; `OtherControlMode()`; assert `len(before) == 3`; `after == [keep.client_name]`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_detach_all_clients_keep_client_spans_sessions_02ead1f1a2 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_detach_all_clients_no_keep_preserves_one | ``-a`` without ``-t`` preserves tmux's most-recently-active client; fixtures `control_mode`, `server`; execute `control_mode()`; `server.cmd('list-clients', '-F', '#{client_name}')`; `server.detach_all_clients()`; assert `len(before) == 2`; `len(after) == 1`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_detach_all_clients_no_keep_preserves_one_2d7eeafd08 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_detach_client_no_target_uses_active | ``Server.detach_client()`` without ``-t`` falls back to active client; fixtures `control_mode`, `server`; execute `control_mode()`; `server.cmd('list-clients', '-F', '#{client_name}')`; `server.detach_client()`; assert `len(before) == 2`; `len(after) == 1`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_detach_client_no_target_uses_active_a6301769cc | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_detach_client_target_client_spans_sessions | ``Server.detach_client(target_client=...)`` resolves names server-wide; fixtures `control_mode`, `server`, `session`; execute `server.new_session(session_name='detach_other_t')`; `control_mode()`; `server.cmd('list-clients', '-F', '#{client_name}')`; `server.detach_client(target_client=elsewhere.client_name)`; `functools.partial(ControlMode, server=server, session=other_session)`; `OtherControlMode()`; assert `len(before) == 2`; `elsewhere.client_name in before`; `elsewhere.client_name not in after`; `len(after) == 1`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_detach_client_target_client_spans_sessions_7b07f24451 | 3.2a through 3.7b | planned parity | +| tests/test_server.py::test_detach_all_clients_keep_client_spans_sessions | ``Server.detach_all_clients(keep_client=...)`` is server-wide; fixtures `control_mode`, `server`, `session`; execute `server.new_session(session_name='detach_all_other')`; `control_mode()`; `server.cmd('list-clients', '-F', '#{client_name}')`; `server.detach_all_clients(keep_client=keep.client_name)`; `functools.partial(ControlMode, server=server, session=other_session)`; `OtherControlMode()`; assert `len(before) == 3`; `after == [keep.client_name]`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_detach_all_clients_keep_client_spans_sessions_02ead1f1a2 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_detach_all_clients_no_keep_preserves_one | ``-a`` without ``-t`` preserves tmux's most-recently-active client; fixtures `control_mode`, `server`; execute `control_mode()`; `server.cmd('list-clients', '-F', '#{client_name}')`; `server.detach_all_clients()`; assert `len(before) == 2`; `len(after) == 1`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_detach_all_clients_no_keep_preserves_one_2d7eeafd08 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_detach_client_no_target_uses_active | ``Server.detach_client()`` without ``-t`` falls back to active client; fixtures `control_mode`, `server`; execute `control_mode()`; `server.cmd('list-clients', '-F', '#{client_name}')`; `server.detach_client()`; assert `len(before) == 2`; `len(after) == 1`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_detach_client_no_target_uses_active_a6301769cc | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_detach_client_target_client_spans_sessions | ``Server.detach_client(target_client=...)`` resolves names server-wide; fixtures `control_mode`, `server`, `session`; execute `server.new_session(session_name='detach_other_t')`; `control_mode()`; `server.cmd('list-clients', '-F', '#{client_name}')`; `server.detach_client(target_client=elsewhere.client_name)`; `functools.partial(ControlMode, server=server, session=other_session)`; `OtherControlMode()`; assert `len(before) == 2`; `elsewhere.client_name in before`; `elsewhere.client_name not in after`; `len(after) == 1`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_detach_client_target_client_spans_sessions_7b07f24451 | 3.3a through 3.7c | planned parity | | tests/test_server.py::test_display_menu_flags[basic] | Test Server.display_menu() argument construction; case inputs `items=('First', '1', 'select-pane')`, `kwargs={}`, `min_tmux_version=None`, `test_id='basic'`; fixtures `monkeypatch`, `server`; execute `server.display_menu(*items, **kwargs)`; `real_cmd(cmd, *args, **_kw)`; assert `captured; message='Server.cmd was not invoked'`; `cmd == 'display-menu'`; `str(value) in flags`; `item in flags`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `cmd == 'display-menu'`: execute `_StubResult()`. | unit test | PythonBehaviorParityTest#tests_test_server_py_test_display_menu_flags_basic_c52ed1c12e | not tmux-bound | planned parity | | tests/test_server.py::test_display_menu_flags[with_border_lines_v34] | Test Server.display_menu() argument construction; case inputs `items=('First', '1', 'select-pane')`, `kwargs={'border_lines': 'single'}`, `min_tmux_version='3.4'`, `test_id='with_border_lines_v34'`; fixtures `monkeypatch`, `server`; execute `server.display_menu(*items, **kwargs)`; `real_cmd(cmd, *args, **_kw)`; assert `captured; message='Server.cmd was not invoked'`; `cmd == 'display-menu'`; `str(value) in flags`; `item in flags`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `cmd == 'display-menu'`: execute `_StubResult()`. | unit test | PythonBehaviorParityTest#tests_test_server_py_test_display_menu_flags_with_border_lines_v34_8fb25e00bb | 3.4 through 3.7b | planned parity | | tests/test_server.py::test_display_menu_flags[with_border_style_v34] | Test Server.display_menu() argument construction; case inputs `items=('First', '1', 'select-pane')`, `kwargs={'border_style': 'fg=red'}`, `min_tmux_version='3.4'`, `test_id='with_border_style_v34'`; fixtures `monkeypatch`, `server`; execute `server.display_menu(*items, **kwargs)`; `real_cmd(cmd, *args, **_kw)`; assert `captured; message='Server.cmd was not invoked'`; `cmd == 'display-menu'`; `str(value) in flags`; `item in flags`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `cmd == 'display-menu'`: execute `_StubResult()`. | unit test | PythonBehaviorParityTest#tests_test_server_py_test_display_menu_flags_with_border_style_v34_ef9e129011 | 3.4 through 3.7b | planned parity | @@ -1260,150 +1260,150 @@ under [`tests/`](https://github.com/tmux-python/libtmux/blob/c4a980b/tests) and | tests/test_server.py::test_display_menu_flags[with_position] | Test Server.display_menu() argument construction; case inputs `items=('First', '1', 'select-pane')`, `kwargs={'x': 'C', 'y': 'C'}`, `min_tmux_version=None`, `test_id='with_position'`; fixtures `monkeypatch`, `server`; execute `server.display_menu(*items, **kwargs)`; `real_cmd(cmd, *args, **_kw)`; assert `captured; message='Server.cmd was not invoked'`; `cmd == 'display-menu'`; `str(value) in flags`; `item in flags`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `cmd == 'display-menu'`: execute `_StubResult()`. | unit test | PythonBehaviorParityTest#tests_test_server_py_test_display_menu_flags_with_position_bec663c9e5 | not tmux-bound | planned parity | | tests/test_server.py::test_display_menu_flags[with_selected_style_v34] | Test Server.display_menu() argument construction; case inputs `items=('First', '1', 'select-pane')`, `kwargs={'selected_style': 'bg=yellow'}`, `min_tmux_version='3.4'`, `test_id='with_selected_style_v34'`; fixtures `monkeypatch`, `server`; execute `server.display_menu(*items, **kwargs)`; `real_cmd(cmd, *args, **_kw)`; assert `captured; message='Server.cmd was not invoked'`; `cmd == 'display-menu'`; `str(value) in flags`; `item in flags`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `cmd == 'display-menu'`: execute `_StubResult()`. | unit test | PythonBehaviorParityTest#tests_test_server_py_test_display_menu_flags_with_selected_style_v34_c12d276edc | 3.4 through 3.7b | planned parity | | tests/test_server.py::test_display_menu_flags[with_starting_choice_v34] | Test Server.display_menu() argument construction; case inputs `items=('First', '1', 'select-pane')`, `kwargs={'starting_choice': '0'}`, `min_tmux_version='3.4'`, `test_id='with_starting_choice_v34'`; fixtures `monkeypatch`, `server`; execute `server.display_menu(*items, **kwargs)`; `real_cmd(cmd, *args, **_kw)`; assert `captured; message='Server.cmd was not invoked'`; `cmd == 'display-menu'`; `str(value) in flags`; `item in flags`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `cmd == 'display-menu'`: execute `_StubResult()`. | unit test | PythonBehaviorParityTest#tests_test_server_py_test_display_menu_flags_with_starting_choice_v34_814a155a3e | 3.4 through 3.7b | planned parity | -| tests/test_server.py::test_display_menu_flags[with_stay_open] | Test Server.display_menu() argument construction; case inputs `items=('First', '1', 'select-pane')`, `kwargs={'stay_open': True}`, `min_tmux_version='3.2'`, `test_id='with_stay_open'`; fixtures `monkeypatch`, `server`; execute `server.display_menu(*items, **kwargs)`; `real_cmd(cmd, *args, **_kw)`; assert `captured; message='Server.cmd was not invoked'`; `cmd == 'display-menu'`; `str(value) in flags`; `item in flags`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `cmd == 'display-menu'`: execute `_StubResult()`. | unit test | PythonBehaviorParityTest#tests_test_server_py_test_display_menu_flags_with_stay_open_b69bb481c4 | 3.2a through 3.7b | planned parity | +| tests/test_server.py::test_display_menu_flags[with_stay_open] | Test Server.display_menu() argument construction; case inputs `items=('First', '1', 'select-pane')`, `kwargs={'stay_open': True}`, `min_tmux_version='3.2'`, `test_id='with_stay_open'`; fixtures `monkeypatch`, `server`; execute `server.display_menu(*items, **kwargs)`; `real_cmd(cmd, *args, **_kw)`; assert `captured; message='Server.cmd was not invoked'`; `cmd == 'display-menu'`; `str(value) in flags`; `item in flags`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `cmd == 'display-menu'`: execute `_StubResult()`. | unit test | PythonBehaviorParityTest#tests_test_server_py_test_display_menu_flags_with_stay_open_b69bb481c4 | 3.3a through 3.7c | planned parity | | tests/test_server.py::test_display_menu_flags[with_style_v34] | Test Server.display_menu() argument construction; case inputs `items=('First', '1', 'select-pane')`, `kwargs={'style': 'bg=blue'}`, `min_tmux_version='3.4'`, `test_id='with_style_v34'`; fixtures `monkeypatch`, `server`; execute `server.display_menu(*items, **kwargs)`; `real_cmd(cmd, *args, **_kw)`; assert `captured; message='Server.cmd was not invoked'`; `cmd == 'display-menu'`; `str(value) in flags`; `item in flags`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `cmd == 'display-menu'`: execute `_StubResult()`. | unit test | PythonBehaviorParityTest#tests_test_server_py_test_display_menu_flags_with_style_v34_69241b4ceb | 3.4 through 3.7b | planned parity | | tests/test_server.py::test_display_menu_flags[with_title] | Test Server.display_menu() argument construction; case inputs `items=('First', '1', 'select-pane')`, `kwargs={'title': 'menu_title'}`, `min_tmux_version=None`, `test_id='with_title'`; fixtures `monkeypatch`, `server`; execute `server.display_menu(*items, **kwargs)`; `real_cmd(cmd, *args, **_kw)`; assert `captured; message='Server.cmd was not invoked'`; `cmd == 'display-menu'`; `str(value) in flags`; `item in flags`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `cmd == 'display-menu'`: execute `_StubResult()`. | unit test | PythonBehaviorParityTest#tests_test_server_py_test_display_menu_flags_with_title_74fc7520c8 | not tmux-bound | planned parity | -| tests/test_server.py::test_getenv | Set environment then Server.show_environment(key); fixtures `server`, `session`; execute `server.set_environment('FOO', 'BAR')`; `server.getenv('FOO')`; `server.set_environment('FOO', 'DAR')`; `server.show_environment()`; assert `server.getenv('FOO') == 'BAR'`; `server.getenv('FOO') == 'DAR'`; `server.show_environment()['FOO'] == 'DAR'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_getenv_e2794ae688 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_has_session | Server.has_session() returns True if session exists; fixtures `server`, `session`; execute `server.has_session(session_name)`; `server.has_session('asdf2314324321')`; assert `session_name is not None`; `server.has_session(session_name)`; `not server.has_session('asdf2314324321')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_has_session_7df25e30d5 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_if_shell_false_with_else | Test Server.if_shell() with false condition and else branch; fixtures `server`; execute `server.new_session(session_name='ifshell_else')`; `server.if_shell('false', 'set -g @if_else_test yes', else_command='set -g @if_else_test no')`; `server.cmd('show-options', '-gv', '@if_else_test')`; assert `result.stdout[0] == 'no'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_if_shell_false_with_else_a42b686bae | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_if_shell_true | Test Server.if_shell() with true condition; fixtures `server`; execute `server.new_session(session_name='ifshell_test')`; `server.if_shell('true', 'set -g @if_test_true yes')`; `server.cmd('show-options', '-gv', '@if_test_true')`; assert `result.stdout[0] == 'yes'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_if_shell_true_bdf7b5be71 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_list_buffers | Test Server.list_buffers(); fixtures `server`; execute `server.new_session(session_name='buf_list')`; `server.set_buffer('buf_a', buffer_name='list_a')`; `server.set_buffer('buf_b', buffer_name='list_b')`; `server.list_buffers()`; assert `len(result) >= 2`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_list_buffers_3cbc39ed66 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_list_buffers_filter_pushes_predicate_into_tmux | ``filter=`` pushes the match into tmux's format engine (-f flag); fixtures `server`; execute `server.new_session(session_name='buf_filter')`; `server.set_buffer('keep_me', buffer_name='gap6match_alpha')`; `server.set_buffer('keep_me', buffer_name='gap6match_beta')`; `server.set_buffer('drop_me', buffer_name='gap6miss_one')`; `server.list_buffers(format_string='#{buffer_name}', filter='#{m:gap6match_*,#{buffer_name}}')`; assert `sorted(matches) == ['gap6match_alpha', 'gap6match_beta']`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_list_buffers_filter_pushes_predicate_into_tmux_09f399a626 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_list_buffers_format_returns_raw_names | ``format_string`` projects raw names instead of the default template; fixtures `server`; execute `server.new_session(session_name='buf_format')`; `server.set_buffer('payload_a', buffer_name='fmt_a')`; `server.set_buffer('payload_b', buffer_name='fmt_b')`; `server.list_buffers(format_string='#{buffer_name}')`; `any(('bytes:' in line for line in names))`; assert `'fmt_a' in names`; `'fmt_b' in names`; `not any(('bytes:' in line for line in names))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_list_buffers_format_returns_raw_names_f953a24e1f | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_list_clients | Test Server.list_clients() returns list without error; fixtures `server`; execute `server.new_session(session_name='list_clients_test')`; `server.list_clients()`; assert `isinstance(result, list)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_list_clients_3bc9a653cf | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_list_commands | Test Server.list_commands() returns command listing; fixtures `server`; execute `server.new_session(session_name='listcmds_test')`; `server.list_commands()`; `server.list_commands(command_name='send-keys')`; assert `len(result) > 50`; `len(result) >= 1`; `'send-keys' in result[0]`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_list_commands_1eb8b2b439 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_list_keys | Test Server.list_keys() returns key bindings; fixtures `server`; execute `server.new_session(session_name='listkeys_test')`; `server.list_keys()`; assert `isinstance(result, list)`; `len(result) > 0`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_list_keys_0bb88cdcfd | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_list_keys_format | Server.list_keys(format_=...) applies -F on tmux 3.7+; warns below; fixtures `server`; execute `server.new_session(session_name='listkeys_fmt')`; when `has_gte_version('3.7')`: execute `server.list_keys(format_='fmt')`; `all((line == 'fmt' for line in result))`; assert `result`; `all((line == 'fmt' for line in result))`; when `not (has_gte_version('3.7'))`: execute `server.list_keys(format_='fmt')`; expect `pytest.warns(UserWarning, match='format requires tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_list_keys_format_8502678464 | 3.2a through 3.7b; conditional boundaries: 3.7 | planned parity | -| tests/test_server.py::test_lock_client | Test Server.lock_client() runs without error; fixtures `control_mode`, `server`; execute `control_mode()`; `server.lock_client()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_lock_client_739fab5726 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_lock_server | Test Server.lock_server() runs without error; fixtures `control_mode`, `server`; execute `control_mode()`; `server.lock_server()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_lock_server_dc3591d2c4 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_new_session | Server.new_session creates and returns valid session; fixtures `server`; execute `server.new_session('test_new_session')`; `server.has_session('test_new_session')`; assert `mysession.session_name == 'test_new_session'`; `server.has_session('test_new_session')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_new_session_0db766e735 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_new_session_client_flags | Test Server.new_session() with client_flags flag; fixtures `server`; execute `server.new_session(session_name='flags_test', client_flags='no-output')`; assert `session.session_name == 'flags_test'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_new_session_client_flags_21a8b45b06 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_new_session_environmental_variables | Server.new_session creates and returns valid session; fixtures `server`; execute `server.new_session('test_new_session', environment={'FOO': 'HI'})`; `my_session.show_environment()`; assert `my_session.show_environment()['FOO'] == 'HI'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_new_session_environmental_variables_d9c684e505 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_new_session_no_name | Server.new_session works with no name; fixtures `server`; execute `server.new_session()`; `server.has_session(first_session_name)`; `server.has_session(second_session_name)`; assert `first_session_name is not None`; `server.has_session(first_session_name)`; `expected_session_name == second_session_name`; `second_session_name is not None`; `server.has_session(second_session_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_new_session_no_name_216d2c2c10 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_new_session_returns_populated_session | Server.new_session returns Session populated from -P output; fixtures `server`; execute `server.new_session(session_name='test_populated')`; assert `session.session_id is not None`; `session.session_name == 'test_populated'`; `session.window_id is not None`; `session.pane_id is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_new_session_returns_populated_session_5df3cdc9f3 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_new_session_shell | Verify ``Server.new_session`` creates valid session running w/ command; fixtures `server`; execute `server.new_session('test_new_session', window_command=cmd)`; `server.has_session('test_new_session')`; `pane_start_command.replace('"', '')`; assert `mysession.session_name == 'test_new_session'`; `server.has_session('test_new_session')`; `pane_start_command is not None`; `pane_start_command.replace('"', '') == cmd`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_new_session_shell_880257086b | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_new_session_shell_env | Verify ``Server.new_session`` creates valid session running w/ command (#553); fixtures `server`; execute `server.new_session('test_new_session_env', window_command=cmd, environment=env)`; `server.has_session('test_new_session_env')`; `time.sleep(0.1)`; `pane_start_command.replace('"', '')`; assert `mysession.session_name == 'test_new_session_env'`; `server.has_session('test_new_session_env')`; `pane_start_command is not None`; `pane_start_command.replace('"', '') == cmd`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_new_session_shell_env_8bd84ff212 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_new_session_start_directory[empty_string] | Test Server.new_session start_directory parameter handling; case inputs `description='Empty string should not add -c flag'`, `start_directory=''`, `test_id='empty_string'`; fixtures `server`, `monkeypatch`, `tmp_path`, `user_path`; execute `server.new_session(session_name=f'test_session_{test_id}', start_directory=actual_start_directory)`; `server.has_session(f'test_session_{test_id}')`; assert `session.session_name == f'test_session_{test_id}'`; `server.has_session(f'test_session_{test_id}')`; when `start_directory and str(start_directory) not in {'', 'None'} and not (f'{user_path}' in str(start_directory)) and str(start_directory).startswith('./')`: execute `temp_dir.mkdir(parents=True, exist_ok=True)`; `temp_dir.resolve()`; when `expected_path`: execute `active_pane.refresh()`; `pathlib.Path(active_pane.pane_current_path).resolve()`; `pathlib.Path(active_pane.pane_current_path)`; assert `active_pane is not None`; `active_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_new_session_start_directory_empty_string_cc3a633c65 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_new_session_start_directory[none_value] | Test Server.new_session start_directory parameter handling; case inputs `description='None should not add -c flag'`, `start_directory=None`, `test_id='none_value'`; fixtures `server`, `monkeypatch`, `tmp_path`, `user_path`; execute `server.new_session(session_name=f'test_session_{test_id}', start_directory=actual_start_directory)`; `server.has_session(f'test_session_{test_id}')`; assert `session.session_name == f'test_session_{test_id}'`; `server.has_session(f'test_session_{test_id}')`; when `start_directory and str(start_directory) not in {'', 'None'} and not (f'{user_path}' in str(start_directory)) and str(start_directory).startswith('./')`: execute `temp_dir.mkdir(parents=True, exist_ok=True)`; `temp_dir.resolve()`; when `expected_path`: execute `active_pane.refresh()`; `pathlib.Path(active_pane.pane_current_path).resolve()`; `pathlib.Path(active_pane.pane_current_path)`; assert `active_pane is not None`; `active_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_new_session_start_directory_none_value_cb73fa98e8 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_new_session_start_directory[relative_path] | Test Server.new_session start_directory parameter handling; case inputs `description='Relative path should add -c flag'`, `start_directory='./relative/path'`, `test_id='relative_path'`; fixtures `server`, `monkeypatch`, `tmp_path`, `user_path`; execute `server.new_session(session_name=f'test_session_{test_id}', start_directory=actual_start_directory)`; `server.has_session(f'test_session_{test_id}')`; assert `session.session_name == f'test_session_{test_id}'`; `server.has_session(f'test_session_{test_id}')`; when `start_directory and str(start_directory) not in {'', 'None'} and not (f'{user_path}' in str(start_directory)) and str(start_directory).startswith('./')`: execute `temp_dir.mkdir(parents=True, exist_ok=True)`; `temp_dir.resolve()`; when `expected_path`: execute `active_pane.refresh()`; `pathlib.Path(active_pane.pane_current_path).resolve()`; `pathlib.Path(active_pane.pane_current_path)`; assert `active_pane is not None`; `active_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_new_session_start_directory_relative_path_dffb8dbd6c | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_new_session_start_directory[user_path] | Test Server.new_session start_directory parameter handling; case inputs `description='User path should add -c flag'`, `start_directory='{user_path}'`, `test_id='user_path'`; fixtures `server`, `monkeypatch`, `tmp_path`, `user_path`; execute `server.new_session(session_name=f'test_session_{test_id}', start_directory=actual_start_directory)`; `server.has_session(f'test_session_{test_id}')`; assert `session.session_name == f'test_session_{test_id}'`; `server.has_session(f'test_session_{test_id}')`; when `start_directory and str(start_directory) not in {'', 'None'} and not (f'{user_path}' in str(start_directory)) and str(start_directory).startswith('./')`: execute `temp_dir.mkdir(parents=True, exist_ok=True)`; `temp_dir.resolve()`; when `expected_path`: execute `active_pane.refresh()`; `pathlib.Path(active_pane.pane_current_path).resolve()`; `pathlib.Path(active_pane.pane_current_path)`; assert `active_pane is not None`; `active_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_new_session_start_directory_user_path_32ae926ce5 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_new_session_start_directory_pathlib | Test Server.new_session accepts pathlib.Path for start_directory; fixtures `server`, `user_path`; execute `server.new_session(session_name='test_pathlib_start_dir', start_directory=user_path)`; `server.has_session('test_pathlib_start_dir')`; `user_path.resolve()`; `active_pane.refresh()`; `pathlib.Path(active_pane.pane_current_path).resolve()`; `pathlib.Path(active_pane.pane_current_path)`; assert `session.session_name == 'test_pathlib_start_dir'`; `server.has_session('test_pathlib_start_dir')`; `active_pane is not None`; `active_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_new_session_start_directory_pathlib_43f72d0bfa | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_new_session_width_height | Verify ``Server.new_session`` creates valid session running w/ dimensions; fixtures `server`; execute `server.new_session('test_new_session_width_height', window_command=cmd, x=32, y=32)`; `pane.display_message('#{window_width}', get_text=True)`; `pane.display_message('#{window_height}', get_text=True)`; assert `pane.display_message('#{window_width}', get_text=True)[0] == '32'`; `pane.display_message('#{window_height}', get_text=True)[0] == '32'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_new_session_width_height_8fb6922568 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_no_server_attached_sessions | Verify ``Server.attached_sessions`` returns empty list without tmux server; fixtures `server`; assert `server.attached_sessions == []`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_no_server_attached_sessions_ccabb719ad | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_no_server_is_alive | Verify is_alive() returns False without tmux server; fixtures `server`; execute `server.is_alive()`; assert `not server.is_alive()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_no_server_is_alive_93ab885f0e | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_no_server_sessions | Verify ``Server.sessions`` returns empty list without tmux server; fixtures `server`; assert `server.sessions == []`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_no_server_sessions_aa88e052c0 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_on_init | Verify on_init callback is called during Server initialization; fixtures `server`; execute `Server(socket_name='test_on_init', on_init=on_init)`; assert `len(called_with) == 1`; `called_with[0] is myserver`; when `myserver.is_alive()`: execute `myserver.kill()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_on_init_03a405e770 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_raise_if_dead_does_not_raise_if_alive | Verify new_session() does not raise if tmux server is alive; fixtures `server`; execute `server.new_session()`; `server.raise_if_dead()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_raise_if_dead_does_not_raise_if_alive_6fe5a9220d | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_raise_if_dead_no_server_raises | Verify ``Server.raise_if_dead`` raises if tmux server is dead; fixtures `server`; execute `server.raise_if_dead()`; expect `pytest.raises(subprocess.CalledProcessError)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_raise_if_dead_no_server_raises_7d65b68090 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_refresh_client | Test Server.refresh_client() runs without error; fixtures `control_mode`, `server`; execute `control_mode()`; `server.refresh_client()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_refresh_client_c7574035e0 | 3.2a through 3.7b | planned parity | +| tests/test_server.py::test_getenv | Set environment then Server.show_environment(key); fixtures `server`, `session`; execute `server.set_environment('FOO', 'BAR')`; `server.getenv('FOO')`; `server.set_environment('FOO', 'DAR')`; `server.show_environment()`; assert `server.getenv('FOO') == 'BAR'`; `server.getenv('FOO') == 'DAR'`; `server.show_environment()['FOO'] == 'DAR'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_getenv_e2794ae688 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_has_session | Server.has_session() returns True if session exists; fixtures `server`, `session`; execute `server.has_session(session_name)`; `server.has_session('asdf2314324321')`; assert `session_name is not None`; `server.has_session(session_name)`; `not server.has_session('asdf2314324321')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_has_session_7df25e30d5 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_if_shell_false_with_else | Test Server.if_shell() with false condition and else branch; fixtures `server`; execute `server.new_session(session_name='ifshell_else')`; `server.if_shell('false', 'set -g @if_else_test yes', else_command='set -g @if_else_test no')`; `server.cmd('show-options', '-gv', '@if_else_test')`; assert `result.stdout[0] == 'no'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_if_shell_false_with_else_a42b686bae | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_if_shell_true | Test Server.if_shell() with true condition; fixtures `server`; execute `server.new_session(session_name='ifshell_test')`; `server.if_shell('true', 'set -g @if_test_true yes')`; `server.cmd('show-options', '-gv', '@if_test_true')`; assert `result.stdout[0] == 'yes'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_if_shell_true_bdf7b5be71 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_list_buffers | Test Server.list_buffers(); fixtures `server`; execute `server.new_session(session_name='buf_list')`; `server.set_buffer('buf_a', buffer_name='list_a')`; `server.set_buffer('buf_b', buffer_name='list_b')`; `server.list_buffers()`; assert `len(result) >= 2`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_list_buffers_3cbc39ed66 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_list_buffers_filter_pushes_predicate_into_tmux | ``filter=`` pushes the match into tmux's format engine (-f flag); fixtures `server`; execute `server.new_session(session_name='buf_filter')`; `server.set_buffer('keep_me', buffer_name='gap6match_alpha')`; `server.set_buffer('keep_me', buffer_name='gap6match_beta')`; `server.set_buffer('drop_me', buffer_name='gap6miss_one')`; `server.list_buffers(format_string='#{buffer_name}', filter='#{m:gap6match_*,#{buffer_name}}')`; assert `sorted(matches) == ['gap6match_alpha', 'gap6match_beta']`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_list_buffers_filter_pushes_predicate_into_tmux_09f399a626 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_list_buffers_format_returns_raw_names | ``format_string`` projects raw names instead of the default template; fixtures `server`; execute `server.new_session(session_name='buf_format')`; `server.set_buffer('payload_a', buffer_name='fmt_a')`; `server.set_buffer('payload_b', buffer_name='fmt_b')`; `server.list_buffers(format_string='#{buffer_name}')`; `any(('bytes:' in line for line in names))`; assert `'fmt_a' in names`; `'fmt_b' in names`; `not any(('bytes:' in line for line in names))`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_list_buffers_format_returns_raw_names_f953a24e1f | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_list_clients | Test Server.list_clients() returns list without error; fixtures `server`; execute `server.new_session(session_name='list_clients_test')`; `server.list_clients()`; assert `isinstance(result, list)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_list_clients_3bc9a653cf | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_list_commands | Test Server.list_commands() returns command listing; fixtures `server`; execute `server.new_session(session_name='listcmds_test')`; `server.list_commands()`; `server.list_commands(command_name='send-keys')`; assert `len(result) > 50`; `len(result) >= 1`; `'send-keys' in result[0]`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_list_commands_1eb8b2b439 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_list_keys | Test Server.list_keys() returns key bindings; fixtures `server`; execute `server.new_session(session_name='listkeys_test')`; `server.list_keys()`; assert `isinstance(result, list)`; `len(result) > 0`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_list_keys_0bb88cdcfd | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_list_keys_format | Server.list_keys(format_=...) applies -F on tmux 3.7+; warns below; fixtures `server`; execute `server.new_session(session_name='listkeys_fmt')`; when `has_gte_version('3.7')`: execute `server.list_keys(format_='fmt')`; `all((line == 'fmt' for line in result))`; assert `result`; `all((line == 'fmt' for line in result))`; when `not (has_gte_version('3.7'))`: execute `server.list_keys(format_='fmt')`; expect `pytest.warns(UserWarning, match='format requires tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_list_keys_format_8502678464 | 3.3a through 3.7c; conditional boundaries: 3.7 | planned parity | +| tests/test_server.py::test_lock_client | Test Server.lock_client() runs without error; fixtures `control_mode`, `server`; execute `control_mode()`; `server.lock_client()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_lock_client_739fab5726 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_lock_server | Test Server.lock_server() runs without error; fixtures `control_mode`, `server`; execute `control_mode()`; `server.lock_server()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_lock_server_dc3591d2c4 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_new_session | Server.new_session creates and returns valid session; fixtures `server`; execute `server.new_session('test_new_session')`; `server.has_session('test_new_session')`; assert `mysession.session_name == 'test_new_session'`; `server.has_session('test_new_session')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_new_session_0db766e735 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_new_session_client_flags | Test Server.new_session() with client_flags flag; fixtures `server`; execute `server.new_session(session_name='flags_test', client_flags='no-output')`; assert `session.session_name == 'flags_test'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_new_session_client_flags_21a8b45b06 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_new_session_environmental_variables | Server.new_session creates and returns valid session; fixtures `server`; execute `server.new_session('test_new_session', environment={'FOO': 'HI'})`; `my_session.show_environment()`; assert `my_session.show_environment()['FOO'] == 'HI'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_new_session_environmental_variables_d9c684e505 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_new_session_no_name | Server.new_session works with no name; fixtures `server`; execute `server.new_session()`; `server.has_session(first_session_name)`; `server.has_session(second_session_name)`; assert `first_session_name is not None`; `server.has_session(first_session_name)`; `expected_session_name == second_session_name`; `second_session_name is not None`; `server.has_session(second_session_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_new_session_no_name_216d2c2c10 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_new_session_returns_populated_session | Server.new_session returns Session populated from -P output; fixtures `server`; execute `server.new_session(session_name='test_populated')`; assert `session.session_id is not None`; `session.session_name == 'test_populated'`; `session.window_id is not None`; `session.pane_id is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_new_session_returns_populated_session_5df3cdc9f3 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_new_session_shell | Verify ``Server.new_session`` creates valid session running w/ command; fixtures `server`; execute `server.new_session('test_new_session', window_command=cmd)`; `server.has_session('test_new_session')`; `pane_start_command.replace('"', '')`; assert `mysession.session_name == 'test_new_session'`; `server.has_session('test_new_session')`; `pane_start_command is not None`; `pane_start_command.replace('"', '') == cmd`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_new_session_shell_880257086b | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_new_session_shell_env | Verify ``Server.new_session`` creates valid session running w/ command (#553); fixtures `server`; execute `server.new_session('test_new_session_env', window_command=cmd, environment=env)`; `server.has_session('test_new_session_env')`; `time.sleep(0.1)`; `pane_start_command.replace('"', '')`; assert `mysession.session_name == 'test_new_session_env'`; `server.has_session('test_new_session_env')`; `pane_start_command is not None`; `pane_start_command.replace('"', '') == cmd`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_new_session_shell_env_8bd84ff212 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_new_session_start_directory[empty_string] | Test Server.new_session start_directory parameter handling; case inputs `description='Empty string should not add -c flag'`, `start_directory=''`, `test_id='empty_string'`; fixtures `server`, `monkeypatch`, `tmp_path`, `user_path`; execute `server.new_session(session_name=f'test_session_{test_id}', start_directory=actual_start_directory)`; `server.has_session(f'test_session_{test_id}')`; assert `session.session_name == f'test_session_{test_id}'`; `server.has_session(f'test_session_{test_id}')`; when `start_directory and str(start_directory) not in {'', 'None'} and not (f'{user_path}' in str(start_directory)) and str(start_directory).startswith('./')`: execute `temp_dir.mkdir(parents=True, exist_ok=True)`; `temp_dir.resolve()`; when `expected_path`: execute `active_pane.refresh()`; `pathlib.Path(active_pane.pane_current_path).resolve()`; `pathlib.Path(active_pane.pane_current_path)`; assert `active_pane is not None`; `active_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_new_session_start_directory_empty_string_cc3a633c65 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_new_session_start_directory[none_value] | Test Server.new_session start_directory parameter handling; case inputs `description='None should not add -c flag'`, `start_directory=None`, `test_id='none_value'`; fixtures `server`, `monkeypatch`, `tmp_path`, `user_path`; execute `server.new_session(session_name=f'test_session_{test_id}', start_directory=actual_start_directory)`; `server.has_session(f'test_session_{test_id}')`; assert `session.session_name == f'test_session_{test_id}'`; `server.has_session(f'test_session_{test_id}')`; when `start_directory and str(start_directory) not in {'', 'None'} and not (f'{user_path}' in str(start_directory)) and str(start_directory).startswith('./')`: execute `temp_dir.mkdir(parents=True, exist_ok=True)`; `temp_dir.resolve()`; when `expected_path`: execute `active_pane.refresh()`; `pathlib.Path(active_pane.pane_current_path).resolve()`; `pathlib.Path(active_pane.pane_current_path)`; assert `active_pane is not None`; `active_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_new_session_start_directory_none_value_cb73fa98e8 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_new_session_start_directory[relative_path] | Test Server.new_session start_directory parameter handling; case inputs `description='Relative path should add -c flag'`, `start_directory='./relative/path'`, `test_id='relative_path'`; fixtures `server`, `monkeypatch`, `tmp_path`, `user_path`; execute `server.new_session(session_name=f'test_session_{test_id}', start_directory=actual_start_directory)`; `server.has_session(f'test_session_{test_id}')`; assert `session.session_name == f'test_session_{test_id}'`; `server.has_session(f'test_session_{test_id}')`; when `start_directory and str(start_directory) not in {'', 'None'} and not (f'{user_path}' in str(start_directory)) and str(start_directory).startswith('./')`: execute `temp_dir.mkdir(parents=True, exist_ok=True)`; `temp_dir.resolve()`; when `expected_path`: execute `active_pane.refresh()`; `pathlib.Path(active_pane.pane_current_path).resolve()`; `pathlib.Path(active_pane.pane_current_path)`; assert `active_pane is not None`; `active_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_new_session_start_directory_relative_path_dffb8dbd6c | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_new_session_start_directory[user_path] | Test Server.new_session start_directory parameter handling; case inputs `description='User path should add -c flag'`, `start_directory='{user_path}'`, `test_id='user_path'`; fixtures `server`, `monkeypatch`, `tmp_path`, `user_path`; execute `server.new_session(session_name=f'test_session_{test_id}', start_directory=actual_start_directory)`; `server.has_session(f'test_session_{test_id}')`; assert `session.session_name == f'test_session_{test_id}'`; `server.has_session(f'test_session_{test_id}')`; when `start_directory and str(start_directory) not in {'', 'None'} and not (f'{user_path}' in str(start_directory)) and str(start_directory).startswith('./')`: execute `temp_dir.mkdir(parents=True, exist_ok=True)`; `temp_dir.resolve()`; when `expected_path`: execute `active_pane.refresh()`; `pathlib.Path(active_pane.pane_current_path).resolve()`; `pathlib.Path(active_pane.pane_current_path)`; assert `active_pane is not None`; `active_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_new_session_start_directory_user_path_32ae926ce5 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_new_session_start_directory_pathlib | Test Server.new_session accepts pathlib.Path for start_directory; fixtures `server`, `user_path`; execute `server.new_session(session_name='test_pathlib_start_dir', start_directory=user_path)`; `server.has_session('test_pathlib_start_dir')`; `user_path.resolve()`; `active_pane.refresh()`; `pathlib.Path(active_pane.pane_current_path).resolve()`; `pathlib.Path(active_pane.pane_current_path)`; assert `session.session_name == 'test_pathlib_start_dir'`; `server.has_session('test_pathlib_start_dir')`; `active_pane is not None`; `active_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_new_session_start_directory_pathlib_43f72d0bfa | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_new_session_width_height | Verify ``Server.new_session`` creates valid session running w/ dimensions; fixtures `server`; execute `server.new_session('test_new_session_width_height', window_command=cmd, x=32, y=32)`; `pane.display_message('#{window_width}', get_text=True)`; `pane.display_message('#{window_height}', get_text=True)`; assert `pane.display_message('#{window_width}', get_text=True)[0] == '32'`; `pane.display_message('#{window_height}', get_text=True)[0] == '32'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_new_session_width_height_8fb6922568 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_no_server_attached_sessions | Verify ``Server.attached_sessions`` returns empty list without tmux server; fixtures `server`; assert `server.attached_sessions == []`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_no_server_attached_sessions_ccabb719ad | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_no_server_is_alive | Verify is_alive() returns False without tmux server; fixtures `server`; execute `server.is_alive()`; assert `not server.is_alive()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_no_server_is_alive_93ab885f0e | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_no_server_sessions | Verify ``Server.sessions`` returns empty list without tmux server; fixtures `server`; assert `server.sessions == []`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_no_server_sessions_aa88e052c0 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_on_init | Verify on_init callback is called during Server initialization; fixtures `server`; execute `Server(socket_name='test_on_init', on_init=on_init)`; assert `len(called_with) == 1`; `called_with[0] is myserver`; when `myserver.is_alive()`: execute `myserver.kill()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_on_init_03a405e770 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_raise_if_dead_does_not_raise_if_alive | Verify new_session() does not raise if tmux server is alive; fixtures `server`; execute `server.new_session()`; `server.raise_if_dead()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_raise_if_dead_does_not_raise_if_alive_6fe5a9220d | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_raise_if_dead_no_server_raises | Verify ``Server.raise_if_dead`` raises if tmux server is dead; fixtures `server`; execute `server.raise_if_dead()`; expect `pytest.raises(subprocess.CalledProcessError)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_raise_if_dead_no_server_raises_7d65b68090 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_refresh_client | Test Server.refresh_client() runs without error; fixtures `control_mode`, `server`; execute `control_mode()`; `server.refresh_client()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_refresh_client_c7574035e0 | 3.3a through 3.7c | planned parity | | tests/test_server.py::test_refresh_client_request_clipboard | refresh_client(request_clipboard=True) emits -l on tmux 3.7+; fixtures `monkeypatch`, `server`; execute `server.refresh_client(request_clipboard=True)`; `real_cmd(cmd, *args, **_kw)`; assert `captured; message='Server.cmd was not invoked'`; `'-l' in flags`; when `not has_gte_version('3.7')`: skip via `pytest.skip('refresh-client -l clipboard query requires tmux 3.7+')`; when `cmd == 'refresh-client'`: execute `_StubResult()`. | unit test | PythonBehaviorParityTest#tests_test_server_py_test_refresh_client_request_clipboard_57beaa2623 | 3.7 through 3.7b | planned parity | -| tests/test_server.py::test_run_shell_args | ``args`` fill #{1}/#{2} in the command on tmux 3.7+; warn below; fixtures `server`; execute `server.new_session(session_name='run_shell_args_test')`; when `has_gte_version('3.7')`: execute `server.run_shell('echo #{1}-#{2}', args=['alpha', 'beta'])`; assert `result == ['alpha-beta']`; when `not (has_gte_version('3.7'))`: execute `server.run_shell('echo plain', args=['alpha'])`; expect `pytest.warns(UserWarning, match='args requires tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_run_shell_args_64d490d4a8 | 3.2a through 3.7b; conditional boundaries: 3.7 | planned parity | -| tests/test_server.py::test_run_shell_background | Test Server.run_shell() in background mode; fixtures `server`; execute `server.new_session(session_name='run_shell_bg_test')`; `server.run_shell('echo bg_test', background=True)`; assert `result is None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_run_shell_background_2301f556fa | 3.2a through 3.7b | planned parity | +| tests/test_server.py::test_run_shell_args | ``args`` fill #{1}/#{2} in the command on tmux 3.7+; warn below; fixtures `server`; execute `server.new_session(session_name='run_shell_args_test')`; when `has_gte_version('3.7')`: execute `server.run_shell('echo #{1}-#{2}', args=['alpha', 'beta'])`; assert `result == ['alpha-beta']`; when `not (has_gte_version('3.7'))`: execute `server.run_shell('echo plain', args=['alpha'])`; expect `pytest.warns(UserWarning, match='args requires tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_run_shell_args_64d490d4a8 | 3.3a through 3.7c; conditional boundaries: 3.7 | planned parity | +| tests/test_server.py::test_run_shell_background | Test Server.run_shell() in background mode; fixtures `server`; execute `server.new_session(session_name='run_shell_bg_test')`; `server.run_shell('echo bg_test', background=True)`; assert `result is None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_run_shell_background_2301f556fa | 3.3a through 3.7c | planned parity | | tests/test_server.py::test_run_shell_basic | Test Server.run_shell() executes command and returns output; fixtures `server`; execute `server.new_session(session_name='run_shell_test')`; `server.run_shell('echo hello_from_run_shell')`; `any(('hello_from_run_shell' in line for line in result))`; assert `result is not None`; `any(('hello_from_run_shell' in line for line in result))`; when `not has_gte_version('3.5')`: skip via `pytest.skip('run-shell stdout passthrough requires tmux 3.5+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_run_shell_basic_1089325e0a | 3.5 through 3.7b | planned parity | | tests/test_server.py::test_run_shell_cwd | ``cwd=`` sets the working directory for the shell command; fixtures `server`, `tmp_path`; execute `server.new_session(session_name='run_shell_cwd_test')`; `server.run_shell('pwd', cwd=tmp_path)`; `any((str(tmp_path) in line for line in result))`; assert `result is not None`; `any((str(tmp_path) in line for line in result))`; when `not has_gte_version('3.5')`: skip via `pytest.skip('run-shell stdout passthrough requires tmux 3.5+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_run_shell_cwd_ce2d8d6630 | 3.5 through 3.7b | planned parity | -| tests/test_server.py::test_run_shell_cwd_warns_on_old_tmux | ``cwd=`` emits a warning and skips ``-c`` on tmux <3.4; fixtures `server`, `monkeypatch`, `tmp_path`; execute `server.new_session(session_name='run_shell_cwd_warn_test')`; `server.run_shell('true', cwd=tmp_path)`; expect `pytest.warns(UserWarning, match='cwd requires tmux 3.4+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_run_shell_cwd_warns_on_old_tmux_556c18c74a | 3.2a through 3.7b | planned parity | +| tests/test_server.py::test_run_shell_cwd_warns_on_old_tmux | ``cwd=`` emits a warning and skips ``-c`` on tmux <3.4; fixtures `server`, `monkeypatch`, `tmp_path`; execute `server.new_session(session_name='run_shell_cwd_warn_test')`; `server.run_shell('true', cwd=tmp_path)`; expect `pytest.warns(UserWarning, match='cwd requires tmux 3.4+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_run_shell_cwd_warns_on_old_tmux_556c18c74a | 3.3a through 3.7c | planned parity | | tests/test_server.py::test_run_shell_show_stderr | ``show_stderr=True`` captures the command's stderr into the output; fixtures `server`; execute `server.new_session(session_name='run_shell_stderr_test')`; `server.run_shell("sh -c 'echo to_stdout; echo to_stderr >&2'", show_stderr=True)`; `'\n'.join(result)`; assert `result is not None`; `'to_stdout' in joined`; `'to_stderr' in joined`; when `not has_gte_version('3.6')`: skip via `pytest.skip('run-shell -E (JOB_SHOWSTDERR) requires tmux 3.6+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_run_shell_show_stderr_17f8c87e6e | 3.6 through 3.7b | planned parity | -| tests/test_server.py::test_run_shell_show_stderr_warns_on_old_tmux | ``show_stderr=True`` emits a warning and skips ``-E`` on tmux <3.6; fixtures `server`, `monkeypatch`; execute `server.new_session(session_name='run_shell_stderr_warn_test')`; `server.run_shell('true', show_stderr=True)`; expect `pytest.warns(UserWarning, match='show_stderr requires tmux 3.6+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_run_shell_show_stderr_warns_on_old_tmux_9edaad1fb4 | 3.2a through 3.7b | planned parity | +| tests/test_server.py::test_run_shell_show_stderr_warns_on_old_tmux | ``show_stderr=True`` emits a warning and skips ``-E`` on tmux <3.6; fixtures `server`, `monkeypatch`; execute `server.new_session(session_name='run_shell_stderr_warn_test')`; `server.run_shell('true', show_stderr=True)`; expect `pytest.warns(UserWarning, match='show_stderr requires tmux 3.6+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_run_shell_show_stderr_warns_on_old_tmux_9edaad1fb4 | 3.3a through 3.7c | planned parity | | tests/test_server.py::test_server_access_argv | ``server_access`` emits -r/-w when read_only/write are set; fixtures `monkeypatch`, `server`; execute `server.server_access(allow='alice', read_only=True)`; `server.server_access(allow='bob', write=True)`; `real_cmd(cmd, *args, **_kw)`; assert `captured[-1][1:] == ('-a', 'alice', '-r')`; `captured[-1][1:] == ('-a', 'bob', '-w')`; when `not has_gte_version('3.3')`: skip via `pytest.skip('server-access added in tmux 3.3')`; when `cmd == 'server-access'`: execute `_StubResult()`. | unit test | PythonBehaviorParityTest#tests_test_server_py_test_server_access_argv_07cf382bc4 | 3.3 through 3.7b | planned parity | | tests/test_server.py::test_server_access_list | Test Server.server_access() list mode; fixtures `server`; execute `server.new_session(session_name='access_test')`; `server.server_access(list_access=True)`; assert `isinstance(result, list)`; when `not has_gte_version('3.3')`: skip via `pytest.skip('server-access added in tmux 3.3')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_server_access_list_31f46755fe | 3.3 through 3.7b | planned parity | | tests/test_server.py::test_server_access_read_only_write_mutex | ``read_only`` and ``write`` are mutually exclusive; fixtures `server`; execute `server.server_access(allow='someuser', read_only=True, write=True)`; expect `pytest.raises(ValueError, match='mutually exclusive')`; when `not has_gte_version('3.3')`: skip via `pytest.skip('server-access added in tmux 3.3')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_server_access_read_only_write_mutex_d778617d24 | 3.3 through 3.7b | planned parity | | tests/test_server.py::test_server_clients_returns_empty_on_tmux_error | ``Server.clients`` returns an empty QueryList on tmux failure; fixtures `server`, `monkeypatch`; execute `exc.LibTmuxException('simulated list-clients failure')`; assert `list(server.clients) == []`. | unit test | PythonBehaviorParityTest#tests_test_server_py_test_server_clients_returns_empty_on_tmux_error_ea525b884d | not tmux-bound | planned parity | -| tests/test_server.py::test_server_context_manager | Test Server context manager functionality; fixtures `TestServer`; execute `TestServer()`; `server.new_session()`; `server.is_alive()`; assert `server.is_alive()`; `len(server.sessions) == 1`; `session in server.sessions`; `not server.is_alive()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_server_context_manager_1e327821d7 | 3.2a through 3.7b | planned parity | +| tests/test_server.py::test_server_context_manager | Test Server context manager functionality; fixtures `TestServer`; execute `TestServer()`; `server.new_session()`; `server.is_alive()`; assert `server.is_alive()`; `len(server.sessions) == 1`; `session in server.sessions`; `not server.is_alive()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_server_context_manager_1e327821d7 | 3.3a through 3.7c | planned parity | | tests/test_server.py::test_server_display_message_flags[all_formats] | Server.display_message() resolves server-scoped formats without a pane; case inputs `cmd=''`, `expected_in_output='session_name'`, `kwargs={'get_text': True, 'all_formats': True}`, `min_tmux_version=None`, `test_id='all_formats'`; fixtures `control_mode`, `server`; execute `control_mode()`; `server.display_message(cmd, **call_kwargs)`; `call_kwargs.setdefault('target_client', ctl.client_name)`; when `not has_gte_version('3.3')`: skip via `pytest.skip('display-message -p via control-mode client unreliable on tmux 3.2a')`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_in_output is not None`: execute `'\n'.join(result)`; assert `result is not None`; `expected_in_output in output`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_server_display_message_flags_all_formats_5192eaab04 | 3.3 through 3.7b | planned parity | | tests/test_server.py::test_server_display_message_flags[no_expand_literal] | Server.display_message() resolves server-scoped formats without a pane; case inputs `cmd='#{version}'`, `expected_in_output='#{version}'`, `kwargs={'get_text': True, 'no_expand': True}`, `min_tmux_version='3.4'`, `test_id='no_expand_literal'`; fixtures `control_mode`, `server`; execute `control_mode()`; `server.display_message(cmd, **call_kwargs)`; `call_kwargs.setdefault('target_client', ctl.client_name)`; when `not has_gte_version('3.3')`: skip via `pytest.skip('display-message -p via control-mode client unreliable on tmux 3.2a')`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_in_output is not None`: execute `'\n'.join(result)`; assert `result is not None`; `expected_in_output in output`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_server_display_message_flags_no_expand_literal_a0dacb0756 | 3.4 through 3.7b | planned parity | | tests/test_server.py::test_server_display_message_flags[socket_path_format_string] | Server.display_message() resolves server-scoped formats without a pane; case inputs `cmd=''`, `expected_in_output='/'`, `kwargs={'get_text': True, 'format_string': '#{socket_path}'}`, `min_tmux_version=None`, `test_id='socket_path_format_string'`; fixtures `control_mode`, `server`; execute `control_mode()`; `server.display_message(cmd, **call_kwargs)`; `call_kwargs.setdefault('target_client', ctl.client_name)`; when `not has_gte_version('3.3')`: skip via `pytest.skip('display-message -p via control-mode client unreliable on tmux 3.2a')`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_in_output is not None`: execute `'\n'.join(result)`; assert `result is not None`; `expected_in_output in output`. | real-tmux integration test | PythonBehaviorParityTest#st_server_py_test_server_display_message_flags_socket_path_format_string_0827497ab3 | 3.3 through 3.7b | planned parity | | tests/test_server.py::test_server_display_message_flags[version] | Server.display_message() resolves server-scoped formats without a pane; case inputs `cmd='#{version}'`, `expected_in_output='.'`, `kwargs={'get_text': True}`, `min_tmux_version=None`, `test_id='version'`; fixtures `control_mode`, `server`; execute `control_mode()`; `server.display_message(cmd, **call_kwargs)`; `call_kwargs.setdefault('target_client', ctl.client_name)`; when `not has_gte_version('3.3')`: skip via `pytest.skip('display-message -p via control-mode client unreliable on tmux 3.2a')`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_in_output is not None`: execute `'\n'.join(result)`; assert `result is not None`; `expected_in_output in output`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_server_display_message_flags_version_8f43cbd540 | 3.3 through 3.7b | planned parity | -| tests/test_server.py::test_server_display_message_no_text_returns_none | Without ``get_text=True`` the call renders to status line and returns None; fixtures `control_mode`, `server`; execute `control_mode()`; `server.display_message('hi from libtmux', target_client=ctl.client_name)`; assert `result is None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_server_display_message_no_text_returns_none_d9e3b01060 | 3.2a through 3.7b | planned parity | +| tests/test_server.py::test_server_display_message_no_text_returns_none | Without ``get_text=True`` the call renders to status line and returns None; fixtures `control_mode`, `server`; execute `control_mode()`; `server.display_message('hi from libtmux', target_client=ctl.client_name)`; assert `result is None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_server_display_message_no_text_returns_none_d9e3b01060 | 3.3a through 3.7c | planned parity | | tests/test_server.py::test_server_display_message_target_client | ``target_client`` is plumbed through as ``-c``; get_text=True returns stdout; fixtures `control_mode`, `server`; execute `control_mode()`; `server.display_message('#{version}', get_text=True, target_client=ctl.client_name)`; `result[0].strip()`; assert `isinstance(result, list)`; `len(result) == 1`; `result[0].strip() != ''`; when `not has_gte_version('3.3')`: skip via `pytest.skip('display-message -p via control-mode client unreliable on tmux 3.2a')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_server_display_message_target_client_8b56463be4 | 3.3 through 3.7b | planned parity | -| tests/test_server.py::test_server_display_message_warns_on_tmux_error | Tmux stderr on ``display-message`` surfaces as a :class:`UserWarning`; fixtures `server`, `session`; execute `server.display_message('x', get_text=True, format_string='#{version}')`; expect `pytest.warns(UserWarning, match='only one of -F or argument')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_server_display_message_warns_on_tmux_error_eae195e1a6 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_server_search_panes_filter_by_id | ``Server.list_panes(filter=...)`` returns only the pane id we asked for; fixtures `server`; execute `server.new_session(session_name='gap7_pane_demo')`; `server.search_panes(filter=f'#{{m:{target.pane_id},#{{pane_id}}}}')`; `sess.active_window.split()`; assert `[p.pane_id for p in matches] == [target.pane_id]`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_server_search_panes_filter_by_id_f47562f057 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_server_search_sessions_filter | ``Server.list_sessions(filter=...)`` returns only matching sessions; fixtures `server`; execute `server.new_session(session_name='gap7_keep_alpha')`; `server.new_session(session_name='gap7_keep_beta')`; `server.new_session(session_name='other_drop')`; `server.search_sessions(filter='#{m:gap7_*,#{session_name}}')`; assert `names == ['gap7_keep_alpha', 'gap7_keep_beta']`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_server_search_sessions_filter_ec7be52ce7 | 3.2a through 3.7b | planned parity | +| tests/test_server.py::test_server_display_message_warns_on_tmux_error | Tmux stderr on ``display-message`` surfaces as a :class:`UserWarning`; fixtures `server`, `session`; execute `server.display_message('x', get_text=True, format_string='#{version}')`; expect `pytest.warns(UserWarning, match='only one of -F or argument')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_server_display_message_warns_on_tmux_error_eae195e1a6 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_server_search_panes_filter_by_id | ``Server.list_panes(filter=...)`` returns only the pane id we asked for; fixtures `server`; execute `server.new_session(session_name='gap7_pane_demo')`; `server.search_panes(filter=f'#{{m:{target.pane_id},#{{pane_id}}}}')`; `sess.active_window.split()`; assert `[p.pane_id for p in matches] == [target.pane_id]`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_server_search_panes_filter_by_id_f47562f057 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_server_search_sessions_filter | ``Server.list_sessions(filter=...)`` returns only matching sessions; fixtures `server`; execute `server.new_session(session_name='gap7_keep_alpha')`; `server.new_session(session_name='gap7_keep_beta')`; `server.new_session(session_name='other_drop')`; `server.search_sessions(filter='#{m:gap7_*,#{session_name}}')`; assert `names == ['gap7_keep_alpha', 'gap7_keep_beta']`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_server_search_sessions_filter_ec7be52ce7 | 3.3a through 3.7c | planned parity | | tests/test_server.py::test_server_search_sessions_propagates_errors | ``Server.search_sessions`` re-raises tmux errors instead of swallowing them; fixtures `server`, `monkeypatch`; execute `server.search_sessions(filter='#{m:keep_*,#{session_name}}')`; `exc.LibTmuxException('simulated list-sessions failure')`; expect `pytest.raises(exc.LibTmuxException, match='simulated list-sessions failure')`. | unit test | PythonBehaviorParityTest#tests_test_server_py_test_server_search_sessions_propagates_errors_66a827c1ce | not tmux-bound | planned parity | -| tests/test_server.py::test_server_search_windows_filter | ``Server.list_windows(filter=...)`` returns only matching windows; fixtures `server`; execute `server.new_session(session_name='gap7_win_demo')`; `server.search_windows(filter='#{m:gap7_*,#{window_name}}')`; `sess.new_window(window_name='gap7_target')`; `sess.new_window(window_name='other_window')`; assert `'gap7_target' in names`; `'other_window' not in names`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_server_search_windows_filter_72abfdcb96 | 3.2a through 3.7b | planned parity | +| tests/test_server.py::test_server_search_windows_filter | ``Server.list_windows(filter=...)`` returns only matching windows; fixtures `server`; execute `server.new_session(session_name='gap7_win_demo')`; `server.search_windows(filter='#{m:gap7_*,#{window_name}}')`; `sess.new_window(window_name='gap7_target')`; `sess.new_window(window_name='other_window')`; assert `'gap7_target' in names`; `'other_window' not in names`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_server_search_windows_filter_72abfdcb96 | 3.3a through 3.7c | planned parity | | tests/test_server.py::test_server_sessions_missing_socket_returns_empty | A not-yet-created tmux socket preserves the empty-list contract; fixtures `tmp_path`; execute `Server(socket_path=tmp_path / 'missing.sock')`; assert `list(missing_server.sessions) == []`. | unit test | PythonBehaviorParityTest#tests_test_server_py_test_server_sessions_missing_socket_returns_empty_8e313dc930 | not tmux-bound | planned parity | | tests/test_server.py::test_server_sessions_permission_error_returns_empty | Connection errors are absorbed into the empty-list contract too; fixtures `server`, `monkeypatch`; execute `exc.LibTmuxException('error connecting to /root/libtmux-review.sock (Permission denied)')`; assert `list(server.sessions) == []`. | unit test | PythonBehaviorParityTest#tests_test_server_py_test_server_sessions_permission_error_returns_empty_bd9199538a | not tmux-bound | planned parity | | tests/test_server.py::test_server_sessions_returns_empty_on_tmux_error | ``Server.sessions`` returns an empty QueryList on tmux failure; fixtures `server`, `monkeypatch`; execute `exc.LibTmuxException('simulated list-sessions failure')`; assert `list(server.sessions) == []`. | unit test | PythonBehaviorParityTest#tests_test_server_py_test_server_sessions_returns_empty_on_tmux_error_0bb2a2f0f0 | not tmux-bound | planned parity | -| tests/test_server.py::test_sessions_property_hydrates_cross_scope | Server.sessions hydrates active window/pane via tmux's format_defaults cascade; fixtures `server`; execute `server.new_session(session_name='hydration_cascade_sessions')`; assert `fetched is not None`; `fetched.window_id == new_session.active_window.window_id`; `active_pane is not None`; `fetched.pane_id == active_pane.pane_id`; `fetched.pane_current_command is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_sessions_property_hydrates_cross_scope_2c7b5a5326 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_show_environment | Server.show_environment() returns dict; fixtures `server`; execute `server.show_environment()`; assert `isinstance(vars_, dict)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_show_environment_7ba64484f0 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_show_environment_not_set | Unset environment variable returns None; fixtures `server`; execute `server.getenv('BAR')`; assert `server.getenv('BAR') is None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_show_environment_not_set_1582655456 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_show_messages | Test Server.show_messages() returns message log; fixtures `control_mode`, `server`; execute `control_mode()`; `server.show_messages(target_client=ctl.client_name)`; assert `isinstance(result, list)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_show_messages_34918c629f | 3.2a through 3.7b | planned parity | +| tests/test_server.py::test_sessions_property_hydrates_cross_scope | Server.sessions hydrates active window/pane via tmux's format_defaults cascade; fixtures `server`; execute `server.new_session(session_name='hydration_cascade_sessions')`; assert `fetched is not None`; `fetched.window_id == new_session.active_window.window_id`; `active_pane is not None`; `fetched.pane_id == active_pane.pane_id`; `fetched.pane_current_command is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_sessions_property_hydrates_cross_scope_2c7b5a5326 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_show_environment | Server.show_environment() returns dict; fixtures `server`; execute `server.show_environment()`; assert `isinstance(vars_, dict)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_show_environment_7ba64484f0 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_show_environment_not_set | Unset environment variable returns None; fixtures `server`; execute `server.getenv('BAR')`; assert `server.getenv('BAR') is None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_show_environment_not_set_1582655456 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_show_messages | Test Server.show_messages() returns message log; fixtures `control_mode`, `server`; execute `control_mode()`; `server.show_messages(target_client=ctl.client_name)`; assert `isinstance(result, list)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_show_messages_34918c629f | 3.3a through 3.7c | planned parity | | tests/test_server.py::test_show_messages_terminals_jobs | Test Server.show_messages(terminals=...) and (jobs=...) work clientless; fixtures `server`; execute `server.new_session(session_name='showmsg_alt_test')`; `server.show_messages(terminals=True)`; `server.show_messages(jobs=True)`; assert `isinstance(terminals, list)`; `isinstance(jobs, list)`; when `not has_gte_version('3.6')`: skip via `pytest.skip('show-messages -T/-J without a client requires tmux 3.6+')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_show_messages_terminals_jobs_e75a4aff0b | 3.6 through 3.7b | planned parity | | tests/test_server.py::test_show_prompt_history | Test Server.show_prompt_history() returns history; fixtures `server`; execute `server.new_session(session_name='showph_test')`; `server.show_prompt_history()`; assert `isinstance(result, list)`; when `not has_gte_version('3.3')`: skip via `pytest.skip('show-prompt-history added in tmux 3.3')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_show_prompt_history_33bd02697d | 3.3 through 3.7b | planned parity | -| tests/test_server.py::test_socket_name | ``-L`` socket_name; fixtures `server`; execute `Server(socket_name='test')`; assert `myserver.socket_name == 'test'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_socket_name_b0b7edd056 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_socket_name_factory | Verify socket_name_factory generates socket names; fixtures `server`; execute `Server(socket_name_factory=socket_name_factory)`; assert `myserver.socket_name == 'test_socket_0'`; `socket_names == ['test_socket_0']`; `myserver2.socket_name == 'test_socket_1'`; `socket_names == ['test_socket_0', 'test_socket_1']`; when `myserver2.is_alive()`: execute `myserver2.kill()`; when `myserver.is_alive()`: execute `myserver.kill()`; when `myserver2.is_alive()`: execute `myserver2.kill()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_socket_name_factory_9fecad211b | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_socket_name_precedence | Verify socket_name takes precedence over socket_name_factory; fixtures `server`; execute `Server(socket_name='explicit_name', socket_name_factory=socket_name_factory)`; `Server(socket_name_factory=socket_name_factory)`; assert `myserver.socket_name == 'explicit_name'`; `myserver2.socket_name == 'from_factory'`; when `myserver.is_alive()`: execute `myserver.kill()`; when `myserver2.is_alive()`: execute `myserver2.kill()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_socket_name_precedence_e229b2ee2c | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_socket_path | ``-S`` socket_path (alternative path for server socket); fixtures `server`; execute `Server(socket_path='test')`; assert `myserver.socket_path == 'test'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_socket_path_414cdd0f5d | 3.2a through 3.7b | planned parity | +| tests/test_server.py::test_socket_name | ``-L`` socket_name; fixtures `server`; execute `Server(socket_name='test')`; assert `myserver.socket_name == 'test'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_socket_name_b0b7edd056 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_socket_name_factory | Verify socket_name_factory generates socket names; fixtures `server`; execute `Server(socket_name_factory=socket_name_factory)`; assert `myserver.socket_name == 'test_socket_0'`; `socket_names == ['test_socket_0']`; `myserver2.socket_name == 'test_socket_1'`; `socket_names == ['test_socket_0', 'test_socket_1']`; when `myserver2.is_alive()`: execute `myserver2.kill()`; when `myserver.is_alive()`: execute `myserver.kill()`; when `myserver2.is_alive()`: execute `myserver2.kill()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_socket_name_factory_9fecad211b | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_socket_name_precedence | Verify socket_name takes precedence over socket_name_factory; fixtures `server`; execute `Server(socket_name='explicit_name', socket_name_factory=socket_name_factory)`; `Server(socket_name_factory=socket_name_factory)`; assert `myserver.socket_name == 'explicit_name'`; `myserver2.socket_name == 'from_factory'`; when `myserver.is_alive()`: execute `myserver.kill()`; when `myserver2.is_alive()`: execute `myserver2.kill()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_socket_name_precedence_e229b2ee2c | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_socket_path | ``-S`` socket_path (alternative path for server socket); fixtures `server`; execute `Server(socket_path='test')`; assert `myserver.socket_path == 'test'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_socket_path_414cdd0f5d | 3.3a through 3.7c | planned parity | | tests/test_server.py::test_socket_path_not_derived_from_socket_name | A named socket leaves ``socket_path`` unset; execute `Server(socket_name='libtmux_test_named_socket')`; assert `myserver.socket_name == 'libtmux_test_named_socket'`; `myserver.socket_path is None`. | unit test | PythonBehaviorParityTest#tests_test_server_py_test_socket_path_not_derived_from_socket_name_6ee7453b0f | not tmux-bound | planned parity | -| tests/test_server.py::test_source_file | Test Server.source_file() sources a config file; fixtures `server`, `tmp_path`; execute `server.new_session(session_name='source_test')`; `server.source_file(conf)`; `server.cmd('show-options', '-gv', '@source_test_opt')`; `conf.write_text('set -g @source_test_opt yes\n')`; assert `result.stdout[0] == 'yes'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_source_file_08aff0a5fe | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_source_file_quiet | Test Server.source_file() with quiet flag ignores missing files; fixtures `server`; execute `server.new_session(session_name='source_quiet')`; `server.source_file('/nonexistent/path.conf', quiet=True)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_source_file_quiet_920d1689dc | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_start_server | Test Server.start_server() runs without error; fixtures `server`; execute `server.new_session(session_name='startsvr_test')`; `server.start_server()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_start_server_aca8dbe99f | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_suspend_client | Test Server.suspend_client() runs without error; fixtures `control_mode`, `server`; execute `control_mode()`; `server.suspend_client()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_suspend_client_bca6a43c1e | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_tmux_bin_custom_path | Custom tmux_bin path is used for commands; fixtures `caplog`; execute `shutil.which('tmux')`; `Server(socket_name='test_tmux_bin', tmux_bin=tmux_path)`; `s.cmd('list-sessions')`; `any((tmux_path in r.tmux_cmd for r in running_records))`; assert `tmux_path is not None`; `s.tmux_bin == tmux_path`; `any((tmux_path in r.tmux_cmd for r in running_records))`; when `s.is_alive()`: execute `s.kill()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_tmux_bin_custom_path_cfdd5e61a4 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_tmux_bin_default | Default tmux_bin is None, falls back to shutil.which; fixtures `server`; assert `server.tmux_bin is None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_tmux_bin_default_0871ffd23c | 3.2a through 3.7b | planned parity | +| tests/test_server.py::test_source_file | Test Server.source_file() sources a config file; fixtures `server`, `tmp_path`; execute `server.new_session(session_name='source_test')`; `server.source_file(conf)`; `server.cmd('show-options', '-gv', '@source_test_opt')`; `conf.write_text('set -g @source_test_opt yes\n')`; assert `result.stdout[0] == 'yes'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_source_file_08aff0a5fe | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_source_file_quiet | Test Server.source_file() with quiet flag ignores missing files; fixtures `server`; execute `server.new_session(session_name='source_quiet')`; `server.source_file('/nonexistent/path.conf', quiet=True)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_source_file_quiet_920d1689dc | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_start_server | Test Server.start_server() runs without error; fixtures `server`; execute `server.new_session(session_name='startsvr_test')`; `server.start_server()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_start_server_aca8dbe99f | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_suspend_client | Test Server.suspend_client() runs without error; fixtures `control_mode`, `server`; execute `control_mode()`; `server.suspend_client()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_suspend_client_bca6a43c1e | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_tmux_bin_custom_path | Custom tmux_bin path is used for commands; fixtures `caplog`; execute `shutil.which('tmux')`; `Server(socket_name='test_tmux_bin', tmux_bin=tmux_path)`; `s.cmd('list-sessions')`; `any((tmux_path in r.tmux_cmd for r in running_records))`; assert `tmux_path is not None`; `s.tmux_bin == tmux_path`; `any((tmux_path in r.tmux_cmd for r in running_records))`; when `s.is_alive()`: execute `s.kill()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_tmux_bin_custom_path_cfdd5e61a4 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_tmux_bin_default | Default tmux_bin is None, falls back to shutil.which; fixtures `server`; assert `server.tmux_bin is None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_tmux_bin_default_0871ffd23c | 3.3a through 3.7c | planned parity | | tests/test_server.py::test_tmux_bin_invalid_path | Invalid tmux_bin raises TmuxCommandNotFound; execute `Server(tmux_bin='/nonexistent/tmux')`; `s.cmd('list-sessions')`; expect `pytest.raises(exc.TmuxCommandNotFound)`. | unit test | PythonBehaviorParityTest#tests_test_server_py_test_tmux_bin_invalid_path_65283949c8 | not tmux-bound | planned parity | | tests/test_server.py::test_tmux_bin_invalid_path_raise_if_dead | Invalid tmux_bin raises TmuxCommandNotFound in raise_if_dead(); execute `Server(tmux_bin='/nonexistent/tmux')`; `s.raise_if_dead()`; expect `pytest.raises(exc.TmuxCommandNotFound)`. | unit test | PythonBehaviorParityTest#tests_test_server_py_test_tmux_bin_invalid_path_raise_if_dead_5cc8c0e5e7 | not tmux-bound | planned parity | -| tests/test_server.py::test_wait_for_set_flag | Test Server.wait_for() with set_flag; fixtures `server`; execute `server.new_session(session_name='wait_test')`; `server.wait_for('test_channel_set', set_flag=True)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_wait_for_set_flag_094065e1a9 | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_windows_property_hydrates_active_pane | Server.windows hydrates each window's active pane via the cascade; fixtures `server`, `session`; execute `session.new_window(window_name='hydration_cascade_windows')`; assert `fetched is not None`; `active_pane is not None`; `fetched.pane_id == active_pane.pane_id`; `fetched.pane_current_command is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_windows_property_hydrates_active_pane_14771ea36b | 3.2a through 3.7b | planned parity | -| tests/test_server.py::test_with_server_is_alive | Verify is_alive() returns True when tmux server is alive; fixtures `server`; execute `server.new_session()`; `server.is_alive()`; assert `server.is_alive()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_with_server_is_alive_b7b068c8dd | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_active_pane | Session.active_pane returns Pane; fixtures `session`; assert `isinstance(session.active_pane, Pane)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_active_pane_43afb8c4be | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_active_window | Session.active_window returns Window; fixtures `session`; assert `isinstance(session.active_window, Window)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_active_window_fae471f129 | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_cmd_inserts_session_id | Verify Session.cmd() inserts session_id; fixtures `session`; execute `session.cmd('not-a-command', last_arg)`; assert `'-t' in cmd.cmd`; `current_session_id in cmd.cmd`; `cmd.cmd[-1] == last_arg`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_cmd_inserts_session_id_7f4de558f3 | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_detach_client_no_target_detaches_all_session_clients | Test Session.detach_client() without a target detaches all session clients; fixtures `control_mode`, `session`, `server`; execute `control_mode()`; `server.cmd('list-clients', '-F', '#{client_name}')`; `session.detach_client()`; assert `len(before) == 2`; `len(after) == 0`. | real-tmux integration test | PythonBehaviorParityTest#est_session_py_test_detach_client_no_target_detaches_all_session_clients_b3e4b9869c | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_empty_session_option_returns_None | Verify Session._show_option returns None for unset option; fixtures `session`; execute `session._show_option('default-shell')`; assert `session._show_option('default-shell') is None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_empty_session_option_returns_None_19c68e0e4c | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_has_session | Server.has_session returns True if has session_name exists; fixtures `server`, `session`; execute `server.has_session(TEST_SESSION_NAME)`; `server.has_session(TEST_SESSION_NAME[:-2])`; `server.has_session(TEST_SESSION_NAME[:-2], exact=False)`; `server.has_session('asdf2314324321')`; assert `TEST_SESSION_NAME is not None`; `server.has_session(TEST_SESSION_NAME)`; `not server.has_session(TEST_SESSION_NAME[:-2])`; `server.has_session(TEST_SESSION_NAME[:-2], exact=False)`; `not server.has_session('asdf2314324321')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_has_session_2fd59585c3 | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_kill_session_group | Session.kill(group=True) uses -g on tmux 3.7+; warn-and-ignore below; fixtures `server`; execute `server.new_session(session_name='kill_group_base')`; when `has_gte_version('3.7')`: execute `session.kill(group=True)`; `server.has_session('kill_group_base')`; assert `not server.has_session('kill_group_base')`; when `not (has_gte_version('3.7'))`: execute `session.kill(group=True)`; expect `pytest.warns(UserWarning, match='group requires tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_kill_session_group_70d2bafe79 | 3.2a through 3.7b; conditional boundaries: 3.7 | planned parity | -| tests/test_session.py::test_last_window | Test Session.last_window() selects previous window; fixtures `session`; execute `session.new_window(window_name='last_a', attach=True)`; `session.new_window(window_name='last_b', attach=True)`; `session.refresh()`; `session.last_window()`; assert `session.active_window.window_id == w2.window_id`; `result.window_id == w1.window_id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_last_window_57ed77cbf1 | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_lock_session | Test Session.lock_session() runs without error; fixtures `session`; execute `session.lock_session()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_lock_session_1cc8396c56 | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_new_session | Server.new_session creates new session; fixtures `server`; execute `server.new_session(session_name=new_session_name, detach=True)`; assert `isinstance(new_session, Session)`; `new_session.session_name == new_session_name`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_new_session_e2de3569ae | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_new_window_kill_existing | Test Session.new_window() with kill_existing flag; fixtures `session`; execute `session.new_window(window_name='kill_orig', window_index='5')`; `session.new_window(window_name='kill_replace', window_index='5', kill_existing=True)`; `session.refresh()`; assert `w1.window_name == 'kill_orig'`; `w1.window_index == '5'`; `w2.window_name == 'kill_replace'`; `w2.window_index == '5'`; `'kill_orig' not in names`; `'kill_replace' in names`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_new_window_kill_existing_bd39f40040 | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_new_window_select_existing | Test Session.new_window() with select_existing flag — new name; fixtures `session`; execute `session.new_window(window_name='selexist_new', select_existing=True)`; assert `w.window_name == 'selexist_new'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_new_window_select_existing_d6c7048fc6 | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_new_window_start_directory[empty_string] | Test Session.new_window start_directory parameter handling; case inputs `description='Empty string should not add -c flag'`, `start_directory=''`, `test_id='empty_string'`; fixtures `session`, `monkeypatch`, `tmp_path`, `user_path`; execute `session.new_window(window_name=f'test_window_{test_id}', start_directory=actual_start_directory)`; assert `window.window_name == f'test_window_{test_id}'`; `window in session.windows`; when `start_directory and str(start_directory) not in {'', 'None'} and not (f'{user_path}' in str(start_directory)) and str(start_directory).startswith('./')`: execute `temp_dir.mkdir(parents=True, exist_ok=True)`; `temp_dir.resolve()`; when `expected_path`: execute `active_pane.refresh()`; `pathlib.Path(active_pane.pane_current_path).resolve()`; `pathlib.Path(active_pane.pane_current_path)`; assert `active_pane is not None`; `active_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_new_window_start_directory_empty_string_53afd32113 | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_new_window_start_directory[none_value] | Test Session.new_window start_directory parameter handling; case inputs `description='None should not add -c flag'`, `start_directory=None`, `test_id='none_value'`; fixtures `session`, `monkeypatch`, `tmp_path`, `user_path`; execute `session.new_window(window_name=f'test_window_{test_id}', start_directory=actual_start_directory)`; assert `window.window_name == f'test_window_{test_id}'`; `window in session.windows`; when `start_directory and str(start_directory) not in {'', 'None'} and not (f'{user_path}' in str(start_directory)) and str(start_directory).startswith('./')`: execute `temp_dir.mkdir(parents=True, exist_ok=True)`; `temp_dir.resolve()`; when `expected_path`: execute `active_pane.refresh()`; `pathlib.Path(active_pane.pane_current_path).resolve()`; `pathlib.Path(active_pane.pane_current_path)`; assert `active_pane is not None`; `active_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_new_window_start_directory_none_value_61ebed7357 | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_new_window_start_directory[relative_path] | Test Session.new_window start_directory parameter handling; case inputs `description='Relative path should add -c flag'`, `start_directory='./relative/path'`, `test_id='relative_path'`; fixtures `session`, `monkeypatch`, `tmp_path`, `user_path`; execute `session.new_window(window_name=f'test_window_{test_id}', start_directory=actual_start_directory)`; assert `window.window_name == f'test_window_{test_id}'`; `window in session.windows`; when `start_directory and str(start_directory) not in {'', 'None'} and not (f'{user_path}' in str(start_directory)) and str(start_directory).startswith('./')`: execute `temp_dir.mkdir(parents=True, exist_ok=True)`; `temp_dir.resolve()`; when `expected_path`: execute `active_pane.refresh()`; `pathlib.Path(active_pane.pane_current_path).resolve()`; `pathlib.Path(active_pane.pane_current_path)`; assert `active_pane is not None`; `active_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_new_window_start_directory_relative_path_df9ac332cd | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_new_window_start_directory[user_path] | Test Session.new_window start_directory parameter handling; case inputs `description='User path should add -c flag'`, `start_directory='{user_path}'`, `test_id='user_path'`; fixtures `session`, `monkeypatch`, `tmp_path`, `user_path`; execute `session.new_window(window_name=f'test_window_{test_id}', start_directory=actual_start_directory)`; assert `window.window_name == f'test_window_{test_id}'`; `window in session.windows`; when `start_directory and str(start_directory) not in {'', 'None'} and not (f'{user_path}' in str(start_directory)) and str(start_directory).startswith('./')`: execute `temp_dir.mkdir(parents=True, exist_ok=True)`; `temp_dir.resolve()`; when `expected_path`: execute `active_pane.refresh()`; `pathlib.Path(active_pane.pane_current_path).resolve()`; `pathlib.Path(active_pane.pane_current_path)`; assert `active_pane is not None`; `active_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_new_window_start_directory_user_path_e9b3c36747 | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_new_window_start_directory_pathlib | Test Session.new_window accepts pathlib.Path for start_directory; fixtures `session`, `user_path`; execute `session.new_window(window_name='test_pathlib_start_dir', start_directory=user_path)`; `user_path.resolve()`; `active_pane.refresh()`; `pathlib.Path(active_pane.pane_current_path).resolve()`; `pathlib.Path(active_pane.pane_current_path)`; assert `window.window_name == 'test_pathlib_start_dir'`; `window in session.windows`; `active_pane is not None`; `active_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_new_window_start_directory_pathlib_ebf77c18a8 | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_new_window_with_environment[multiple_env_vars] | Verify new window with environment vars; case inputs `environment={'ENV_VAR_1': 'window_1', 'ENV_VAR_2': 'window_2'}`, `test_id='multiple_env_vars'`; fixtures `session`; execute `session.new_window(attach=True, window_name='window_with_environment', window_shell=f"{env} PS1='$ ' sh", environment=environment)`; `pane.send_keys(f'echo ${k}')`; `pane.capture_pane()`; `shutil.which('env')`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; `pane.capture_pane()[-2] == v`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_new_window_with_environment_multiple_env_vars_cc4d25b3ea | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_new_window_with_environment[single_env_var] | Verify new window with environment vars; case inputs `environment={'ENV_VAR': 'window'}`, `test_id='single_env_var'`; fixtures `session`; execute `session.new_window(attach=True, window_name='window_with_environment', window_shell=f"{env} PS1='$ ' sh", environment=environment)`; `pane.send_keys(f'echo ${k}')`; `pane.capture_pane()`; `shutil.which('env')`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; `pane.capture_pane()[-2] == v`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_new_window_with_environment_single_env_var_2f715b3eb0 | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_next_window | Test Session.next_window() selects next window; fixtures `session`; execute `session.new_window(window_name='next_a', attach=True)`; `session.new_window(window_name='next_b', attach=False)`; `session.refresh()`; `session.next_window()`; assert `session.active_window.window_id == w1.window_id`; `result.window_id != w1.window_id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_next_window_4309c4baa6 | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_periods_raise_bad_session_name[colon_in_name] | Verify session names with periods raise BadSessionName; case inputs `raises=True`, `session_name='hey:its a colon'`, `test_id='colon_in_name'`; fixtures `server`, `session`; when `raises`: execute `session.rename_session(new_name)`; `server.new_session(session_name)`; `server.has_session(session_name)`; `server.switch_client(new_name)`; `server.attach_session(new_name)`; expect `pytest.raises(exc.BadSessionName)`; when `not (raises)`: execute `server.new_session(session_name)`; `server.has_session(session_name)`; `session.rename_session(new_name)`; `server.switch_client(new_name)`; expect `pytest.raises(exc.LibTmuxException)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_periods_raise_bad_session_name_colon_in_name_aca7571512 | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_periods_raise_bad_session_name[period_in_name] | Verify session names with periods raise BadSessionName; case inputs `raises=True`, `session_name='hey.period'`, `test_id='period_in_name'`; fixtures `server`, `session`; when `raises`: execute `session.rename_session(new_name)`; `server.new_session(session_name)`; `server.has_session(session_name)`; `server.switch_client(new_name)`; `server.attach_session(new_name)`; expect `pytest.raises(exc.BadSessionName)`; when `not (raises)`: execute `server.new_session(session_name)`; `server.has_session(session_name)`; `session.rename_session(new_name)`; `server.switch_client(new_name)`; expect `pytest.raises(exc.LibTmuxException)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_periods_raise_bad_session_name_period_in_name_4aca936d92 | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_periods_raise_bad_session_name[valid_name] | Verify session names with periods raise BadSessionName; case inputs `raises=False`, `session_name='hey moo'`, `test_id='valid_name'`; fixtures `server`, `session`; when `raises`: execute `session.rename_session(new_name)`; `server.new_session(session_name)`; `server.has_session(session_name)`; `server.switch_client(new_name)`; `server.attach_session(new_name)`; expect `pytest.raises(exc.BadSessionName)`; when `not (raises)`: execute `server.new_session(session_name)`; `server.has_session(session_name)`; `session.rename_session(new_name)`; `server.switch_client(new_name)`; expect `pytest.raises(exc.LibTmuxException)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_periods_raise_bad_session_name_valid_name_58f1b62bac | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_previous_window | Test Session.previous_window() selects previous window; fixtures `session`; execute `session.new_window(window_name='prev_a', attach=True)`; `session.new_window(window_name='prev_b', attach=True)`; `session.refresh()`; `session.previous_window()`; assert `session.active_window.window_id == w2.window_id`; `result.window_id == w1.window_id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_previous_window_b24c28acd6 | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_remove_environment | Remove environment variable; fixtures `session`; execute `session.getenv('BAM')`; `session.set_environment('BAM', 'OK')`; `session.remove_environment('BAM')`; assert `session.getenv('BAM') is None`; `session.getenv('BAM') == 'OK'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_remove_environment_3dc5ca0644 | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_select_window | Session.select_window moves window; fixtures `session`; execute `session.new_window(window_name='test_window')`; `session.select_window(window_base_index)`; `session.select_window(window_base_index + 1)`; assert `window_idx is not None`; `window_count >= 2`; `len(session.windows) == window_count`; `isinstance(selected_window1, Window)`; `selected_window1.id == active_window1.id`; `isinstance(selected_window2, Window)`; `selected_window2.id == active_window2.id`; `selected_window1.id != selected_window2.id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_select_window_3c2530cb01 | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_select_window_returns_Window | Session.select_window returns Window object; fixtures `session`; execute `session.select_window(window_base_index)`; assert `len(session.windows) == window_count`; `window_idx is not None`; `isinstance(window, Window)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_select_window_returns_Window_cfccc7771f | 3.2a through 3.7b | planned parity | +| tests/test_server.py::test_wait_for_set_flag | Test Server.wait_for() with set_flag; fixtures `server`; execute `server.new_session(session_name='wait_test')`; `server.wait_for('test_channel_set', set_flag=True)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_wait_for_set_flag_094065e1a9 | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_windows_property_hydrates_active_pane | Server.windows hydrates each window's active pane via the cascade; fixtures `server`, `session`; execute `session.new_window(window_name='hydration_cascade_windows')`; assert `fetched is not None`; `active_pane is not None`; `fetched.pane_id == active_pane.pane_id`; `fetched.pane_current_command is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_windows_property_hydrates_active_pane_14771ea36b | 3.3a through 3.7c | planned parity | +| tests/test_server.py::test_with_server_is_alive | Verify is_alive() returns True when tmux server is alive; fixtures `server`; execute `server.new_session()`; `server.is_alive()`; assert `server.is_alive()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_server_py_test_with_server_is_alive_b7b068c8dd | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_active_pane | Session.active_pane returns Pane; fixtures `session`; assert `isinstance(session.active_pane, Pane)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_active_pane_43afb8c4be | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_active_window | Session.active_window returns Window; fixtures `session`; assert `isinstance(session.active_window, Window)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_active_window_fae471f129 | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_cmd_inserts_session_id | Verify Session.cmd() inserts session_id; fixtures `session`; execute `session.cmd('not-a-command', last_arg)`; assert `'-t' in cmd.cmd`; `current_session_id in cmd.cmd`; `cmd.cmd[-1] == last_arg`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_cmd_inserts_session_id_7f4de558f3 | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_detach_client_no_target_detaches_all_session_clients | Test Session.detach_client() without a target detaches all session clients; fixtures `control_mode`, `session`, `server`; execute `control_mode()`; `server.cmd('list-clients', '-F', '#{client_name}')`; `session.detach_client()`; assert `len(before) == 2`; `len(after) == 0`. | real-tmux integration test | PythonBehaviorParityTest#est_session_py_test_detach_client_no_target_detaches_all_session_clients_b3e4b9869c | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_empty_session_option_returns_None | Verify Session._show_option returns None for unset option; fixtures `session`; execute `session._show_option('default-shell')`; assert `session._show_option('default-shell') is None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_empty_session_option_returns_None_19c68e0e4c | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_has_session | Server.has_session returns True if has session_name exists; fixtures `server`, `session`; execute `server.has_session(TEST_SESSION_NAME)`; `server.has_session(TEST_SESSION_NAME[:-2])`; `server.has_session(TEST_SESSION_NAME[:-2], exact=False)`; `server.has_session('asdf2314324321')`; assert `TEST_SESSION_NAME is not None`; `server.has_session(TEST_SESSION_NAME)`; `not server.has_session(TEST_SESSION_NAME[:-2])`; `server.has_session(TEST_SESSION_NAME[:-2], exact=False)`; `not server.has_session('asdf2314324321')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_has_session_2fd59585c3 | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_kill_session_group | Session.kill(group=True) uses -g on tmux 3.7+; warn-and-ignore below; fixtures `server`; execute `server.new_session(session_name='kill_group_base')`; when `has_gte_version('3.7')`: execute `session.kill(group=True)`; `server.has_session('kill_group_base')`; assert `not server.has_session('kill_group_base')`; when `not (has_gte_version('3.7'))`: execute `session.kill(group=True)`; expect `pytest.warns(UserWarning, match='group requires tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_kill_session_group_70d2bafe79 | 3.3a through 3.7c; conditional boundaries: 3.7 | planned parity | +| tests/test_session.py::test_last_window | Test Session.last_window() selects previous window; fixtures `session`; execute `session.new_window(window_name='last_a', attach=True)`; `session.new_window(window_name='last_b', attach=True)`; `session.refresh()`; `session.last_window()`; assert `session.active_window.window_id == w2.window_id`; `result.window_id == w1.window_id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_last_window_57ed77cbf1 | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_lock_session | Test Session.lock_session() runs without error; fixtures `session`; execute `session.lock_session()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_lock_session_1cc8396c56 | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_new_session | Server.new_session creates new session; fixtures `server`; execute `server.new_session(session_name=new_session_name, detach=True)`; assert `isinstance(new_session, Session)`; `new_session.session_name == new_session_name`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_new_session_e2de3569ae | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_new_window_kill_existing | Test Session.new_window() with kill_existing flag; fixtures `session`; execute `session.new_window(window_name='kill_orig', window_index='5')`; `session.new_window(window_name='kill_replace', window_index='5', kill_existing=True)`; `session.refresh()`; assert `w1.window_name == 'kill_orig'`; `w1.window_index == '5'`; `w2.window_name == 'kill_replace'`; `w2.window_index == '5'`; `'kill_orig' not in names`; `'kill_replace' in names`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_new_window_kill_existing_bd39f40040 | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_new_window_select_existing | Test Session.new_window() with select_existing flag — new name; fixtures `session`; execute `session.new_window(window_name='selexist_new', select_existing=True)`; assert `w.window_name == 'selexist_new'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_new_window_select_existing_d6c7048fc6 | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_new_window_start_directory[empty_string] | Test Session.new_window start_directory parameter handling; case inputs `description='Empty string should not add -c flag'`, `start_directory=''`, `test_id='empty_string'`; fixtures `session`, `monkeypatch`, `tmp_path`, `user_path`; execute `session.new_window(window_name=f'test_window_{test_id}', start_directory=actual_start_directory)`; assert `window.window_name == f'test_window_{test_id}'`; `window in session.windows`; when `start_directory and str(start_directory) not in {'', 'None'} and not (f'{user_path}' in str(start_directory)) and str(start_directory).startswith('./')`: execute `temp_dir.mkdir(parents=True, exist_ok=True)`; `temp_dir.resolve()`; when `expected_path`: execute `active_pane.refresh()`; `pathlib.Path(active_pane.pane_current_path).resolve()`; `pathlib.Path(active_pane.pane_current_path)`; assert `active_pane is not None`; `active_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_new_window_start_directory_empty_string_53afd32113 | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_new_window_start_directory[none_value] | Test Session.new_window start_directory parameter handling; case inputs `description='None should not add -c flag'`, `start_directory=None`, `test_id='none_value'`; fixtures `session`, `monkeypatch`, `tmp_path`, `user_path`; execute `session.new_window(window_name=f'test_window_{test_id}', start_directory=actual_start_directory)`; assert `window.window_name == f'test_window_{test_id}'`; `window in session.windows`; when `start_directory and str(start_directory) not in {'', 'None'} and not (f'{user_path}' in str(start_directory)) and str(start_directory).startswith('./')`: execute `temp_dir.mkdir(parents=True, exist_ok=True)`; `temp_dir.resolve()`; when `expected_path`: execute `active_pane.refresh()`; `pathlib.Path(active_pane.pane_current_path).resolve()`; `pathlib.Path(active_pane.pane_current_path)`; assert `active_pane is not None`; `active_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_new_window_start_directory_none_value_61ebed7357 | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_new_window_start_directory[relative_path] | Test Session.new_window start_directory parameter handling; case inputs `description='Relative path should add -c flag'`, `start_directory='./relative/path'`, `test_id='relative_path'`; fixtures `session`, `monkeypatch`, `tmp_path`, `user_path`; execute `session.new_window(window_name=f'test_window_{test_id}', start_directory=actual_start_directory)`; assert `window.window_name == f'test_window_{test_id}'`; `window in session.windows`; when `start_directory and str(start_directory) not in {'', 'None'} and not (f'{user_path}' in str(start_directory)) and str(start_directory).startswith('./')`: execute `temp_dir.mkdir(parents=True, exist_ok=True)`; `temp_dir.resolve()`; when `expected_path`: execute `active_pane.refresh()`; `pathlib.Path(active_pane.pane_current_path).resolve()`; `pathlib.Path(active_pane.pane_current_path)`; assert `active_pane is not None`; `active_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_new_window_start_directory_relative_path_df9ac332cd | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_new_window_start_directory[user_path] | Test Session.new_window start_directory parameter handling; case inputs `description='User path should add -c flag'`, `start_directory='{user_path}'`, `test_id='user_path'`; fixtures `session`, `monkeypatch`, `tmp_path`, `user_path`; execute `session.new_window(window_name=f'test_window_{test_id}', start_directory=actual_start_directory)`; assert `window.window_name == f'test_window_{test_id}'`; `window in session.windows`; when `start_directory and str(start_directory) not in {'', 'None'} and not (f'{user_path}' in str(start_directory)) and str(start_directory).startswith('./')`: execute `temp_dir.mkdir(parents=True, exist_ok=True)`; `temp_dir.resolve()`; when `expected_path`: execute `active_pane.refresh()`; `pathlib.Path(active_pane.pane_current_path).resolve()`; `pathlib.Path(active_pane.pane_current_path)`; assert `active_pane is not None`; `active_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_new_window_start_directory_user_path_e9b3c36747 | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_new_window_start_directory_pathlib | Test Session.new_window accepts pathlib.Path for start_directory; fixtures `session`, `user_path`; execute `session.new_window(window_name='test_pathlib_start_dir', start_directory=user_path)`; `user_path.resolve()`; `active_pane.refresh()`; `pathlib.Path(active_pane.pane_current_path).resolve()`; `pathlib.Path(active_pane.pane_current_path)`; assert `window.window_name == 'test_pathlib_start_dir'`; `window in session.windows`; `active_pane is not None`; `active_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_new_window_start_directory_pathlib_ebf77c18a8 | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_new_window_with_environment[multiple_env_vars] | Verify new window with environment vars; case inputs `environment={'ENV_VAR_1': 'window_1', 'ENV_VAR_2': 'window_2'}`, `test_id='multiple_env_vars'`; fixtures `session`; execute `session.new_window(attach=True, window_name='window_with_environment', window_shell=f"{env} PS1='$ ' sh", environment=environment)`; `pane.send_keys(f'echo ${k}')`; `pane.capture_pane()`; `shutil.which('env')`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; `pane.capture_pane()[-2] == v`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_new_window_with_environment_multiple_env_vars_cc4d25b3ea | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_new_window_with_environment[single_env_var] | Verify new window with environment vars; case inputs `environment={'ENV_VAR': 'window'}`, `test_id='single_env_var'`; fixtures `session`; execute `session.new_window(attach=True, window_name='window_with_environment', window_shell=f"{env} PS1='$ ' sh", environment=environment)`; `pane.send_keys(f'echo ${k}')`; `pane.capture_pane()`; `shutil.which('env')`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; `pane.capture_pane()[-2] == v`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_new_window_with_environment_single_env_var_2f715b3eb0 | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_next_window | Test Session.next_window() selects next window; fixtures `session`; execute `session.new_window(window_name='next_a', attach=True)`; `session.new_window(window_name='next_b', attach=False)`; `session.refresh()`; `session.next_window()`; assert `session.active_window.window_id == w1.window_id`; `result.window_id != w1.window_id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_next_window_4309c4baa6 | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_periods_raise_bad_session_name[colon_in_name] | Verify session names with periods raise BadSessionName; case inputs `raises=True`, `session_name='hey:its a colon'`, `test_id='colon_in_name'`; fixtures `server`, `session`; when `raises`: execute `session.rename_session(new_name)`; `server.new_session(session_name)`; `server.has_session(session_name)`; `server.switch_client(new_name)`; `server.attach_session(new_name)`; expect `pytest.raises(exc.BadSessionName)`; when `not (raises)`: execute `server.new_session(session_name)`; `server.has_session(session_name)`; `session.rename_session(new_name)`; `server.switch_client(new_name)`; expect `pytest.raises(exc.LibTmuxException)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_periods_raise_bad_session_name_colon_in_name_aca7571512 | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_periods_raise_bad_session_name[period_in_name] | Verify session names with periods raise BadSessionName; case inputs `raises=True`, `session_name='hey.period'`, `test_id='period_in_name'`; fixtures `server`, `session`; when `raises`: execute `session.rename_session(new_name)`; `server.new_session(session_name)`; `server.has_session(session_name)`; `server.switch_client(new_name)`; `server.attach_session(new_name)`; expect `pytest.raises(exc.BadSessionName)`; when `not (raises)`: execute `server.new_session(session_name)`; `server.has_session(session_name)`; `session.rename_session(new_name)`; `server.switch_client(new_name)`; expect `pytest.raises(exc.LibTmuxException)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_periods_raise_bad_session_name_period_in_name_4aca936d92 | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_periods_raise_bad_session_name[valid_name] | Verify session names with periods raise BadSessionName; case inputs `raises=False`, `session_name='hey moo'`, `test_id='valid_name'`; fixtures `server`, `session`; when `raises`: execute `session.rename_session(new_name)`; `server.new_session(session_name)`; `server.has_session(session_name)`; `server.switch_client(new_name)`; `server.attach_session(new_name)`; expect `pytest.raises(exc.BadSessionName)`; when `not (raises)`: execute `server.new_session(session_name)`; `server.has_session(session_name)`; `session.rename_session(new_name)`; `server.switch_client(new_name)`; expect `pytest.raises(exc.LibTmuxException)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_periods_raise_bad_session_name_valid_name_58f1b62bac | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_previous_window | Test Session.previous_window() selects previous window; fixtures `session`; execute `session.new_window(window_name='prev_a', attach=True)`; `session.new_window(window_name='prev_b', attach=True)`; `session.refresh()`; `session.previous_window()`; assert `session.active_window.window_id == w2.window_id`; `result.window_id == w1.window_id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_previous_window_b24c28acd6 | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_remove_environment | Remove environment variable; fixtures `session`; execute `session.getenv('BAM')`; `session.set_environment('BAM', 'OK')`; `session.remove_environment('BAM')`; assert `session.getenv('BAM') is None`; `session.getenv('BAM') == 'OK'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_remove_environment_3dc5ca0644 | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_select_window | Session.select_window moves window; fixtures `session`; execute `session.new_window(window_name='test_window')`; `session.select_window(window_base_index)`; `session.select_window(window_base_index + 1)`; assert `window_idx is not None`; `window_count >= 2`; `len(session.windows) == window_count`; `isinstance(selected_window1, Window)`; `selected_window1.id == active_window1.id`; `isinstance(selected_window2, Window)`; `selected_window2.id == active_window2.id`; `selected_window1.id != selected_window2.id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_select_window_3c2530cb01 | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_select_window_returns_Window | Session.select_window returns Window object; fixtures `session`; execute `session.select_window(window_base_index)`; assert `len(session.windows) == window_count`; `window_idx is not None`; `isinstance(window, Window)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_select_window_returns_Window_cfccc7771f | 3.3a through 3.7c | planned parity | | tests/test_session.py::test_session_attach_does_not_fail_if_session_killed_during_attach[session_killed_during_attach_should_not_raise] | Regression test: Session.attach() should not fail if session is killed; case inputs `raises=False`, `test_id='session_killed_during_attach_should_not_raise'`; fixtures `server`, `monkeypatch`; execute `server.new_session(detach=True)`; `original_cmd(cmd_name, *args, **kwargs)`; `t.cast('type[Exception]', raises)`; `t.cast('RaisesExc', does_not_raise())`; `does_not_raise()`; `test_session.attach()`; when `cmd_name == 'attach-session'`: execute `test_session.kill()`; `MockTmuxCmd()`. | unit test | PythonBehaviorParityTest#ssion_killed_during_attach_session_killed_during_attach_should_not_raise_5a5ea1348c | not tmux-bound | planned parity | -| tests/test_session.py::test_session_context_manager | Test Session context manager functionality; fixtures `server`; execute `server.new_session()`; `session.new_window()`; `server.has_session(session_name)`; assert `len(session.windows) >= 2`; `window in session.windows`; `session_name is not None`; `not server.has_session(session_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_session_context_manager_7305aa9ad8 | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_session_format_field_declared_and_hydrated[session_format] | Tmux's session-scope format tokens hydrate onto the typed ``Session``; case inputs `field_name='session_format'`; fixtures `session`; execute `session.refresh()`; assert `field_name in session.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#ession_py_test_session_format_field_declared_and_hydrated_session_format_6959099e5b | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_session_format_field_declared_and_hydrated[session_group_attached_list] | Tmux's session-scope format tokens hydrate onto the typed ``Session``; case inputs `field_name='session_group_attached_list'`; fixtures `session`; execute `session.refresh()`; assert `field_name in session.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#t_session_format_field_declared_and_hydrated_session_group_attached_list_a8533e10cb | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_session_format_field_declared_and_hydrated[session_group_many_attached] | Tmux's session-scope format tokens hydrate onto the typed ``Session``; case inputs `field_name='session_group_many_attached'`; fixtures `session`; execute `session.refresh()`; assert `field_name in session.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#t_session_format_field_declared_and_hydrated_session_group_many_attached_586ff0ac5a | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_session_format_field_declared_and_hydrated[session_grouped] | Tmux's session-scope format tokens hydrate onto the typed ``Session``; case inputs `field_name='session_grouped'`; fixtures `session`; execute `session.refresh()`; assert `field_name in session.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#ssion_py_test_session_format_field_declared_and_hydrated_session_grouped_dd7ad36766 | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_session_format_field_declared_and_hydrated[session_many_attached] | Tmux's session-scope format tokens hydrate onto the typed ``Session``; case inputs `field_name='session_many_attached'`; fixtures `session`; execute `session.refresh()`; assert `field_name in session.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#py_test_session_format_field_declared_and_hydrated_session_many_attached_221190fa18 | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_session_format_field_declared_and_hydrated[session_marked] | Tmux's session-scope format tokens hydrate onto the typed ``Session``; case inputs `field_name='session_marked'`; fixtures `session`; execute `session.refresh()`; assert `field_name in session.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#ession_py_test_session_format_field_declared_and_hydrated_session_marked_f2b182bbe3 | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_session_last_window_exception_tags_subcommand | Wrapper raises tag LibTmuxException with the originating tmux subcommand; fixtures `session`; execute `session.last_window()`; expect `pytest.raises(exc.LibTmuxException)`; assert `excinfo.value.subcommand == 'last-window'`; `str(excinfo.value).startswith('last-window:')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_session_last_window_exception_tags_subcommand_720092e2a6 | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_session_new_window_with_direction | Verify new window with direction; fixtures `session`; execute `window.refresh()`; `session.new_window(window_name='Example')`; `session.new_window(window_name='Window before', direction=WindowDirection.Before)`; `session.new_window(window_name='Window after', direction=WindowDirection.After)`; `window_initial.refresh()`; `window_after.refresh()`; assert `window_initial.window_index == '2'`; `window_before.window_index == '1'`; `window_initial.window_index == '3'`; `window_after.window_index == '3'`; `window_initial.window_index == '4'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_session_new_window_with_direction_290e43ec8f | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_session_refresh_raises_when_session_id_is_none | ``Session.refresh()`` raises ``ValueError`` when ``session_id`` is unset; fixtures `server`; execute `session.refresh()`; `Session(server=server)`; expect `pytest.raises(ValueError, match='session_id')`; assert `session.session_id is None`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_session_py_test_session_refresh_raises_when_session_id_is_none_02badcb26e | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_session_rename | Session.rename_session renames session; fixtures `session`; execute `session.rename_session(test_name)`; `session.rename_session(TEST_SESSION_NAME)`; assert `session_name is not None`; `session_name == test_name`; `session_name == TEST_SESSION_NAME`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_session_rename_b25190e247 | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_session_scope_override_field_hydrates[active_window_index] | Session-scope overrides admit each token into list-sessions -F; case inputs `field_name='active_window_index'`; fixtures `session`; execute `session.refresh()`; assert `value is not None; message=f'{field_name} should hydrate via list-sessions'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#ession_py_test_session_scope_override_field_hydrates_active_window_index_a5f79a80e3 | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_session_scope_override_field_hydrates[last_window_index] | Session-scope overrides admit each token into list-sessions -F; case inputs `field_name='last_window_index'`; fixtures `session`; execute `session.refresh()`; assert `value is not None; message=f'{field_name} should hydrate via list-sessions'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#_session_py_test_session_scope_override_field_hydrates_last_window_index_aec50e17d6 | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_session_search_panes_filter_by_id | ``Session.search_panes(filter=...)`` returns only matching pane ids; fixtures `session`; execute `window.split()`; `session.search_panes(filter=f'#{{m:{target.pane_id},#{{pane_id}}}}')`; assert `[p.pane_id for p in matches] == [target.pane_id]`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_session_search_panes_filter_by_id_b253910268 | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_session_search_windows_filter | ``Session.search_windows(filter=...)`` filters via tmux's native -f flag; fixtures `session`; execute `session.new_window(window_name='gap7s_keep')`; `session.new_window(window_name='other_drop')`; `session.search_windows(filter='#{m:gap7s_*,#{window_name}}')`; assert `names == ['gap7s_keep']`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_session_search_windows_filter_7b3007135d | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_set_option_ambiguous | Session.set_option raises AmbiguousOption for invalid option; fixtures `session`; execute `session.set_option('default-', 43)`; expect `pytest.raises(exc.AmbiguousOption)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_set_option_ambiguous_06e0278764 | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_set_option_invalid | Session.set_option raises InvalidOption for invalid option; fixtures `session`; execute `session.set_option('afewewfew', 43)`; expect `pytest.raises(exc.InvalidOption)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_set_option_invalid_a60ed811d7 | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_set_show_environment_single | Set environment then Session.show_environment(key); fixtures `session`; execute `session.set_environment('FOO', 'BAR')`; `session.getenv('FOO')`; `session.set_environment('FOO', 'DAR')`; `session.show_environment()`; assert `session.getenv('FOO') == 'BAR'`; `session.getenv('FOO') == 'DAR'`; `session.show_environment()['FOO'] == 'DAR'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_set_show_environment_single_04a3f86366 | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_set_show_option | Set option then Session._show_option(key); fixtures `session`; execute `session.set_option('history-limit', 20)`; `session._show_option('history-limit')`; `session.set_option('history-limit', 40)`; assert `session._show_option('history-limit') == 20`; `session._show_option('history-limit') == 40`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_set_show_option_bd853b248e | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_set_show_options_single | Set option then Session._show_options(key); fixtures `session`; execute `session.set_option('history-limit', 20)`; `session._show_option('history-limit')`; `session.set_option('history-limit', 40)`; `session._show_options()`; assert `session._show_option('history-limit') == 20`; `session._show_option('history-limit') == 40`; `session._show_options()['history-limit'] == 40`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_set_show_options_single_d5eb9d218d | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_show_environment | Session.show_environment() returns dict; fixtures `session`; execute `session.show_environment()`; assert `isinstance(vars_, dict)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_show_environment_9f76bdb7e3 | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_show_environment_not_set | Not set environment variable returns None; fixtures `session`; execute `session.getenv('BAR')`; assert `session.getenv('BAR') is None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_show_environment_not_set_2bcea8ec05 | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_show_option_ambiguous | Session._show_option raises AmbiguousOption for ambiguous option; fixtures `session`; execute `session._show_option('default-')`; expect `pytest.raises(exc.AmbiguousOption)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_show_option_ambiguous_90d740f1ff | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_show_option_unknown | Session.show_option raises InvalidOption for invalid option; fixtures `session`; execute `session.show_option('moooz')`; expect `pytest.raises(exc.InvalidOption)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_show_option_unknown_41d85dc0df | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_show_options | Session._show_options() returns dict; fixtures `session`; execute `session._show_options()`; assert `isinstance(options, dict)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_show_options_dd3d5288e1 | 3.2a through 3.7b | planned parity | -| tests/test_session.py::test_unset_environment | Unset environment variable; fixtures `session`; execute `session.getenv('BAM')`; `session.set_environment('BAM', 'OK')`; `session.unset_environment('BAM')`; assert `session.getenv('BAM') is None`; `session.getenv('BAM') == 'OK'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_unset_environment_5178b103fc | 3.2a through 3.7b | planned parity | -| tests/test_tmuxobject.py::test_filter | Test self.filter() retrieves child object; fixtures `server`; execute `server.new_session('test')`; `window.split()`; `server.windows.filter(window_id='$' + next(namer))`; `session.windows.filter(window_id='@' + next(namer))`; `window.panes.filter(pane_id='%' + next(namer))`; assert `session_id is not None`; `get_session_by_id == session`; `isinstance(get_session_by_id, Session)`; `len(server.windows.filter(window_id='$' + next(namer))) == 0`; `window_id is not None`; `get_window_by_id == window`; `isinstance(get_window_by_id, Window)`; `len(session.windows.filter(window_id='@' + next(namer))) == 0`; `pane_id is not None`; `get_pane_by_id == pane`; `isinstance(get_pane_by_id, Pane)`; `len(window.panes.filter(pane_id='%' + next(namer))) == 0`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_tmuxobject_py_test_filter_3a7e9e1ce9 | 3.2a through 3.7b | planned parity | -| tests/test_tmuxobject.py::test_find_where | Test that find_where() retrieves single matching object; fixtures `server`, `session`; execute `server.sessions.filter(session_id=session_id)`; assert `session_id is not None`; `server.sessions.get(session_id=session_id) == session_`; `isinstance(server.sessions.filter(session_id=session_id)[0], Session)`; `window_id is not None`; `session_.windows.get(window_id=window_id) == window`; `isinstance(session_.windows.get(window_id=window_id), Window)`; `pane_id is not None`; `window.panes.get(pane_id=pane_id) == pane`; `isinstance(window.panes.get(pane_id=pane_id), Pane)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_tmuxobject_py_test_find_where_e854af52ee | 3.2a through 3.7b | planned parity | -| tests/test_tmuxobject.py::test_find_where_None | .find_where returns None if no results found; fixtures `server`, `session`; execute `server.sessions.filter(session_name=nonexistent_session)`; assert `len(server.sessions.filter(session_name=nonexistent_session)) == 0`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_tmuxobject_py_test_find_where_None_c347302743 | 3.2a through 3.7b | planned parity | -| tests/test_tmuxobject.py::test_find_where_multiple_infos | .find_where returns objects with multiple attributes; fixtures `server`, `session`; assert `session_id is not None`; `session_name is not None`; `find_where == session_`; `isinstance(find_where, Session)`; `window_id is not None`; `window_index is not None`; `find_window_where == window`; `isinstance(find_window_where, Window)`; `pane_id is not None`; `pane_tty is not None`; `find_pane_where == pane`; `isinstance(find_pane_where, Pane)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_tmuxobject_py_test_find_where_multiple_infos_3f54521fc2 | 3.2a through 3.7b | planned parity | -| tests/test_tmuxobject.py::test_where | Test self.where() returns matching objects; fixtures `server`, `session`; execute `window.split()`; `server.sessions.filter(session_id=session_id, session_name=session_name)`; `window.panes.filter(pane_id=pane_id, pane_tty=pane_tty)`; `session_.windows.filter(window_id=window_id, window_index=window_index)`; assert `session_id is not None`; `session_name is not None`; `len(server_sessions) == 1`; `isinstance(server_sessions, list)`; `server_sessions[0] == session_`; `isinstance(server_sessions[0], Session)`; `window_id is not None`; `window_index is not None`; `len(session_windows) == 1`; `isinstance(session_windows, list)`; `session_windows[0] == window`; `isinstance(session_windows[0], Window)`; `pane_id is not None`; `pane_tty is not None`; `len(window_panes) == 1`; `isinstance(window_panes, list)`; `window_panes[0] == pane`; `isinstance(window_panes[0], Pane)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_tmuxobject_py_test_where_e9a9019393 | 3.2a through 3.7b | planned parity | +| tests/test_session.py::test_session_context_manager | Test Session context manager functionality; fixtures `server`; execute `server.new_session()`; `session.new_window()`; `server.has_session(session_name)`; assert `len(session.windows) >= 2`; `window in session.windows`; `session_name is not None`; `not server.has_session(session_name)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_session_context_manager_7305aa9ad8 | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_session_format_field_declared_and_hydrated[session_format] | Tmux's session-scope format tokens hydrate onto the typed ``Session``; case inputs `field_name='session_format'`; fixtures `session`; execute `session.refresh()`; assert `field_name in session.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#ession_py_test_session_format_field_declared_and_hydrated_session_format_6959099e5b | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_session_format_field_declared_and_hydrated[session_group_attached_list] | Tmux's session-scope format tokens hydrate onto the typed ``Session``; case inputs `field_name='session_group_attached_list'`; fixtures `session`; execute `session.refresh()`; assert `field_name in session.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#t_session_format_field_declared_and_hydrated_session_group_attached_list_a8533e10cb | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_session_format_field_declared_and_hydrated[session_group_many_attached] | Tmux's session-scope format tokens hydrate onto the typed ``Session``; case inputs `field_name='session_group_many_attached'`; fixtures `session`; execute `session.refresh()`; assert `field_name in session.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#t_session_format_field_declared_and_hydrated_session_group_many_attached_586ff0ac5a | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_session_format_field_declared_and_hydrated[session_grouped] | Tmux's session-scope format tokens hydrate onto the typed ``Session``; case inputs `field_name='session_grouped'`; fixtures `session`; execute `session.refresh()`; assert `field_name in session.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#ssion_py_test_session_format_field_declared_and_hydrated_session_grouped_dd7ad36766 | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_session_format_field_declared_and_hydrated[session_many_attached] | Tmux's session-scope format tokens hydrate onto the typed ``Session``; case inputs `field_name='session_many_attached'`; fixtures `session`; execute `session.refresh()`; assert `field_name in session.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#py_test_session_format_field_declared_and_hydrated_session_many_attached_221190fa18 | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_session_format_field_declared_and_hydrated[session_marked] | Tmux's session-scope format tokens hydrate onto the typed ``Session``; case inputs `field_name='session_marked'`; fixtures `session`; execute `session.refresh()`; assert `field_name in session.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#ession_py_test_session_format_field_declared_and_hydrated_session_marked_f2b182bbe3 | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_session_last_window_exception_tags_subcommand | Wrapper raises tag LibTmuxException with the originating tmux subcommand; fixtures `session`; execute `session.last_window()`; expect `pytest.raises(exc.LibTmuxException)`; assert `excinfo.value.subcommand == 'last-window'`; `str(excinfo.value).startswith('last-window:')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_session_last_window_exception_tags_subcommand_720092e2a6 | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_session_new_window_with_direction | Verify new window with direction; fixtures `session`; execute `window.refresh()`; `session.new_window(window_name='Example')`; `session.new_window(window_name='Window before', direction=WindowDirection.Before)`; `session.new_window(window_name='Window after', direction=WindowDirection.After)`; `window_initial.refresh()`; `window_after.refresh()`; assert `window_initial.window_index == '2'`; `window_before.window_index == '1'`; `window_initial.window_index == '3'`; `window_after.window_index == '3'`; `window_initial.window_index == '4'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_session_new_window_with_direction_290e43ec8f | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_session_refresh_raises_when_session_id_is_none | ``Session.refresh()`` raises ``ValueError`` when ``session_id`` is unset; fixtures `server`; execute `session.refresh()`; `Session(server=server)`; expect `pytest.raises(ValueError, match='session_id')`; assert `session.session_id is None`. | real-tmux integration test | PythonBehaviorParityTest#ests_test_session_py_test_session_refresh_raises_when_session_id_is_none_02badcb26e | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_session_rename | Session.rename_session renames session; fixtures `session`; execute `session.rename_session(test_name)`; `session.rename_session(TEST_SESSION_NAME)`; assert `session_name is not None`; `session_name == test_name`; `session_name == TEST_SESSION_NAME`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_session_rename_b25190e247 | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_session_scope_override_field_hydrates[active_window_index] | Session-scope overrides admit each token into list-sessions -F; case inputs `field_name='active_window_index'`; fixtures `session`; execute `session.refresh()`; assert `value is not None; message=f'{field_name} should hydrate via list-sessions'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#ession_py_test_session_scope_override_field_hydrates_active_window_index_a5f79a80e3 | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_session_scope_override_field_hydrates[last_window_index] | Session-scope overrides admit each token into list-sessions -F; case inputs `field_name='last_window_index'`; fixtures `session`; execute `session.refresh()`; assert `value is not None; message=f'{field_name} should hydrate via list-sessions'`; `isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#_session_py_test_session_scope_override_field_hydrates_last_window_index_aec50e17d6 | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_session_search_panes_filter_by_id | ``Session.search_panes(filter=...)`` returns only matching pane ids; fixtures `session`; execute `window.split()`; `session.search_panes(filter=f'#{{m:{target.pane_id},#{{pane_id}}}}')`; assert `[p.pane_id for p in matches] == [target.pane_id]`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_session_search_panes_filter_by_id_b253910268 | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_session_search_windows_filter | ``Session.search_windows(filter=...)`` filters via tmux's native -f flag; fixtures `session`; execute `session.new_window(window_name='gap7s_keep')`; `session.new_window(window_name='other_drop')`; `session.search_windows(filter='#{m:gap7s_*,#{window_name}}')`; assert `names == ['gap7s_keep']`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_session_search_windows_filter_7b3007135d | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_set_option_ambiguous | Session.set_option raises AmbiguousOption for invalid option; fixtures `session`; execute `session.set_option('default-', 43)`; expect `pytest.raises(exc.AmbiguousOption)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_set_option_ambiguous_06e0278764 | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_set_option_invalid | Session.set_option raises InvalidOption for invalid option; fixtures `session`; execute `session.set_option('afewewfew', 43)`; expect `pytest.raises(exc.InvalidOption)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_set_option_invalid_a60ed811d7 | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_set_show_environment_single | Set environment then Session.show_environment(key); fixtures `session`; execute `session.set_environment('FOO', 'BAR')`; `session.getenv('FOO')`; `session.set_environment('FOO', 'DAR')`; `session.show_environment()`; assert `session.getenv('FOO') == 'BAR'`; `session.getenv('FOO') == 'DAR'`; `session.show_environment()['FOO'] == 'DAR'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_set_show_environment_single_04a3f86366 | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_set_show_option | Set option then Session._show_option(key); fixtures `session`; execute `session.set_option('history-limit', 20)`; `session._show_option('history-limit')`; `session.set_option('history-limit', 40)`; assert `session._show_option('history-limit') == 20`; `session._show_option('history-limit') == 40`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_set_show_option_bd853b248e | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_set_show_options_single | Set option then Session._show_options(key); fixtures `session`; execute `session.set_option('history-limit', 20)`; `session._show_option('history-limit')`; `session.set_option('history-limit', 40)`; `session._show_options()`; assert `session._show_option('history-limit') == 20`; `session._show_option('history-limit') == 40`; `session._show_options()['history-limit'] == 40`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_set_show_options_single_d5eb9d218d | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_show_environment | Session.show_environment() returns dict; fixtures `session`; execute `session.show_environment()`; assert `isinstance(vars_, dict)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_show_environment_9f76bdb7e3 | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_show_environment_not_set | Not set environment variable returns None; fixtures `session`; execute `session.getenv('BAR')`; assert `session.getenv('BAR') is None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_show_environment_not_set_2bcea8ec05 | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_show_option_ambiguous | Session._show_option raises AmbiguousOption for ambiguous option; fixtures `session`; execute `session._show_option('default-')`; expect `pytest.raises(exc.AmbiguousOption)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_show_option_ambiguous_90d740f1ff | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_show_option_unknown | Session.show_option raises InvalidOption for invalid option; fixtures `session`; execute `session.show_option('moooz')`; expect `pytest.raises(exc.InvalidOption)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_show_option_unknown_41d85dc0df | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_show_options | Session._show_options() returns dict; fixtures `session`; execute `session._show_options()`; assert `isinstance(options, dict)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_show_options_dd3d5288e1 | 3.3a through 3.7c | planned parity | +| tests/test_session.py::test_unset_environment | Unset environment variable; fixtures `session`; execute `session.getenv('BAM')`; `session.set_environment('BAM', 'OK')`; `session.unset_environment('BAM')`; assert `session.getenv('BAM') is None`; `session.getenv('BAM') == 'OK'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_session_py_test_unset_environment_5178b103fc | 3.3a through 3.7c | planned parity | +| tests/test_tmuxobject.py::test_filter | Test self.filter() retrieves child object; fixtures `server`; execute `server.new_session('test')`; `window.split()`; `server.windows.filter(window_id='$' + next(namer))`; `session.windows.filter(window_id='@' + next(namer))`; `window.panes.filter(pane_id='%' + next(namer))`; assert `session_id is not None`; `get_session_by_id == session`; `isinstance(get_session_by_id, Session)`; `len(server.windows.filter(window_id='$' + next(namer))) == 0`; `window_id is not None`; `get_window_by_id == window`; `isinstance(get_window_by_id, Window)`; `len(session.windows.filter(window_id='@' + next(namer))) == 0`; `pane_id is not None`; `get_pane_by_id == pane`; `isinstance(get_pane_by_id, Pane)`; `len(window.panes.filter(pane_id='%' + next(namer))) == 0`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_tmuxobject_py_test_filter_3a7e9e1ce9 | 3.3a through 3.7c | planned parity | +| tests/test_tmuxobject.py::test_find_where | Test that find_where() retrieves single matching object; fixtures `server`, `session`; execute `server.sessions.filter(session_id=session_id)`; assert `session_id is not None`; `server.sessions.get(session_id=session_id) == session_`; `isinstance(server.sessions.filter(session_id=session_id)[0], Session)`; `window_id is not None`; `session_.windows.get(window_id=window_id) == window`; `isinstance(session_.windows.get(window_id=window_id), Window)`; `pane_id is not None`; `window.panes.get(pane_id=pane_id) == pane`; `isinstance(window.panes.get(pane_id=pane_id), Pane)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_tmuxobject_py_test_find_where_e854af52ee | 3.3a through 3.7c | planned parity | +| tests/test_tmuxobject.py::test_find_where_None | .find_where returns None if no results found; fixtures `server`, `session`; execute `server.sessions.filter(session_name=nonexistent_session)`; assert `len(server.sessions.filter(session_name=nonexistent_session)) == 0`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_tmuxobject_py_test_find_where_None_c347302743 | 3.3a through 3.7c | planned parity | +| tests/test_tmuxobject.py::test_find_where_multiple_infos | .find_where returns objects with multiple attributes; fixtures `server`, `session`; assert `session_id is not None`; `session_name is not None`; `find_where == session_`; `isinstance(find_where, Session)`; `window_id is not None`; `window_index is not None`; `find_window_where == window`; `isinstance(find_window_where, Window)`; `pane_id is not None`; `pane_tty is not None`; `find_pane_where == pane`; `isinstance(find_pane_where, Pane)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_tmuxobject_py_test_find_where_multiple_infos_3f54521fc2 | 3.3a through 3.7c | planned parity | +| tests/test_tmuxobject.py::test_where | Test self.where() returns matching objects; fixtures `server`, `session`; execute `window.split()`; `server.sessions.filter(session_id=session_id, session_name=session_name)`; `window.panes.filter(pane_id=pane_id, pane_tty=pane_tty)`; `session_.windows.filter(window_id=window_id, window_index=window_index)`; assert `session_id is not None`; `session_name is not None`; `len(server_sessions) == 1`; `isinstance(server_sessions, list)`; `server_sessions[0] == session_`; `isinstance(server_sessions[0], Session)`; `window_id is not None`; `window_index is not None`; `len(session_windows) == 1`; `isinstance(session_windows, list)`; `session_windows[0] == window`; `isinstance(session_windows[0], Window)`; `pane_id is not None`; `pane_tty is not None`; `len(window_panes) == 1`; `isinstance(window_panes, list)`; `window_panes[0] == pane`; `isinstance(window_panes[0], Pane)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_tmuxobject_py_test_where_e9a9019393 | 3.3a through 3.7c | planned parity | | tests/test_version.py::test_version[beta_version] | Assert LooseVersion constructor against various version strings; case inputs `test_id='beta_version'`, `version='1.0.0b'`; execute `LooseVersion(version)`; assert `LooseVersion(version)`. | unit test | PythonBehaviorParityTest#tests_test_version_py_test_version_beta_version_f692c9af3d | not tmux-bound | planned parity | | tests/test_version.py::test_version[beta_with_number] | Assert LooseVersion constructor against various version strings; case inputs `test_id='beta_with_number'`, `version='1.0.0b1'`; execute `LooseVersion(version)`; assert `LooseVersion(version)`. | unit test | PythonBehaviorParityTest#tests_test_version_py_test_version_beta_with_number_3c86e150d1 | not tmux-bound | planned parity | | tests/test_version.py::test_version[beta_with_os] | Assert LooseVersion constructor against various version strings; case inputs `test_id='beta_with_os'`, `version='1.0.0b-openbsd'`; execute `LooseVersion(version)`; assert `LooseVersion(version)`. | unit test | PythonBehaviorParityTest#tests_test_version_py_test_version_beta_with_os_d06ed0daf6 | not tmux-bound | planned parity | @@ -1421,84 +1421,84 @@ under [`tests/`](https://github.com/tmux-python/libtmux/blob/c4a980b/tests) and | tests/test_version.py::test_version_compare[less_than_equal_raises] | Assert version comparisons; case inputs `a='1'`, `b='1'`, `op=<built-in function lt>`, `raises=<class 'AssertionError'>`, `test_id='less_than_equal_raises'`; execute `op(LooseVersion(a), LooseVersion(b))`; `t.cast('type[Exception]', raises)`; `t.cast('RaisesExc', does_not_raise())`; `does_not_raise()`; `LooseVersion(a)`; `LooseVersion(b)`; assert `op(LooseVersion(a), LooseVersion(b))`. | unit test | PythonBehaviorParityTest#tests_test_version_py_test_version_compare_less_than_equal_raises_79bfce1a72 | not tmux-bound | planned parity | | tests/test_version.py::test_version_compare[less_than_openbsd] | Assert version comparisons; case inputs `a='1'`, `b='1.0.0-openbsd'`, `op=<built-in function lt>`, `raises=False`, `test_id='less_than_openbsd'`; execute `op(LooseVersion(a), LooseVersion(b))`; `t.cast('type[Exception]', raises)`; `t.cast('RaisesExc', does_not_raise())`; `does_not_raise()`; `LooseVersion(a)`; `LooseVersion(b)`; assert `op(LooseVersion(a), LooseVersion(b))`. | unit test | PythonBehaviorParityTest#tests_test_version_py_test_version_compare_less_than_openbsd_3e1b42f2d1 | not tmux-bound | planned parity | | tests/test_version.py::test_version_compare[less_than_patch] | Assert version comparisons; case inputs `a='1'`, `b='1.0.0p1'`, `op=<built-in function lt>`, `raises=False`, `test_id='less_than_patch'`; execute `op(LooseVersion(a), LooseVersion(b))`; `t.cast('type[Exception]', raises)`; `t.cast('RaisesExc', does_not_raise())`; `does_not_raise()`; `LooseVersion(a)`; `LooseVersion(b)`; assert `op(LooseVersion(a), LooseVersion(b))`. | unit test | PythonBehaviorParityTest#tests_test_version_py_test_version_compare_less_than_patch_3976fa2757 | not tmux-bound | planned parity | -| tests/test_window.py::test_active_pane | Window.active_window returns active Pane; fixtures `session`; assert `isinstance(window.active_pane, Pane)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_active_pane_2a044ab40a | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_deprecated_window_methods_emit_warning[set_window_option] | Verify deprecated Window methods emit DeprecationWarning (0.50.0); case inputs `test_case=DeprecatedMethodTestCase(test_id='set_window_option', method_name='set_window_option', args=('main-pane-height', 20), kwargs={}, expected_error_match='Window\\.set_window_option\\(\\) is deprecated')`; fixtures `session`; execute `session.new_window(window_name='test_deprecation')`; `method(*test_case.args, **test_case.kwargs)`; expect `pytest.warns(DeprecationWarning, match=test_case.expected_error_match)`. | real-tmux integration test | PythonBehaviorParityTest#_window_py_test_deprecated_window_methods_emit_warning_set_window_option_a7a3be4c0b | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_deprecated_window_methods_emit_warning[show_window_option] | Verify deprecated Window methods emit DeprecationWarning (0.50.0); case inputs `test_case=DeprecatedMethodTestCase(test_id='show_window_option', method_name='show_window_option', args=('main-pane-height',), kwargs={}, expected_error_match='Window\\.show_window_option\\(\\) is deprecated')`; fixtures `session`; execute `session.new_window(window_name='test_deprecation')`; `method(*test_case.args, **test_case.kwargs)`; expect `pytest.warns(DeprecationWarning, match=test_case.expected_error_match)`. | real-tmux integration test | PythonBehaviorParityTest#window_py_test_deprecated_window_methods_emit_warning_show_window_option_6e07ddb7b6 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_deprecated_window_methods_emit_warning[show_window_options] | Verify deprecated Window methods emit DeprecationWarning (0.50.0); case inputs `test_case=DeprecatedMethodTestCase(test_id='show_window_options', method_name='show_window_options', args=(), kwargs={}, expected_error_match='Window\\.show_window_options\\(\\) is deprecated')`; fixtures `session`; execute `session.new_window(window_name='test_deprecation')`; `method(*test_case.args, **test_case.kwargs)`; expect `pytest.warns(DeprecationWarning, match=test_case.expected_error_match)`. | real-tmux integration test | PythonBehaviorParityTest#indow_py_test_deprecated_window_methods_emit_warning_show_window_options_bb60c6c4a9 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_empty_window_name | New windows can be created with empty string for window name; fixtures `session`; execute `session.set_option('automatic-rename', 'off')`; `session.new_window(window_name="''", attach=True)`; `session.cmd('list-windows', '-F', '#{window_name}', '-f', '#{==:#{session_name},' + session.session_name + '}')`; assert `window == session.active_window`; `window.window_name == "''"`; `session.session_name is not None`; `"''" in cmd.stdout`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_empty_window_name_fe50634976 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_empty_window_option_returns_None | Verify unset window option returns None; fixtures `session`; execute `session.new_window(window_name='test_window')`; `window.show_option('alternate-screen')`; assert `window.show_option('alternate-screen') is None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_empty_window_option_returns_None_72aa5bb02d | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_fresh_window_data | Verify window data is fresh; fixtures `session`; execute `window.split()`; `session.new_window(window_name='second')`; `session.new_window(window_name='hey')`; `session.select_window('1')`; `active_window._show_option('pane-base-index', global_=True)`; `pane_to_select.select()`; assert `active_window is not None`; `pane_base_idx is not None`; `len(session.windows) == 1`; `len(session.active_window.panes) == 1`; `session.session_id != '@0'`; `current_windows == 1`; `isinstance(session.server, Server)`; `isinstance(w, Window)`; `isinstance(window, Window)`; `pane_to_select is not None`; `active_pane is not None`; `pane_to_select_2 is not None`; `current_windows == len(session.windows)`; `window_to_kill is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_fresh_window_data_c86c1a0f96 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_kill_window | Test window.kill() kills window; fixtures `session`; execute `session.new_window()`; `w.kill()`; `w.refresh()`; expect `pytest.raises(ObjectDoesNotExist)`; assert `w.window_id is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_kill_window_d8eb2eff37 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_last_pane | Test Window.last_pane() selects the previously active pane; fixtures `session`; execute `session.new_window(window_name='test_last_pane')`; `window.last_pane()`; `pane1.split()`; `pane2.select()`; `pane1.select()`; `pane2.refresh()`; assert `pane1 is not None`; `result is not None`; `pane2.pane_active == '1'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_last_pane_d5eb4916f7 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_link_unlink_window | Test Window.link() and Window.unlink(); fixtures `server`, `session`; execute `server.new_session(session_name='link_target')`; `session.new_window(window_name='link_test')`; `w.link(s2, detach=True)`; `s2.refresh()`; `linked_windows[0].unlink()`; assert `'link_test' in s2_window_names`; `len(linked_windows) > 0`; `'link_test' not in s2_window_names`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_link_unlink_window_32d7f06beb | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_move_window | Window.move_window results in changed index; fixtures `session`; execute `session.new_window(window_name='test_window')`; `window.move_window(new_index)`; assert `window.window_index is not None`; `window.window_index == new_index`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_move_window_7001ddfe96 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_move_window_kill_target | Test Window.move_window() with kill_target flag; fixtures `session`; execute `session.new_window(window_name='move_w1')`; `session.new_window(window_name='move_w2')`; `session.refresh()`; `extra_windows[0].move_window(destination=w2_index, kill_target=True)`; assert `w2.window_index is not None`; `len(extra_windows) == 1`; `len(session.windows) == initial_count - 1`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_move_window_kill_target_b7e089fe88 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_move_window_no_select | Test Window.move_window() with no_select flag; fixtures `session`; execute `session.new_window(window_name='nosel_w1', attach=True)`; `session.new_window(window_name='nosel_w2', attach=False)`; `session.refresh()`; `w2.move_window(destination='99', no_select=True)`; assert `session.active_window.window_id == w1.window_id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_move_window_no_select_a6ab22d3d4 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_move_window_relative_returns_fresh_window[after-0] | Window.move_window() returns fresh state for relative moves; case inputs `destination_offset=0`, `flag_name='after'`; fixtures `session`; execute `session.new_window(window_name='move_middle')`; `session.new_window(window_name='move_relative')`; `Window.from_window_id(server=session.server, window_id=moving_window.window_id)`; assert `destination_window.window_index is not None`; `moving_window.window_id is not None`; `moving_window.window_index == fresh_window.window_index`; `moving_window.session_id == fresh_window.session_id`; when `flag_name == 'after'`: execute `moving_window.move_window(destination, after=True)`; when `not (flag_name == 'after')`: execute `moving_window.move_window(destination, before=True)`. | real-tmux integration test | PythonBehaviorParityTest#ts_test_window_py_test_move_window_relative_returns_fresh_window_after_0_de00174c42 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_move_window_relative_returns_fresh_window[before-1] | Window.move_window() returns fresh state for relative moves; case inputs `destination_offset=1`, `flag_name='before'`; fixtures `session`; execute `session.new_window(window_name='move_middle')`; `session.new_window(window_name='move_relative')`; `Window.from_window_id(server=session.server, window_id=moving_window.window_id)`; assert `destination_window.window_index is not None`; `moving_window.window_id is not None`; `moving_window.window_index == fresh_window.window_index`; `moving_window.session_id == fresh_window.session_id`; when `flag_name == 'after'`: execute `moving_window.move_window(destination, after=True)`; when `not (flag_name == 'after')`: execute `moving_window.move_window(destination, before=True)`. | real-tmux integration test | PythonBehaviorParityTest#s_test_window_py_test_move_window_relative_returns_fresh_window_before_1_151f2f516d | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_move_window_renumber | Test Window.move_window() with renumber flag; fixtures `session`; execute `session.new_window(window_name='ren_w1')`; `session.new_window(window_name='ren_w2')`; `session.new_window(window_name='ren_w3')`; `session.refresh()`; `w2.kill()`; `w3.move_window(renumber=True)`; assert `indices[i + 1] - indices[i] == 1`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_move_window_renumber_d5796a5b32 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_move_window_to_other_session | Window.move_window to other session; fixtures `server`, `session`; execute `session.new_window(window_name='test_window')`; `server.new_session('test_move_window')`; `window.move_window(session=new_session.session_id)`; assert `window_id is not None`; `new_session.windows.get(window_id=window_id) == window`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_move_window_to_other_session_92d3c24001 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_move_window_to_other_session_with_destination | Window.move_window() returns fresh state for cross-session moves; fixtures `server`, `session`; execute `session.new_window(window_name='move_cross_session')`; `server.new_session('test_move_window_destination')`; `window.move_window(destination=destination, session=new_session.session_id)`; `Window.from_window_id(server=server, window_id=window.window_id)`; assert `window.window_id is not None`; `fresh_window.session_id == new_session.session_id`; `fresh_window.window_index == destination`; `window.session_id == fresh_window.session_id`; `window.window_index == fresh_window.window_index`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_move_window_to_other_session_with_destination_e7f86afe1d | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_new_pane | Window.new_pane() creates a floating pane on tmux 3.7+ (else raises); fixtures `session`; execute `session.new_window(window_name='win_floating')`; `window.resize(height=50, width=200)`; when `has_gte_version('3.7')`: execute `window.new_pane(width=60, height=12, shell='sleep 30')`; assert `floating.pane_floating_flag == '1'`; `floating in window.panes`; when `not (has_gte_version('3.7'))`: execute `window.new_pane(width=40, height=10)`; expect `pytest.raises(exc.LibTmuxException, match='new_pane .*requires tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_new_pane_3580a3317d | 3.2a through 3.7b; conditional boundaries: 3.7 | planned parity | -| tests/test_window.py::test_new_window_name_colon_period_accepted[colon] | Window names with ':' and '.' are accepted verbatim; case inputs `test_id='colon'`, `window_name='project:frontend'`; fixtures `session`; execute `session.new_window(window_name=window_name)`; `window.kill()`; assert `window.window_name == window_name`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_new_window_name_colon_period_accepted_colon_007435d489 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_new_window_name_colon_period_accepted[period] | Window names with ':' and '.' are accepted verbatim; case inputs `test_id='period'`, `window_name='app-v1.0'`; fixtures `session`; execute `session.new_window(window_name=window_name)`; `window.kill()`; assert `window.window_name == window_name`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_new_window_name_colon_period_accepted_period_6ef9d54ed8 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_new_window_with_direction | Verify new window with direction; fixtures `session`; execute `window.refresh()`; `session.new_window(window_name='Example')`; `window_initial.new_window(window_name='Window before', direction=WindowDirection.Before)`; `window_initial.refresh()`; `window_initial.new_window(window_name='Window after', direction=WindowDirection.After)`; `window_after.refresh()`; assert `window_initial.window_index == '2'`; `window_before.window_index == '2'`; `window_initial.window_index == '3'`; `window_after.window_index == '4'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_new_window_with_direction_c2a47c1ee5 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_newest_pane_data | Test window.panes has fresh data; fixtures `session`; execute `session.new_window(window_name='test', attach=True)`; `window.split(attach=True)`; assert `isinstance(window, Window)`; `len(window.panes) == 1`; `len(window.panes) == 2`; `len(window.panes) == 3`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_newest_pane_data_79e2ce3983 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_next_layout | Test Window.next_layout() cycles to the next layout; fixtures `session`; execute `session.new_window(window_name='test_next_layout')`; `window.resize(height=40, width=80)`; `pane.split()`; `window.select_layout('even-horizontal')`; `window.refresh()`; `window.next_layout()`; assert `pane is not None`; `layout_before != layout_after`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_next_layout_b7b4774637 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_previous_layout | Test Window.previous_layout() cycles back; fixtures `session`; execute `session.new_window(window_name='test_prev_layout')`; `window.resize(height=40, width=80)`; `pane.split()`; `window.select_layout('even-horizontal')`; `window.refresh()`; `window.next_layout()`; assert `pane is not None`; `layout_before == layout_after`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_previous_layout_dde68474fa | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_resize | Verify resizing window; fixtures `session`; execute `session.cmd('detach-client', '-s')`; `window.resize(height=10)`; `window.resize(width=10)`; `window.resize(adjustment_direction=ResizeAdjustmentDirection.Down, adjustment=window_height_adjustment * 2)`; `window.resize(adjustment_direction=ResizeAdjustmentDirection.Up, adjustment=window_height_adjustment)`; `window.resize(height=50)`; assert `window.window_height is not None`; `window.window_width is not None`; `int(window.window_height) == 10`; `int(window.window_width) == 10`; `window_height_before + window_height_adjustment * 2 == int(window.window_height)`; `window_height_before - window_height_adjustment == int(window.window_height)`; `window_height_before > window_height_shrunk`; `window`; `window_height_before < window_height_expanded`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_resize_c4965fd743 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_respawn_window | Test Window.respawn() with kill flag; fixtures `session`; execute `session.new_window(window_name='test_respawn_w')`; `window.respawn(kill=True, shell='sh')`; `window.refresh()`; `session.refresh()`; assert `window.window_id in [w.window_id for w in session.windows]`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_respawn_window_b5eafa65d6 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_rotate_window | Test Window.rotate() rotates pane positions; fixtures `session`; execute `session.new_window(window_name='test_rotate')`; `window.resize(height=40, width=80)`; `window.rotate()`; `pane1.split()`; `pane2.split()`; `pane1.refresh()`; assert `pane1 is not None`; `idx_before != idx_after`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_rotate_window_6eca5ac10b | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_select_layout_accepts_no_arg | Tmux allows select-layout with no arguments, so let's allow it here; fixtures `server`, `session`; execute `session.new_window(window_name='test_window')`; `window.select_layout()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_select_layout_accepts_no_arg_0af9999b14 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_select_layout_mutual_exclusion | Test that layout string and flags are mutually exclusive; fixtures `session`; execute `session.new_window(window_name='test_layout_mutex')`; `window.select_layout('tiled', spread=True)`; expect `pytest.raises(ValueError, match='Cannot specify both')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_select_layout_mutual_exclusion_884674c936 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_select_layout_next_previous | Test Window.select_layout() with next/previous flags; fixtures `session`; execute `session.new_window(window_name='test_layout_cycle')`; `window.resize(height=40, width=80)`; `pane.split()`; `window.select_layout('even-horizontal')`; `window.refresh()`; `window.select_layout(next_layout=True)`; assert `pane is not None`; `layout_before != layout_after_next`; `layout_after_prev == layout_before`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_select_layout_next_previous_a1947ac64f | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_select_layout_spread | Test Window.select_layout() with spread flag; fixtures `session`; execute `session.new_window(window_name='test_layout_spread')`; `window.resize(height=40, width=80)`; `pane.split()`; `window.select_layout(spread=True)`; assert `pane is not None`; `len(window.panes) == 3`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_select_layout_spread_df22951796 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_select_window | Test Window.select_window(); fixtures `session`; execute `session.new_window(window_name='testing 3')`; `session.select_window(str(window_base_index))`; `session.select_window('testing 3')`; assert `window_count == 1`; `session.active_window.window_index is not None`; `window.window_index is not None`; `int(window_base_index) + 1 == int(window.window_index)`; `window_base_index == int(session.active_window.window_index)`; `int(window_base_index) + 1 == int(session.active_window.window_index)`; `len(session.windows) == 2`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_select_window_7403165fda | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_set_and_show_window_options | Window.set_option() then Window._show_options(key); fixtures `session`; execute `session.new_window(window_name='test_window')`; `window.set_option('main-pane-height', 20)`; `window._show_option('main-pane-height')`; `window.set_option('main-pane-height', 40)`; `window._show_options(scope=OptionScope.Session)`; `window.set_option('pane-border-format', ' #P ')`; expect `pytest.raises(KeyError)`; assert `window._show_option('main-pane-height') == 20`; `window._show_option('main-pane-height') == 40`; `window._show_options(scope=OptionScope.Session)['main-pane-height'] == 40`; `window._show_option('pane-border-format') == ' #P '`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_set_and_show_window_options_227ba527da | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_set_window_and_show_window_options | Window.set_option() then Window.show_option(key); fixtures `session`; execute `session.new_window(window_name='test_window')`; `window.set_option('main-pane-height', 20)`; `window.show_option('main-pane-height')`; `window.set_option('main-pane-height', 40)`; `window.show_options()`; `window.set_option('pane-border-format', ' #P ')`; assert `window.show_option('main-pane-height') == 20`; `window.show_option('main-pane-height') == 40`; `window.show_options()['main-pane-height'] == 40`; `window.show_option('pane-border-format') == ' #P '`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_set_window_and_show_window_options_d96249517a | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_set_window_option_ambiguous | set_option raises AmbiguousOption for ambiguous option; fixtures `session`; execute `session.new_window(window_name='test_window')`; `window.set_option('clock-mode', 12)`; expect `pytest.raises(exc.AmbiguousOption)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_set_window_option_ambiguous_1c63d8c847 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_set_window_option_invalid | Window.set_option raises InvalidOption for invalid option key; fixtures `session`; execute `session.new_window(window_name='test_window')`; `window.set_option('afewewfew', 43)`; expect `pytest.raises(exc.InvalidOption)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_set_window_option_invalid_dc1098935a | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_show_window_option | Set option then Window.show_option(key); fixtures `session`; execute `session.new_window(window_name='test_window')`; `window.set_option('main-pane-height', 20)`; `window.show_option('main-pane-height')`; `window.set_option('main-pane-height', 40)`; assert `window.show_option('main-pane-height') == 20`; `window.show_option('main-pane-height') == 40`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_show_window_option_81f670d32c | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_show_window_option_ambiguous | show_option raises AmbiguousOption for ambiguous option; fixtures `session`; execute `session.new_window(window_name='test_window')`; `window.show_option('clock-mode')`; expect `pytest.raises(exc.AmbiguousOption)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_show_window_option_ambiguous_7e784bd71f | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_show_window_option_unknown | Window.show_option raises InvalidOption for bad option key; fixtures `session`; execute `session.new_window(window_name='test_window')`; `window.show_option('moooz')`; expect `pytest.raises(exc.InvalidOption)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_show_window_option_unknown_b9f31bc4f9 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_show_window_options | Window.show_options() returns dict; fixtures `session`; execute `session.new_window(window_name='test_window')`; `window.show_options()`; `window._show_options()`; `window._show_options(scope=OptionScope.Pane)`; `window._show_options(scope=OptionScope.Pane, global_=True)`; `window._show_options(scope=OptionScope.Window)`; assert `isinstance(options, dict)`; `isinstance(options_2, dict)`; `isinstance(pane_options, dict)`; `isinstance(pane_options_global, dict)`; `isinstance(window_options, dict)`; `isinstance(window_options_global, dict)`; `isinstance(server_options, dict)`; `isinstance(server_options_global, dict)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_show_window_options_13b17290c2 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_split | Window.split() splits window, returns new Pane, vertical; fixtures `session`; execute `session.new_window(window_name=window_name, attach=True)`; `window.split()`; `float(first_pane.pane_height)`; `float(window.window_width)`; assert `len(window.panes) == 2`; `isinstance(pane, Pane)`; `window.window_width is not None`; `first_pane.pane_height is not None`; `float(first_pane.pane_height) <= (float(window.window_width) + 1) / 2`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_split_ed84764758 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_split_horizontal | Window.split() splits window, returns new Pane, horizontal; fixtures `session`; execute `session.new_window(window_name=window_name, attach=True)`; `window.split(direction=PaneDirection.Right)`; `float(first_pane.pane_width)`; `float(window.window_width)`; assert `len(window.panes) == 2`; `isinstance(pane, Pane)`; `first_pane.pane_width is not None`; `window.window_width is not None`; `float(first_pane.pane_width) <= (float(window.window_width) + 1) / 2`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_split_horizontal_7407298f62 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_split_shell | Window.split() splits window, returns new Pane, vertical; fixtures `session`; execute `session.new_window(window_name=window_name, attach=True)`; `window.split(shell=cmd)`; `float(first_pane.pane_height)`; `float(window.window_width)`; `pane_start_command.replace('"', '')`; assert `len(window.panes) == 2`; `isinstance(pane, Pane)`; `first_pane.pane_height is not None`; `window.window_width is not None`; `float(first_pane.pane_height) <= (float(window.window_width) + 1) / 2`; `pane_start_command.replace('"', '') == cmd`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_split_shell_4321a69614 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_split_size | Window.split() respects size; fixtures `session`; execute `session.new_window(window_name='split window size')`; `window.resize(height=100, width=100)`; `window.split(size=10)`; `window.split(direction=PaneDirection.Right, size=10)`; `window.split(size='10%')`; `window.split(direction=PaneDirection.Right, size='10%')`; assert `pane.pane_height == '10'`; `pane.pane_width == '10'`; `pane.pane_height == '8'`; `pane.pane_width == '8'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_split_size_9a457fc89d | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_split_start_directory[empty_string] | Test Window.split start_directory parameter handling; case inputs `description='Empty string should not add -c flag'`, `start_directory=''`, `test_id='empty_string'`; fixtures `session`, `monkeypatch`, `tmp_path`, `user_path`; execute `session.new_window(window_name=f'test_window_split_{test_id}')`; `window.split(start_directory=actual_start_directory)`; assert `new_pane in window.panes`; `len(window.panes) == 2`; when `start_directory and str(start_directory) not in {'', 'None'} and not (f'{user_path}' in str(start_directory)) and str(start_directory).startswith('./')`: execute `temp_dir.mkdir(parents=True, exist_ok=True)`; `temp_dir.resolve()`; when `expected_path`: execute `new_pane.refresh()`; `pathlib.Path(new_pane.pane_current_path).resolve()`; `pathlib.Path(new_pane.pane_current_path)`; assert `new_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_split_start_directory_empty_string_aa5d5e39ca | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_split_start_directory[none_value] | Test Window.split start_directory parameter handling; case inputs `description='None should not add -c flag'`, `start_directory=None`, `test_id='none_value'`; fixtures `session`, `monkeypatch`, `tmp_path`, `user_path`; execute `session.new_window(window_name=f'test_window_split_{test_id}')`; `window.split(start_directory=actual_start_directory)`; assert `new_pane in window.panes`; `len(window.panes) == 2`; when `start_directory and str(start_directory) not in {'', 'None'} and not (f'{user_path}' in str(start_directory)) and str(start_directory).startswith('./')`: execute `temp_dir.mkdir(parents=True, exist_ok=True)`; `temp_dir.resolve()`; when `expected_path`: execute `new_pane.refresh()`; `pathlib.Path(new_pane.pane_current_path).resolve()`; `pathlib.Path(new_pane.pane_current_path)`; assert `new_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_split_start_directory_none_value_b9fffb1db2 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_split_start_directory[relative_path] | Test Window.split start_directory parameter handling; case inputs `description='Relative path should add -c flag'`, `start_directory='./relative/path'`, `test_id='relative_path'`; fixtures `session`, `monkeypatch`, `tmp_path`, `user_path`; execute `session.new_window(window_name=f'test_window_split_{test_id}')`; `window.split(start_directory=actual_start_directory)`; assert `new_pane in window.panes`; `len(window.panes) == 2`; when `start_directory and str(start_directory) not in {'', 'None'} and not (f'{user_path}' in str(start_directory)) and str(start_directory).startswith('./')`: execute `temp_dir.mkdir(parents=True, exist_ok=True)`; `temp_dir.resolve()`; when `expected_path`: execute `new_pane.refresh()`; `pathlib.Path(new_pane.pane_current_path).resolve()`; `pathlib.Path(new_pane.pane_current_path)`; assert `new_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_split_start_directory_relative_path_0cc2e5ad5e | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_split_start_directory[user_path] | Test Window.split start_directory parameter handling; case inputs `description='User path should add -c flag'`, `start_directory='{user_path}'`, `test_id='user_path'`; fixtures `session`, `monkeypatch`, `tmp_path`, `user_path`; execute `session.new_window(window_name=f'test_window_split_{test_id}')`; `window.split(start_directory=actual_start_directory)`; assert `new_pane in window.panes`; `len(window.panes) == 2`; when `start_directory and str(start_directory) not in {'', 'None'} and not (f'{user_path}' in str(start_directory)) and str(start_directory).startswith('./')`: execute `temp_dir.mkdir(parents=True, exist_ok=True)`; `temp_dir.resolve()`; when `expected_path`: execute `new_pane.refresh()`; `pathlib.Path(new_pane.pane_current_path).resolve()`; `pathlib.Path(new_pane.pane_current_path)`; assert `new_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_split_start_directory_user_path_84ef1bf3db | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_split_start_directory_pathlib | Test Window.split accepts pathlib.Path for start_directory; fixtures `session`, `user_path`; execute `session.new_window(window_name='test_window_split_pathlib')`; `window.split(start_directory=user_path)`; `user_path.resolve()`; `new_pane.refresh()`; `pathlib.Path(new_pane.pane_current_path).resolve()`; `pathlib.Path(new_pane.pane_current_path)`; assert `new_pane in window.panes`; `len(window.panes) == 2`; `new_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_split_start_directory_pathlib_dcdba93572 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_split_window_zoom | Verify splitting window with zoom; fixtures `session`; execute `session.new_window(window_name='split_without_zoom')`; `session.new_window(window_name='split_with_zoom')`; `window_without_zoom.split(zoom=False)`; `window_with_zoom.split(zoom=True)`; assert `pane_without_zoom.width == pane_without_zoom.window_width`; `pane_without_zoom.height is not None`; `pane_without_zoom.window_height is not None`; `pane_without_zoom.height < pane_without_zoom.window_height`; `pane_with_zoom.width == pane_with_zoom.window_width`; `pane_with_zoom.height == pane_with_zoom.window_height`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_split_window_zoom_0aea52f199 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_split_with_environment[multiple_env_vars] | Verify splitting window with environment variables; case inputs `environment={'ENV_VAR_1': 'pane_1', 'ENV_VAR_2': 'pane_2'}`, `test_id='multiple_env_vars'`; fixtures `session`; execute `session.new_window(window_name='split_with_environment')`; `window.split(shell=f"{env} PS1='$ ' sh", environment=environment)`; `pane.send_keys(f'echo ${k}')`; `pane.capture_pane()`; `shutil.which('env')`; `time.sleep(0.05)`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; `pane.capture_pane()[-2] == v`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_split_with_environment_multiple_env_vars_2a95b86945 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_split_with_environment[single_env_var] | Verify splitting window with environment variables; case inputs `environment={'ENV_VAR': 'pane'}`, `test_id='single_env_var'`; fixtures `session`; execute `session.new_window(window_name='split_with_environment')`; `window.split(shell=f"{env} PS1='$ ' sh", environment=environment)`; `pane.send_keys(f'echo ${k}')`; `pane.capture_pane()`; `shutil.which('env')`; `time.sleep(0.05)`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; `pane.capture_pane()[-2] == v`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_split_with_environment_single_env_var_60d98c3334 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_swap_window | Test Window.swap() swaps two windows; fixtures `session`; execute `session.new_window(window_name='swap_w1')`; `session.new_window(window_name='swap_w2')`; `w1.swap(w2)`; `w1.refresh()`; `w2.refresh()`; assert `w1.window_index == w2_idx`; `w2.window_index == w1_idx`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_swap_window_5c8e1d1d27 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_window_context_manager | Test Window context manager functionality; fixtures `session`; execute `session.new_window()`; `window.split()`; assert `window in session.windows`; `pane in window.panes`; `len(window.panes) == 2`; `window not in session.windows`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_window_context_manager_a437730957 | 3.2a through 3.7b | planned parity | +| tests/test_window.py::test_active_pane | Window.active_window returns active Pane; fixtures `session`; assert `isinstance(window.active_pane, Pane)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_active_pane_2a044ab40a | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_deprecated_window_methods_emit_warning[set_window_option] | Verify deprecated Window methods emit DeprecationWarning (0.50.0); case inputs `test_case=DeprecatedMethodTestCase(test_id='set_window_option', method_name='set_window_option', args=('main-pane-height', 20), kwargs={}, expected_error_match='Window\\.set_window_option\\(\\) is deprecated')`; fixtures `session`; execute `session.new_window(window_name='test_deprecation')`; `method(*test_case.args, **test_case.kwargs)`; expect `pytest.warns(DeprecationWarning, match=test_case.expected_error_match)`. | real-tmux integration test | PythonBehaviorParityTest#_window_py_test_deprecated_window_methods_emit_warning_set_window_option_a7a3be4c0b | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_deprecated_window_methods_emit_warning[show_window_option] | Verify deprecated Window methods emit DeprecationWarning (0.50.0); case inputs `test_case=DeprecatedMethodTestCase(test_id='show_window_option', method_name='show_window_option', args=('main-pane-height',), kwargs={}, expected_error_match='Window\\.show_window_option\\(\\) is deprecated')`; fixtures `session`; execute `session.new_window(window_name='test_deprecation')`; `method(*test_case.args, **test_case.kwargs)`; expect `pytest.warns(DeprecationWarning, match=test_case.expected_error_match)`. | real-tmux integration test | PythonBehaviorParityTest#window_py_test_deprecated_window_methods_emit_warning_show_window_option_6e07ddb7b6 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_deprecated_window_methods_emit_warning[show_window_options] | Verify deprecated Window methods emit DeprecationWarning (0.50.0); case inputs `test_case=DeprecatedMethodTestCase(test_id='show_window_options', method_name='show_window_options', args=(), kwargs={}, expected_error_match='Window\\.show_window_options\\(\\) is deprecated')`; fixtures `session`; execute `session.new_window(window_name='test_deprecation')`; `method(*test_case.args, **test_case.kwargs)`; expect `pytest.warns(DeprecationWarning, match=test_case.expected_error_match)`. | real-tmux integration test | PythonBehaviorParityTest#indow_py_test_deprecated_window_methods_emit_warning_show_window_options_bb60c6c4a9 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_empty_window_name | New windows can be created with empty string for window name; fixtures `session`; execute `session.set_option('automatic-rename', 'off')`; `session.new_window(window_name="''", attach=True)`; `session.cmd('list-windows', '-F', '#{window_name}', '-f', '#{==:#{session_name},' + session.session_name + '}')`; assert `window == session.active_window`; `window.window_name == "''"`; `session.session_name is not None`; `"''" in cmd.stdout`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_empty_window_name_fe50634976 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_empty_window_option_returns_None | Verify unset window option returns None; fixtures `session`; execute `session.new_window(window_name='test_window')`; `window.show_option('alternate-screen')`; assert `window.show_option('alternate-screen') is None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_empty_window_option_returns_None_72aa5bb02d | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_fresh_window_data | Verify window data is fresh; fixtures `session`; execute `window.split()`; `session.new_window(window_name='second')`; `session.new_window(window_name='hey')`; `session.select_window('1')`; `active_window._show_option('pane-base-index', global_=True)`; `pane_to_select.select()`; assert `active_window is not None`; `pane_base_idx is not None`; `len(session.windows) == 1`; `len(session.active_window.panes) == 1`; `session.session_id != '@0'`; `current_windows == 1`; `isinstance(session.server, Server)`; `isinstance(w, Window)`; `isinstance(window, Window)`; `pane_to_select is not None`; `active_pane is not None`; `pane_to_select_2 is not None`; `current_windows == len(session.windows)`; `window_to_kill is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_fresh_window_data_c86c1a0f96 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_kill_window | Test window.kill() kills window; fixtures `session`; execute `session.new_window()`; `w.kill()`; `w.refresh()`; expect `pytest.raises(ObjectDoesNotExist)`; assert `w.window_id is not None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_kill_window_d8eb2eff37 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_last_pane | Test Window.last_pane() selects the previously active pane; fixtures `session`; execute `session.new_window(window_name='test_last_pane')`; `window.last_pane()`; `pane1.split()`; `pane2.select()`; `pane1.select()`; `pane2.refresh()`; assert `pane1 is not None`; `result is not None`; `pane2.pane_active == '1'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_last_pane_d5eb4916f7 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_link_unlink_window | Test Window.link() and Window.unlink(); fixtures `server`, `session`; execute `server.new_session(session_name='link_target')`; `session.new_window(window_name='link_test')`; `w.link(s2, detach=True)`; `s2.refresh()`; `linked_windows[0].unlink()`; assert `'link_test' in s2_window_names`; `len(linked_windows) > 0`; `'link_test' not in s2_window_names`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_link_unlink_window_32d7f06beb | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_move_window | Window.move_window results in changed index; fixtures `session`; execute `session.new_window(window_name='test_window')`; `window.move_window(new_index)`; assert `window.window_index is not None`; `window.window_index == new_index`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_move_window_7001ddfe96 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_move_window_kill_target | Test Window.move_window() with kill_target flag; fixtures `session`; execute `session.new_window(window_name='move_w1')`; `session.new_window(window_name='move_w2')`; `session.refresh()`; `extra_windows[0].move_window(destination=w2_index, kill_target=True)`; assert `w2.window_index is not None`; `len(extra_windows) == 1`; `len(session.windows) == initial_count - 1`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_move_window_kill_target_b7e089fe88 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_move_window_no_select | Test Window.move_window() with no_select flag; fixtures `session`; execute `session.new_window(window_name='nosel_w1', attach=True)`; `session.new_window(window_name='nosel_w2', attach=False)`; `session.refresh()`; `w2.move_window(destination='99', no_select=True)`; assert `session.active_window.window_id == w1.window_id`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_move_window_no_select_a6ab22d3d4 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_move_window_relative_returns_fresh_window[after-0] | Window.move_window() returns fresh state for relative moves; case inputs `destination_offset=0`, `flag_name='after'`; fixtures `session`; execute `session.new_window(window_name='move_middle')`; `session.new_window(window_name='move_relative')`; `Window.from_window_id(server=session.server, window_id=moving_window.window_id)`; assert `destination_window.window_index is not None`; `moving_window.window_id is not None`; `moving_window.window_index == fresh_window.window_index`; `moving_window.session_id == fresh_window.session_id`; when `flag_name == 'after'`: execute `moving_window.move_window(destination, after=True)`; when `not (flag_name == 'after')`: execute `moving_window.move_window(destination, before=True)`. | real-tmux integration test | PythonBehaviorParityTest#ts_test_window_py_test_move_window_relative_returns_fresh_window_after_0_de00174c42 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_move_window_relative_returns_fresh_window[before-1] | Window.move_window() returns fresh state for relative moves; case inputs `destination_offset=1`, `flag_name='before'`; fixtures `session`; execute `session.new_window(window_name='move_middle')`; `session.new_window(window_name='move_relative')`; `Window.from_window_id(server=session.server, window_id=moving_window.window_id)`; assert `destination_window.window_index is not None`; `moving_window.window_id is not None`; `moving_window.window_index == fresh_window.window_index`; `moving_window.session_id == fresh_window.session_id`; when `flag_name == 'after'`: execute `moving_window.move_window(destination, after=True)`; when `not (flag_name == 'after')`: execute `moving_window.move_window(destination, before=True)`. | real-tmux integration test | PythonBehaviorParityTest#s_test_window_py_test_move_window_relative_returns_fresh_window_before_1_151f2f516d | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_move_window_renumber | Test Window.move_window() with renumber flag; fixtures `session`; execute `session.new_window(window_name='ren_w1')`; `session.new_window(window_name='ren_w2')`; `session.new_window(window_name='ren_w3')`; `session.refresh()`; `w2.kill()`; `w3.move_window(renumber=True)`; assert `indices[i + 1] - indices[i] == 1`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_move_window_renumber_d5796a5b32 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_move_window_to_other_session | Window.move_window to other session; fixtures `server`, `session`; execute `session.new_window(window_name='test_window')`; `server.new_session('test_move_window')`; `window.move_window(session=new_session.session_id)`; assert `window_id is not None`; `new_session.windows.get(window_id=window_id) == window`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_move_window_to_other_session_92d3c24001 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_move_window_to_other_session_with_destination | Window.move_window() returns fresh state for cross-session moves; fixtures `server`, `session`; execute `session.new_window(window_name='move_cross_session')`; `server.new_session('test_move_window_destination')`; `window.move_window(destination=destination, session=new_session.session_id)`; `Window.from_window_id(server=server, window_id=window.window_id)`; assert `window.window_id is not None`; `fresh_window.session_id == new_session.session_id`; `fresh_window.window_index == destination`; `window.session_id == fresh_window.session_id`; `window.window_index == fresh_window.window_index`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_move_window_to_other_session_with_destination_e7f86afe1d | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_new_pane | Window.new_pane() creates a floating pane on tmux 3.7+ (else raises); fixtures `session`; execute `session.new_window(window_name='win_floating')`; `window.resize(height=50, width=200)`; when `has_gte_version('3.7')`: execute `window.new_pane(width=60, height=12, shell='sleep 30')`; assert `floating.pane_floating_flag == '1'`; `floating in window.panes`; when `not (has_gte_version('3.7'))`: execute `window.new_pane(width=40, height=10)`; expect `pytest.raises(exc.LibTmuxException, match='new_pane .*requires tmux 3.7')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_new_pane_3580a3317d | 3.3a through 3.7c; conditional boundaries: 3.7 | planned parity | +| tests/test_window.py::test_new_window_name_colon_period_accepted[colon] | Window names with ':' and '.' are accepted verbatim; case inputs `test_id='colon'`, `window_name='project:frontend'`; fixtures `session`; execute `session.new_window(window_name=window_name)`; `window.kill()`; assert `window.window_name == window_name`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_new_window_name_colon_period_accepted_colon_007435d489 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_new_window_name_colon_period_accepted[period] | Window names with ':' and '.' are accepted verbatim; case inputs `test_id='period'`, `window_name='app-v1.0'`; fixtures `session`; execute `session.new_window(window_name=window_name)`; `window.kill()`; assert `window.window_name == window_name`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_new_window_name_colon_period_accepted_period_6ef9d54ed8 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_new_window_with_direction | Verify new window with direction; fixtures `session`; execute `window.refresh()`; `session.new_window(window_name='Example')`; `window_initial.new_window(window_name='Window before', direction=WindowDirection.Before)`; `window_initial.refresh()`; `window_initial.new_window(window_name='Window after', direction=WindowDirection.After)`; `window_after.refresh()`; assert `window_initial.window_index == '2'`; `window_before.window_index == '2'`; `window_initial.window_index == '3'`; `window_after.window_index == '4'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_new_window_with_direction_c2a47c1ee5 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_newest_pane_data | Test window.panes has fresh data; fixtures `session`; execute `session.new_window(window_name='test', attach=True)`; `window.split(attach=True)`; assert `isinstance(window, Window)`; `len(window.panes) == 1`; `len(window.panes) == 2`; `len(window.panes) == 3`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_newest_pane_data_79e2ce3983 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_next_layout | Test Window.next_layout() cycles to the next layout; fixtures `session`; execute `session.new_window(window_name='test_next_layout')`; `window.resize(height=40, width=80)`; `pane.split()`; `window.select_layout('even-horizontal')`; `window.refresh()`; `window.next_layout()`; assert `pane is not None`; `layout_before != layout_after`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_next_layout_b7b4774637 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_previous_layout | Test Window.previous_layout() cycles back; fixtures `session`; execute `session.new_window(window_name='test_prev_layout')`; `window.resize(height=40, width=80)`; `pane.split()`; `window.select_layout('even-horizontal')`; `window.refresh()`; `window.next_layout()`; assert `pane is not None`; `layout_before == layout_after`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_previous_layout_dde68474fa | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_resize | Verify resizing window; fixtures `session`; execute `session.cmd('detach-client', '-s')`; `window.resize(height=10)`; `window.resize(width=10)`; `window.resize(adjustment_direction=ResizeAdjustmentDirection.Down, adjustment=window_height_adjustment * 2)`; `window.resize(adjustment_direction=ResizeAdjustmentDirection.Up, adjustment=window_height_adjustment)`; `window.resize(height=50)`; assert `window.window_height is not None`; `window.window_width is not None`; `int(window.window_height) == 10`; `int(window.window_width) == 10`; `window_height_before + window_height_adjustment * 2 == int(window.window_height)`; `window_height_before - window_height_adjustment == int(window.window_height)`; `window_height_before > window_height_shrunk`; `window`; `window_height_before < window_height_expanded`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_resize_c4965fd743 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_respawn_window | Test Window.respawn() with kill flag; fixtures `session`; execute `session.new_window(window_name='test_respawn_w')`; `window.respawn(kill=True, shell='sh')`; `window.refresh()`; `session.refresh()`; assert `window.window_id in [w.window_id for w in session.windows]`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_respawn_window_b5eafa65d6 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_rotate_window | Test Window.rotate() rotates pane positions; fixtures `session`; execute `session.new_window(window_name='test_rotate')`; `window.resize(height=40, width=80)`; `window.rotate()`; `pane1.split()`; `pane2.split()`; `pane1.refresh()`; assert `pane1 is not None`; `idx_before != idx_after`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_rotate_window_6eca5ac10b | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_select_layout_accepts_no_arg | Tmux allows select-layout with no arguments, so let's allow it here; fixtures `server`, `session`; execute `session.new_window(window_name='test_window')`; `window.select_layout()`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_select_layout_accepts_no_arg_0af9999b14 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_select_layout_mutual_exclusion | Test that layout string and flags are mutually exclusive; fixtures `session`; execute `session.new_window(window_name='test_layout_mutex')`; `window.select_layout('tiled', spread=True)`; expect `pytest.raises(ValueError, match='Cannot specify both')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_select_layout_mutual_exclusion_884674c936 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_select_layout_next_previous | Test Window.select_layout() with next/previous flags; fixtures `session`; execute `session.new_window(window_name='test_layout_cycle')`; `window.resize(height=40, width=80)`; `pane.split()`; `window.select_layout('even-horizontal')`; `window.refresh()`; `window.select_layout(next_layout=True)`; assert `pane is not None`; `layout_before != layout_after_next`; `layout_after_prev == layout_before`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_select_layout_next_previous_a1947ac64f | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_select_layout_spread | Test Window.select_layout() with spread flag; fixtures `session`; execute `session.new_window(window_name='test_layout_spread')`; `window.resize(height=40, width=80)`; `pane.split()`; `window.select_layout(spread=True)`; assert `pane is not None`; `len(window.panes) == 3`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_select_layout_spread_df22951796 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_select_window | Test Window.select_window(); fixtures `session`; execute `session.new_window(window_name='testing 3')`; `session.select_window(str(window_base_index))`; `session.select_window('testing 3')`; assert `window_count == 1`; `session.active_window.window_index is not None`; `window.window_index is not None`; `int(window_base_index) + 1 == int(window.window_index)`; `window_base_index == int(session.active_window.window_index)`; `int(window_base_index) + 1 == int(session.active_window.window_index)`; `len(session.windows) == 2`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_select_window_7403165fda | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_set_and_show_window_options | Window.set_option() then Window._show_options(key); fixtures `session`; execute `session.new_window(window_name='test_window')`; `window.set_option('main-pane-height', 20)`; `window._show_option('main-pane-height')`; `window.set_option('main-pane-height', 40)`; `window._show_options(scope=OptionScope.Session)`; `window.set_option('pane-border-format', ' #P ')`; expect `pytest.raises(KeyError)`; assert `window._show_option('main-pane-height') == 20`; `window._show_option('main-pane-height') == 40`; `window._show_options(scope=OptionScope.Session)['main-pane-height'] == 40`; `window._show_option('pane-border-format') == ' #P '`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_set_and_show_window_options_227ba527da | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_set_window_and_show_window_options | Window.set_option() then Window.show_option(key); fixtures `session`; execute `session.new_window(window_name='test_window')`; `window.set_option('main-pane-height', 20)`; `window.show_option('main-pane-height')`; `window.set_option('main-pane-height', 40)`; `window.show_options()`; `window.set_option('pane-border-format', ' #P ')`; assert `window.show_option('main-pane-height') == 20`; `window.show_option('main-pane-height') == 40`; `window.show_options()['main-pane-height'] == 40`; `window.show_option('pane-border-format') == ' #P '`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_set_window_and_show_window_options_d96249517a | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_set_window_option_ambiguous | set_option raises AmbiguousOption for ambiguous option; fixtures `session`; execute `session.new_window(window_name='test_window')`; `window.set_option('clock-mode', 12)`; expect `pytest.raises(exc.AmbiguousOption)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_set_window_option_ambiguous_1c63d8c847 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_set_window_option_invalid | Window.set_option raises InvalidOption for invalid option key; fixtures `session`; execute `session.new_window(window_name='test_window')`; `window.set_option('afewewfew', 43)`; expect `pytest.raises(exc.InvalidOption)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_set_window_option_invalid_dc1098935a | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_show_window_option | Set option then Window.show_option(key); fixtures `session`; execute `session.new_window(window_name='test_window')`; `window.set_option('main-pane-height', 20)`; `window.show_option('main-pane-height')`; `window.set_option('main-pane-height', 40)`; assert `window.show_option('main-pane-height') == 20`; `window.show_option('main-pane-height') == 40`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_show_window_option_81f670d32c | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_show_window_option_ambiguous | show_option raises AmbiguousOption for ambiguous option; fixtures `session`; execute `session.new_window(window_name='test_window')`; `window.show_option('clock-mode')`; expect `pytest.raises(exc.AmbiguousOption)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_show_window_option_ambiguous_7e784bd71f | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_show_window_option_unknown | Window.show_option raises InvalidOption for bad option key; fixtures `session`; execute `session.new_window(window_name='test_window')`; `window.show_option('moooz')`; expect `pytest.raises(exc.InvalidOption)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_show_window_option_unknown_b9f31bc4f9 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_show_window_options | Window.show_options() returns dict; fixtures `session`; execute `session.new_window(window_name='test_window')`; `window.show_options()`; `window._show_options()`; `window._show_options(scope=OptionScope.Pane)`; `window._show_options(scope=OptionScope.Pane, global_=True)`; `window._show_options(scope=OptionScope.Window)`; assert `isinstance(options, dict)`; `isinstance(options_2, dict)`; `isinstance(pane_options, dict)`; `isinstance(pane_options_global, dict)`; `isinstance(window_options, dict)`; `isinstance(window_options_global, dict)`; `isinstance(server_options, dict)`; `isinstance(server_options_global, dict)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_show_window_options_13b17290c2 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_split | Window.split() splits window, returns new Pane, vertical; fixtures `session`; execute `session.new_window(window_name=window_name, attach=True)`; `window.split()`; `float(first_pane.pane_height)`; `float(window.window_width)`; assert `len(window.panes) == 2`; `isinstance(pane, Pane)`; `window.window_width is not None`; `first_pane.pane_height is not None`; `float(first_pane.pane_height) <= (float(window.window_width) + 1) / 2`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_split_ed84764758 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_split_horizontal | Window.split() splits window, returns new Pane, horizontal; fixtures `session`; execute `session.new_window(window_name=window_name, attach=True)`; `window.split(direction=PaneDirection.Right)`; `float(first_pane.pane_width)`; `float(window.window_width)`; assert `len(window.panes) == 2`; `isinstance(pane, Pane)`; `first_pane.pane_width is not None`; `window.window_width is not None`; `float(first_pane.pane_width) <= (float(window.window_width) + 1) / 2`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_split_horizontal_7407298f62 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_split_shell | Window.split() splits window, returns new Pane, vertical; fixtures `session`; execute `session.new_window(window_name=window_name, attach=True)`; `window.split(shell=cmd)`; `float(first_pane.pane_height)`; `float(window.window_width)`; `pane_start_command.replace('"', '')`; assert `len(window.panes) == 2`; `isinstance(pane, Pane)`; `first_pane.pane_height is not None`; `window.window_width is not None`; `float(first_pane.pane_height) <= (float(window.window_width) + 1) / 2`; `pane_start_command.replace('"', '') == cmd`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_split_shell_4321a69614 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_split_size | Window.split() respects size; fixtures `session`; execute `session.new_window(window_name='split window size')`; `window.resize(height=100, width=100)`; `window.split(size=10)`; `window.split(direction=PaneDirection.Right, size=10)`; `window.split(size='10%')`; `window.split(direction=PaneDirection.Right, size='10%')`; assert `pane.pane_height == '10'`; `pane.pane_width == '10'`; `pane.pane_height == '8'`; `pane.pane_width == '8'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_split_size_9a457fc89d | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_split_start_directory[empty_string] | Test Window.split start_directory parameter handling; case inputs `description='Empty string should not add -c flag'`, `start_directory=''`, `test_id='empty_string'`; fixtures `session`, `monkeypatch`, `tmp_path`, `user_path`; execute `session.new_window(window_name=f'test_window_split_{test_id}')`; `window.split(start_directory=actual_start_directory)`; assert `new_pane in window.panes`; `len(window.panes) == 2`; when `start_directory and str(start_directory) not in {'', 'None'} and not (f'{user_path}' in str(start_directory)) and str(start_directory).startswith('./')`: execute `temp_dir.mkdir(parents=True, exist_ok=True)`; `temp_dir.resolve()`; when `expected_path`: execute `new_pane.refresh()`; `pathlib.Path(new_pane.pane_current_path).resolve()`; `pathlib.Path(new_pane.pane_current_path)`; assert `new_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_split_start_directory_empty_string_aa5d5e39ca | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_split_start_directory[none_value] | Test Window.split start_directory parameter handling; case inputs `description='None should not add -c flag'`, `start_directory=None`, `test_id='none_value'`; fixtures `session`, `monkeypatch`, `tmp_path`, `user_path`; execute `session.new_window(window_name=f'test_window_split_{test_id}')`; `window.split(start_directory=actual_start_directory)`; assert `new_pane in window.panes`; `len(window.panes) == 2`; when `start_directory and str(start_directory) not in {'', 'None'} and not (f'{user_path}' in str(start_directory)) and str(start_directory).startswith('./')`: execute `temp_dir.mkdir(parents=True, exist_ok=True)`; `temp_dir.resolve()`; when `expected_path`: execute `new_pane.refresh()`; `pathlib.Path(new_pane.pane_current_path).resolve()`; `pathlib.Path(new_pane.pane_current_path)`; assert `new_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_split_start_directory_none_value_b9fffb1db2 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_split_start_directory[relative_path] | Test Window.split start_directory parameter handling; case inputs `description='Relative path should add -c flag'`, `start_directory='./relative/path'`, `test_id='relative_path'`; fixtures `session`, `monkeypatch`, `tmp_path`, `user_path`; execute `session.new_window(window_name=f'test_window_split_{test_id}')`; `window.split(start_directory=actual_start_directory)`; assert `new_pane in window.panes`; `len(window.panes) == 2`; when `start_directory and str(start_directory) not in {'', 'None'} and not (f'{user_path}' in str(start_directory)) and str(start_directory).startswith('./')`: execute `temp_dir.mkdir(parents=True, exist_ok=True)`; `temp_dir.resolve()`; when `expected_path`: execute `new_pane.refresh()`; `pathlib.Path(new_pane.pane_current_path).resolve()`; `pathlib.Path(new_pane.pane_current_path)`; assert `new_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_split_start_directory_relative_path_0cc2e5ad5e | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_split_start_directory[user_path] | Test Window.split start_directory parameter handling; case inputs `description='User path should add -c flag'`, `start_directory='{user_path}'`, `test_id='user_path'`; fixtures `session`, `monkeypatch`, `tmp_path`, `user_path`; execute `session.new_window(window_name=f'test_window_split_{test_id}')`; `window.split(start_directory=actual_start_directory)`; assert `new_pane in window.panes`; `len(window.panes) == 2`; when `start_directory and str(start_directory) not in {'', 'None'} and not (f'{user_path}' in str(start_directory)) and str(start_directory).startswith('./')`: execute `temp_dir.mkdir(parents=True, exist_ok=True)`; `temp_dir.resolve()`; when `expected_path`: execute `new_pane.refresh()`; `pathlib.Path(new_pane.pane_current_path).resolve()`; `pathlib.Path(new_pane.pane_current_path)`; assert `new_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_split_start_directory_user_path_84ef1bf3db | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_split_start_directory_pathlib | Test Window.split accepts pathlib.Path for start_directory; fixtures `session`, `user_path`; execute `session.new_window(window_name='test_window_split_pathlib')`; `window.split(start_directory=user_path)`; `user_path.resolve()`; `new_pane.refresh()`; `pathlib.Path(new_pane.pane_current_path).resolve()`; `pathlib.Path(new_pane.pane_current_path)`; assert `new_pane in window.panes`; `len(window.panes) == 2`; `new_pane.pane_current_path is not None`; `actual_path == expected_path`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_split_start_directory_pathlib_dcdba93572 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_split_window_zoom | Verify splitting window with zoom; fixtures `session`; execute `session.new_window(window_name='split_without_zoom')`; `session.new_window(window_name='split_with_zoom')`; `window_without_zoom.split(zoom=False)`; `window_with_zoom.split(zoom=True)`; assert `pane_without_zoom.width == pane_without_zoom.window_width`; `pane_without_zoom.height is not None`; `pane_without_zoom.window_height is not None`; `pane_without_zoom.height < pane_without_zoom.window_height`; `pane_with_zoom.width == pane_with_zoom.window_width`; `pane_with_zoom.height == pane_with_zoom.window_height`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_split_window_zoom_0aea52f199 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_split_with_environment[multiple_env_vars] | Verify splitting window with environment variables; case inputs `environment={'ENV_VAR_1': 'pane_1', 'ENV_VAR_2': 'pane_2'}`, `test_id='multiple_env_vars'`; fixtures `session`; execute `session.new_window(window_name='split_with_environment')`; `window.split(shell=f"{env} PS1='$ ' sh", environment=environment)`; `pane.send_keys(f'echo ${k}')`; `pane.capture_pane()`; `shutil.which('env')`; `time.sleep(0.05)`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; `pane.capture_pane()[-2] == v`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_split_with_environment_multiple_env_vars_2a95b86945 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_split_with_environment[single_env_var] | Verify splitting window with environment variables; case inputs `environment={'ENV_VAR': 'pane'}`, `test_id='single_env_var'`; fixtures `session`; execute `session.new_window(window_name='split_with_environment')`; `window.split(shell=f"{env} PS1='$ ' sh", environment=environment)`; `pane.send_keys(f'echo ${k}')`; `pane.capture_pane()`; `shutil.which('env')`; `time.sleep(0.05)`; assert `env is not None; message='Cannot find usable `env` in PATH.'`; `pane is not None`; `pane.capture_pane()[-2] == v`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_split_with_environment_single_env_var_60d98c3334 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_swap_window | Test Window.swap() swaps two windows; fixtures `session`; execute `session.new_window(window_name='swap_w1')`; `session.new_window(window_name='swap_w2')`; `w1.swap(w2)`; `w1.refresh()`; `w2.refresh()`; assert `w1.window_index == w2_idx`; `w2.window_index == w1_idx`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_swap_window_5c8e1d1d27 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_window_context_manager | Test Window context manager functionality; fixtures `session`; execute `session.new_window()`; `window.split()`; assert `window in session.windows`; `pane in window.panes`; `len(window.panes) == 2`; `window not in session.windows`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_window_context_manager_a437730957 | 3.3a through 3.7c | planned parity | | tests/test_window.py::test_window_display_message_flags[no_expand_literal] | Window.display_message() resolves window-scoped formats; case inputs `cmd='#{window_id}'`, `expected_in_output='#{window_id}'`, `kwargs={'get_text': True, 'no_expand': True}`, `min_tmux_version='3.4'`, `test_id='no_expand_literal'`; fixtures `session`; execute `window.display_message(cmd, **kwargs)`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_in_output is not None`: execute `'\n'.join(result)`; assert `result is not None`; `expected_in_output in output`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_window_display_message_flags_no_expand_literal_a801096c41 | 3.4 through 3.7b | planned parity | -| tests/test_window.py::test_window_display_message_flags[window_id] | Window.display_message() resolves window-scoped formats; case inputs `cmd='#{window_id}'`, `expected_in_output='@'`, `kwargs={'get_text': True}`, `min_tmux_version=None`, `test_id='window_id'`; fixtures `session`; execute `window.display_message(cmd, **kwargs)`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_in_output is not None`: execute `'\n'.join(result)`; assert `result is not None`; `expected_in_output in output`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_window_display_message_flags_window_id_5a05df26f2 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_window_display_message_flags[window_index_via_format_string] | Window.display_message() resolves window-scoped formats; case inputs `cmd=''`, `expected_in_output='1'`, `kwargs={'get_text': True, 'format_string': '#{window_index}'}`, `min_tmux_version=None`, `test_id='window_index_via_format_string'`; fixtures `session`; execute `window.display_message(cmd, **kwargs)`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_in_output is not None`: execute `'\n'.join(result)`; assert `result is not None`; `expected_in_output in output`. | real-tmux integration test | PythonBehaviorParityTest#ndow_py_test_window_display_message_flags_window_index_via_format_string_8f708c0dda | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_window_display_message_flags[zoomed_flag_default_zero] | Window.display_message() resolves window-scoped formats; case inputs `cmd='#{window_zoomed_flag}'`, `expected_in_output='0'`, `kwargs={'get_text': True}`, `min_tmux_version=None`, `test_id='zoomed_flag_default_zero'`; fixtures `session`; execute `window.display_message(cmd, **kwargs)`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_in_output is not None`: execute `'\n'.join(result)`; assert `result is not None`; `expected_in_output in output`. | real-tmux integration test | PythonBehaviorParityTest#est_window_py_test_window_display_message_flags_zoomed_flag_default_zero_264423d0a5 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_window_display_message_no_text_returns_none | Without ``get_text=True`` the call renders to status line and returns None; fixtures `session`; execute `window.display_message('hi from libtmux')`; assert `result is None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_window_display_message_no_text_returns_none_4beae0a2b4 | 3.2a through 3.7b | planned parity | +| tests/test_window.py::test_window_display_message_flags[window_id] | Window.display_message() resolves window-scoped formats; case inputs `cmd='#{window_id}'`, `expected_in_output='@'`, `kwargs={'get_text': True}`, `min_tmux_version=None`, `test_id='window_id'`; fixtures `session`; execute `window.display_message(cmd, **kwargs)`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_in_output is not None`: execute `'\n'.join(result)`; assert `result is not None`; `expected_in_output in output`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_window_display_message_flags_window_id_5a05df26f2 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_window_display_message_flags[window_index_via_format_string] | Window.display_message() resolves window-scoped formats; case inputs `cmd=''`, `expected_in_output='1'`, `kwargs={'get_text': True, 'format_string': '#{window_index}'}`, `min_tmux_version=None`, `test_id='window_index_via_format_string'`; fixtures `session`; execute `window.display_message(cmd, **kwargs)`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_in_output is not None`: execute `'\n'.join(result)`; assert `result is not None`; `expected_in_output in output`. | real-tmux integration test | PythonBehaviorParityTest#ndow_py_test_window_display_message_flags_window_index_via_format_string_8f708c0dda | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_window_display_message_flags[zoomed_flag_default_zero] | Window.display_message() resolves window-scoped formats; case inputs `cmd='#{window_zoomed_flag}'`, `expected_in_output='0'`, `kwargs={'get_text': True}`, `min_tmux_version=None`, `test_id='zoomed_flag_default_zero'`; fixtures `session`; execute `window.display_message(cmd, **kwargs)`; when `min_tmux_version and (not has_gte_version(min_tmux_version))`: skip via `pytest.skip(f'Requires tmux {min_tmux_version}+')`; when `expected_in_output is not None`: execute `'\n'.join(result)`; assert `result is not None`; `expected_in_output in output`. | real-tmux integration test | PythonBehaviorParityTest#est_window_py_test_window_display_message_flags_zoomed_flag_default_zero_264423d0a5 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_window_display_message_no_text_returns_none | Without ``get_text=True`` the call renders to status line and returns None; fixtures `session`; execute `window.display_message('hi from libtmux')`; assert `result is None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_window_display_message_no_text_returns_none_4beae0a2b4 | 3.3a through 3.7c | planned parity | | tests/test_window.py::test_window_display_message_target_client | ``target_client`` is plumbed through as ``-c``; fixtures `control_mode`, `session`; execute `control_mode()`; `window.display_message('#{window_id}', get_text=True, target_client=ctl.client_name)`; `result[0].startswith('@')`; assert `isinstance(result, list)`; `len(result) == 1`; `result[0].startswith('@')`; when `not has_gte_version('3.3')`: skip via `pytest.skip('display-message -p via control-mode client unreliable on tmux 3.2a')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_window_display_message_target_client_9f23f6ef06 | 3.3 through 3.7b | planned parity | -| tests/test_window.py::test_window_display_message_warns_on_tmux_error | Tmux stderr on ``display-message`` surfaces as a :class:`UserWarning`; fixtures `session`; execute `window.display_message('x', get_text=True, format_string='#{window_id}')`; expect `pytest.warns(UserWarning, match='only one of -F or argument')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_window_display_message_warns_on_tmux_error_9c99c2acac | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_window_flags_field_returns_string | ``window_flags`` summarizes window state and reads as a string; fixtures `session`; execute `window.refresh()`; assert `isinstance(window.window_flags, str)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_window_flags_field_returns_string_3ef969de3e | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_window_format_field_declared_and_hydrated[window_active_clients_list] | Tmux's window-scope format tokens hydrate onto the typed ``Window``; case inputs `field_name='window_active_clients_list'`; fixtures `session`; execute `window.refresh()`; assert `field_name in window.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#est_window_format_field_declared_and_hydrated_window_active_clients_list_94961a7521 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_window_format_field_declared_and_hydrated[window_active_sessions_list] | Tmux's window-scope format tokens hydrate onto the typed ``Window``; case inputs `field_name='window_active_sessions_list'`; fixtures `session`; execute `window.refresh()`; assert `field_name in window.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#st_window_format_field_declared_and_hydrated_window_active_sessions_list_1eaa504749 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_window_format_field_declared_and_hydrated[window_activity_flag] | Tmux's window-scope format tokens hydrate onto the typed ``Window``; case inputs `field_name='window_activity_flag'`; fixtures `session`; execute `window.refresh()`; assert `field_name in window.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#w_py_test_window_format_field_declared_and_hydrated_window_activity_flag_6f9534f5b6 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_window_format_field_declared_and_hydrated[window_bell_flag] | Tmux's window-scope format tokens hydrate onto the typed ``Window``; case inputs `field_name='window_bell_flag'`; fixtures `session`; execute `window.refresh()`; assert `field_name in window.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#indow_py_test_window_format_field_declared_and_hydrated_window_bell_flag_b530d90b78 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_window_format_field_declared_and_hydrated[window_bigger] | Tmux's window-scope format tokens hydrate onto the typed ``Window``; case inputs `field_name='window_bigger'`; fixtures `session`; execute `window.refresh()`; assert `field_name in window.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#t_window_py_test_window_format_field_declared_and_hydrated_window_bigger_b2db8ac5ed | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_window_format_field_declared_and_hydrated[window_end_flag] | Tmux's window-scope format tokens hydrate onto the typed ``Window``; case inputs `field_name='window_end_flag'`; fixtures `session`; execute `window.refresh()`; assert `field_name in window.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#window_py_test_window_format_field_declared_and_hydrated_window_end_flag_2e1a1a7868 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_window_format_field_declared_and_hydrated[window_flags] | Tmux's window-scope format tokens hydrate onto the typed ``Window``; case inputs `field_name='window_flags'`; fixtures `session`; execute `window.refresh()`; assert `field_name in window.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#st_window_py_test_window_format_field_declared_and_hydrated_window_flags_58713f2850 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_window_format_field_declared_and_hydrated[window_format] | Tmux's window-scope format tokens hydrate onto the typed ``Window``; case inputs `field_name='window_format'`; fixtures `session`; execute `window.refresh()`; assert `field_name in window.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#t_window_py_test_window_format_field_declared_and_hydrated_window_format_2ddf445c63 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_window_format_field_declared_and_hydrated[window_last_flag] | Tmux's window-scope format tokens hydrate onto the typed ``Window``; case inputs `field_name='window_last_flag'`; fixtures `session`; execute `window.refresh()`; assert `field_name in window.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#indow_py_test_window_format_field_declared_and_hydrated_window_last_flag_dc04b5fc3c | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_window_format_field_declared_and_hydrated[window_silence_flag] | Tmux's window-scope format tokens hydrate onto the typed ``Window``; case inputs `field_name='window_silence_flag'`; fixtures `session`; execute `window.refresh()`; assert `field_name in window.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#ow_py_test_window_format_field_declared_and_hydrated_window_silence_flag_8d06c7bef7 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_window_format_field_declared_and_hydrated[window_start_flag] | Tmux's window-scope format tokens hydrate onto the typed ``Window``; case inputs `field_name='window_start_flag'`; fixtures `session`; execute `window.refresh()`; assert `field_name in window.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#ndow_py_test_window_format_field_declared_and_hydrated_window_start_flag_8a06735207 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_window_format_field_declared_and_hydrated[window_visible_layout] | Tmux's window-scope format tokens hydrate onto the typed ``Window``; case inputs `field_name='window_visible_layout'`; fixtures `session`; execute `window.refresh()`; assert `field_name in window.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#_py_test_window_format_field_declared_and_hydrated_window_visible_layout_a78ac844e1 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_window_refresh_raises_when_window_id_is_none | ``Window.refresh()`` raises ``ValueError`` when ``window_id`` is unset; fixtures `session`; execute `window.refresh()`; `Window(server=session.server)`; expect `pytest.raises(ValueError, match='window_id')`; assert `window.window_id is None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_window_refresh_raises_when_window_id_is_none_45ce538830 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_window_rename[rename_with_escapes] | Test Window.rename_window(); case inputs `test_id='rename_with_escapes'`, `window_name_after='hello \\\\ wazzup 0'`, `window_name_before='test'`, `window_name_input='hello \\ wazzup 0'`; fixtures `session`; execute `session.set_option('automatic-rename', 'off', scope=None)`; `session.new_window(window_name=window_name_before, attach=True)`; `window.rename_window(window_name_input)`; assert `window == session.active_window`; `window.window_name == window_name_before`; `window.window_name == window_name_after`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_window_rename_rename_with_escapes_69f5d0ecce | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_window_rename[rename_with_spaces] | Test Window.rename_window(); case inputs `test_id='rename_with_spaces'`, `window_name_after='ha ha ha fjewlkjflwef'`, `window_name_before='test'`, `window_name_input='ha ha ha fjewlkjflwef'`; fixtures `session`; execute `session.set_option('automatic-rename', 'off', scope=None)`; `session.new_window(window_name=window_name_before, attach=True)`; `window.rename_window(window_name_input)`; assert `window == session.active_window`; `window.window_name == window_name_before`; `window.window_name == window_name_after`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_window_rename_rename_with_spaces_c12af2abc5 | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_window_search_panes_filter_by_id | ``Window.search_panes(filter=...)`` returns only the matching pane id; fixtures `session`; execute `window.split()`; `window.search_panes(filter=f'#{{m:{target.pane_id},#{{pane_id}}}}')`; assert `[p.pane_id for p in matches] == [target.pane_id]`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_window_search_panes_filter_by_id_4dd56628de | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_window_search_panes_no_filter_equivalent_to_property | ``search_panes()`` with no filter matches the existing ``panes`` property; fixtures `session`; execute `window.split()`; `window.search_panes()`; assert `from_property == from_method`. | real-tmux integration test | PythonBehaviorParityTest#test_window_py_test_window_search_panes_no_filter_equivalent_to_property_e52428dc6e | 3.2a through 3.7b | planned parity | -| tests/test_window.py::test_window_zoomed_flag_field_toggle | ``window.window_zoomed_flag`` reflects tmux's zoom state across refresh; fixtures `session`; execute `window.split()`; `window.refresh()`; `pane.resize(zoom=True)`; assert `window.window_zoomed_flag == '0'`; `pane is not None`; `window.window_zoomed_flag == '1'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_window_zoomed_flag_field_toggle_d1301f3b2f | 3.2a through 3.7b | planned parity | +| tests/test_window.py::test_window_display_message_warns_on_tmux_error | Tmux stderr on ``display-message`` surfaces as a :class:`UserWarning`; fixtures `session`; execute `window.display_message('x', get_text=True, format_string='#{window_id}')`; expect `pytest.warns(UserWarning, match='only one of -F or argument')`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_window_display_message_warns_on_tmux_error_9c99c2acac | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_window_flags_field_returns_string | ``window_flags`` summarizes window state and reads as a string; fixtures `session`; execute `window.refresh()`; assert `isinstance(window.window_flags, str)`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_window_flags_field_returns_string_3ef969de3e | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_window_format_field_declared_and_hydrated[window_active_clients_list] | Tmux's window-scope format tokens hydrate onto the typed ``Window``; case inputs `field_name='window_active_clients_list'`; fixtures `session`; execute `window.refresh()`; assert `field_name in window.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#est_window_format_field_declared_and_hydrated_window_active_clients_list_94961a7521 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_window_format_field_declared_and_hydrated[window_active_sessions_list] | Tmux's window-scope format tokens hydrate onto the typed ``Window``; case inputs `field_name='window_active_sessions_list'`; fixtures `session`; execute `window.refresh()`; assert `field_name in window.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#st_window_format_field_declared_and_hydrated_window_active_sessions_list_1eaa504749 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_window_format_field_declared_and_hydrated[window_activity_flag] | Tmux's window-scope format tokens hydrate onto the typed ``Window``; case inputs `field_name='window_activity_flag'`; fixtures `session`; execute `window.refresh()`; assert `field_name in window.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#w_py_test_window_format_field_declared_and_hydrated_window_activity_flag_6f9534f5b6 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_window_format_field_declared_and_hydrated[window_bell_flag] | Tmux's window-scope format tokens hydrate onto the typed ``Window``; case inputs `field_name='window_bell_flag'`; fixtures `session`; execute `window.refresh()`; assert `field_name in window.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#indow_py_test_window_format_field_declared_and_hydrated_window_bell_flag_b530d90b78 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_window_format_field_declared_and_hydrated[window_bigger] | Tmux's window-scope format tokens hydrate onto the typed ``Window``; case inputs `field_name='window_bigger'`; fixtures `session`; execute `window.refresh()`; assert `field_name in window.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#t_window_py_test_window_format_field_declared_and_hydrated_window_bigger_b2db8ac5ed | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_window_format_field_declared_and_hydrated[window_end_flag] | Tmux's window-scope format tokens hydrate onto the typed ``Window``; case inputs `field_name='window_end_flag'`; fixtures `session`; execute `window.refresh()`; assert `field_name in window.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#window_py_test_window_format_field_declared_and_hydrated_window_end_flag_2e1a1a7868 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_window_format_field_declared_and_hydrated[window_flags] | Tmux's window-scope format tokens hydrate onto the typed ``Window``; case inputs `field_name='window_flags'`; fixtures `session`; execute `window.refresh()`; assert `field_name in window.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#st_window_py_test_window_format_field_declared_and_hydrated_window_flags_58713f2850 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_window_format_field_declared_and_hydrated[window_format] | Tmux's window-scope format tokens hydrate onto the typed ``Window``; case inputs `field_name='window_format'`; fixtures `session`; execute `window.refresh()`; assert `field_name in window.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#t_window_py_test_window_format_field_declared_and_hydrated_window_format_2ddf445c63 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_window_format_field_declared_and_hydrated[window_last_flag] | Tmux's window-scope format tokens hydrate onto the typed ``Window``; case inputs `field_name='window_last_flag'`; fixtures `session`; execute `window.refresh()`; assert `field_name in window.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#indow_py_test_window_format_field_declared_and_hydrated_window_last_flag_dc04b5fc3c | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_window_format_field_declared_and_hydrated[window_silence_flag] | Tmux's window-scope format tokens hydrate onto the typed ``Window``; case inputs `field_name='window_silence_flag'`; fixtures `session`; execute `window.refresh()`; assert `field_name in window.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#ow_py_test_window_format_field_declared_and_hydrated_window_silence_flag_8d06c7bef7 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_window_format_field_declared_and_hydrated[window_start_flag] | Tmux's window-scope format tokens hydrate onto the typed ``Window``; case inputs `field_name='window_start_flag'`; fixtures `session`; execute `window.refresh()`; assert `field_name in window.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#ndow_py_test_window_format_field_declared_and_hydrated_window_start_flag_8a06735207 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_window_format_field_declared_and_hydrated[window_visible_layout] | Tmux's window-scope format tokens hydrate onto the typed ``Window``; case inputs `field_name='window_visible_layout'`; fixtures `session`; execute `window.refresh()`; assert `field_name in window.__dataclass_fields__`; `value is None or isinstance(value, str)`. | real-tmux integration test | PythonBehaviorParityTest#_py_test_window_format_field_declared_and_hydrated_window_visible_layout_a78ac844e1 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_window_refresh_raises_when_window_id_is_none | ``Window.refresh()`` raises ``ValueError`` when ``window_id`` is unset; fixtures `session`; execute `window.refresh()`; `Window(server=session.server)`; expect `pytest.raises(ValueError, match='window_id')`; assert `window.window_id is None`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_window_refresh_raises_when_window_id_is_none_45ce538830 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_window_rename[rename_with_escapes] | Test Window.rename_window(); case inputs `test_id='rename_with_escapes'`, `window_name_after='hello \\\\ wazzup 0'`, `window_name_before='test'`, `window_name_input='hello \\ wazzup 0'`; fixtures `session`; execute `session.set_option('automatic-rename', 'off', scope=None)`; `session.new_window(window_name=window_name_before, attach=True)`; `window.rename_window(window_name_input)`; assert `window == session.active_window`; `window.window_name == window_name_before`; `window.window_name == window_name_after`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_window_rename_rename_with_escapes_69f5d0ecce | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_window_rename[rename_with_spaces] | Test Window.rename_window(); case inputs `test_id='rename_with_spaces'`, `window_name_after='ha ha ha fjewlkjflwef'`, `window_name_before='test'`, `window_name_input='ha ha ha fjewlkjflwef'`; fixtures `session`; execute `session.set_option('automatic-rename', 'off', scope=None)`; `session.new_window(window_name=window_name_before, attach=True)`; `window.rename_window(window_name_input)`; assert `window == session.active_window`; `window.window_name == window_name_before`; `window.window_name == window_name_after`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_window_rename_rename_with_spaces_c12af2abc5 | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_window_search_panes_filter_by_id | ``Window.search_panes(filter=...)`` returns only the matching pane id; fixtures `session`; execute `window.split()`; `window.search_panes(filter=f'#{{m:{target.pane_id},#{{pane_id}}}}')`; assert `[p.pane_id for p in matches] == [target.pane_id]`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_window_search_panes_filter_by_id_4dd56628de | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_window_search_panes_no_filter_equivalent_to_property | ``search_panes()`` with no filter matches the existing ``panes`` property; fixtures `session`; execute `window.split()`; `window.search_panes()`; assert `from_property == from_method`. | real-tmux integration test | PythonBehaviorParityTest#test_window_py_test_window_search_panes_no_filter_equivalent_to_property_e52428dc6e | 3.3a through 3.7c | planned parity | +| tests/test_window.py::test_window_zoomed_flag_field_toggle | ``window.window_zoomed_flag`` reflects tmux's zoom state across refresh; fixtures `session`; execute `window.split()`; `window.refresh()`; `pane.resize(zoom=True)`; assert `window.window_zoomed_flag == '0'`; `pane is not None`; `window.window_zoomed_flag == '1'`. | real-tmux integration test | PythonBehaviorParityTest#tests_test_window_py_test_window_zoomed_flag_field_toggle_d1301f3b2f | 3.3a through 3.7c | planned parity | diff --git a/docs/reference/operations.md b/docs/reference/operations.md index 2524c53a1..1d086deec 100644 --- a/docs/reference/operations.md +++ b/docs/reference/operations.md @@ -158,6 +158,7 @@ Generated from `@Operation`-annotated methods by the operation-catalog Doclet; d | `io.github.libtmux.Server` | `environment()` | CAPTURED | The server's environment, which every session inherits and every new process is given. | | `io.github.libtmux.Server` | `hooks()` | CAPTURED | The global hooks every session inherits. | | `io.github.libtmux.Server` | `version()` | READ | Which tmux this server is running. | +| `io.github.libtmux.Server` | `requireCompatibleTmux()` | READ | Requires a compatible daemon, or a compatible executable when this endpoint is unused. | | `io.github.libtmux.Server` | `identity()` | CAPTURED | Which server this is. | | `io.github.libtmux.Server` | `open(io.github.libtmux.ServerConfig)` | LIFECYCLE | A server over a transport it owns and closes. | | `io.github.libtmux.Server` | `using(io.github.libtmux.ServerConfig, io.github.libtmux.transport.TmuxTransport)` | LIFECYCLE | A server over a transport the caller owns. | diff --git a/docs/src/test/java/io/github/libtmux/docs/Documentation.java b/docs/src/test/java/io/github/libtmux/docs/Documentation.java index 14b571029..d4d73f391 100644 --- a/docs/src/test/java/io/github/libtmux/docs/Documentation.java +++ b/docs/src/test/java/io/github/libtmux/docs/Documentation.java @@ -88,6 +88,9 @@ private static Snippet.Expectation expectationOf(String directive, Path file) { if (directive.startsWith("does-not-compile:") && directive.length() > "does-not-compile:".length()) { return Snippet.Expectation.DOES_NOT_COMPILE; } + if (directive.startsWith("control:") && directive.length() > "control:".length()) { + return Snippet.Expectation.CONTROL; + } if (directive.startsWith("throws:") && directive.length() > "throws:".length()) { return Snippet.Expectation.THROWS; } @@ -101,7 +104,7 @@ private static Snippet.Expectation expectationOf(String directive, Path file) { // exists to prevent. Naming the file because a typo here is silent otherwise. throw new IllegalArgumentException("unknown snippet directive '" + directive + "' in " + file + "; expected 'does-not-compile: ', 'throws: ', 'compile-only: '" - + " or 'skip: '"); + + ", 'control: ' or 'skip: '"); } /** diff --git a/docs/src/test/java/io/github/libtmux/docs/DocumentationFactsTest.java b/docs/src/test/java/io/github/libtmux/docs/DocumentationFactsTest.java index 177106a69..eb9ef2b3e 100644 --- a/docs/src/test/java/io/github/libtmux/docs/DocumentationFactsTest.java +++ b/docs/src/test/java/io/github/libtmux/docs/DocumentationFactsTest.java @@ -48,6 +48,8 @@ final class DocumentationFactsTest { "libtmux-jackson", "libtmux-junit5", "libtmux-kotlin", + "libtmux-clojure", + "libtmux-clojure-core-async", "libtmux-mcp", "libtmux-workspace", "libtmux-workspace-cli", @@ -68,11 +70,11 @@ final class DocumentationFactsTest { Pattern.compile("libtmux[a-z0-9-]*\\s*([0-9][^<]*)"), Pattern.compile("io\\.github\\.libtmux:[a-z0-9-]+ -> ([0-9][^\\s)]*)")); - /** Fences this build compiles and runs. A fence in any other source language is not checked. */ - private static final Set EXECUTED = Set.of("java", "kotlin"); + /** Fences an owned documentation gate compiles and runs. */ + private static final Set EXECUTED = Set.of("java", "kotlin", "clojure"); /** Source languages a guide may reasonably carry, whether or not anything here builds them. */ - private static final Set SOURCE = Set.of("java", "kotlin", "scala", "groovy"); + private static final Set SOURCE = Set.of("java", "kotlin", "clojure", "scala", "groovy"); private static String read(String path) { try { diff --git a/docs/src/test/java/io/github/libtmux/docs/DocumentationSnippetsTest.java b/docs/src/test/java/io/github/libtmux/docs/DocumentationSnippetsTest.java index 3fb1d99ea..53e7c754f 100644 --- a/docs/src/test/java/io/github/libtmux/docs/DocumentationSnippetsTest.java +++ b/docs/src/test/java/io/github/libtmux/docs/DocumentationSnippetsTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -10,7 +11,9 @@ import io.github.libtmux.Pane; import io.github.libtmux.Server; import io.github.libtmux.Session; +import io.github.libtmux.TmuxVersion; import io.github.libtmux.Window; +import io.github.libtmux.exception.UnsupportedFeatureException; import io.github.libtmux.junit5.TmuxExtension; import io.github.libtmux.junit5.TmuxSocketPath; import java.io.IOException; @@ -141,7 +144,17 @@ void theSnippetIsWhatItClaimsToBe(Snippet snippet, Server server, TmuxSocketPath snippet.where() + " failed with the wrong thing: " + thrown); return; } - if (snippet.expectation() == Snippet.Expectation.RUNS) { + if (snippet.expectation() == Snippet.Expectation.CONTROL) { + TmuxVersion version = server.version(); + if (!io.github.libtmux.internal.ControlAcquisition.supports(version)) { + assertThrows( + UnsupportedFeatureException.class, () -> compiler.run(compiled, bindings(server, socket))); + assertTrue(server.clients().isEmpty(), "a refused snippet attached a client"); + return; + } + } + if (snippet.expectation() == Snippet.Expectation.RUNS + || snippet.expectation() == Snippet.Expectation.CONTROL) { compiler.run(compiled, bindings(server, socket)); } } finally { diff --git a/docs/src/test/java/io/github/libtmux/docs/Snippet.java b/docs/src/test/java/io/github/libtmux/docs/Snippet.java index 252d8e1ce..c7d2ed8b4 100644 --- a/docs/src/test/java/io/github/libtmux/docs/Snippet.java +++ b/docs/src/test/java/io/github/libtmux/docs/Snippet.java @@ -24,6 +24,9 @@ enum Expectation { */ RUNS, + /** Runs on tmux 3.7+; older daemons must refuse control before attachment. */ + CONTROL, + /** * Must compile, but is not run, and the directive has to say why. * diff --git a/examples/README.md b/examples/README.md index c9452dab1..46d11a197 100644 --- a/examples/README.md +++ b/examples/README.md @@ -24,6 +24,9 @@ they are the part of a project nobody compiles and everybody reads first. | [`ResourceBoundaries`](src/main/scala/io/github/libtmux/scaladsl/examples/ResourceBoundaries.scala) | Scala, Cats: borrowing a Java client, and cancelling one dispatched wait | | [`WatchWithOx`](src/main/scala/io/github/libtmux/scaladsl/examples/WatchWithOx.scala) | Scala, Ox: a live view as a `Flow` in a supervised fork, watching a window appear | +Control and live-view examples require tmux 3.7+ and a carrier-owned +pinned socket. Other examples retain the core 3.3a minimum. + ## Run one The [common API programs](api/README.md) provide separate complete Java, diff --git a/examples/api/README.md b/examples/api/README.md index 6eb46ca6d..d7e2e3458 100644 --- a/examples/api/README.md +++ b/examples/api/README.md @@ -34,7 +34,7 @@ their effects through `IOApp`; constructing an `IO` alone does not run it. ## Minimal consumer -Use JDK 25, Git, and tmux 3.2a through 3.7c on Linux. The source checkout +Use JDK 25, Git, and tmux 3.3a through 3.7c on Linux. The source checkout supplies the Gradle wrapper and library dependencies. Save the chosen variant's `settings.gradle.kts` and `build.gradle.kts`, the shared `gradle.properties` and `run.sh`, and one complete program under `src/main/java`, `src/main/kotlin`, diff --git a/examples/src/test/java/io/github/libtmux/examples/ExamplesRunTest.java b/examples/src/test/java/io/github/libtmux/examples/ExamplesRunTest.java index a43d1c0a9..da0746012 100644 --- a/examples/src/test/java/io/github/libtmux/examples/ExamplesRunTest.java +++ b/examples/src/test/java/io/github/libtmux/examples/ExamplesRunTest.java @@ -3,11 +3,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; import io.github.libtmux.Pane; import io.github.libtmux.Server; import io.github.libtmux.control.ControlEvent; import io.github.libtmux.control.PaneOutput; +import io.github.libtmux.internal.ControlAcquisition; import io.github.libtmux.junit5.TmuxExtension; import io.github.libtmux.junit5.TmuxSocketPath; import java.time.Duration; @@ -78,7 +80,8 @@ void aPrintedLineIsFoundBehindAModeSwitchButNotInTheTypedCommand() { } @Test - void watchingAPaneSeesWhatItPrints(TmuxSocketPath socket) { + void watchingAPaneSeesWhatItPrints(Server server, TmuxSocketPath socket) { + assumeTrue(ControlAcquisition.supports(server.version()), "managed control needs tmux 3.7 or newer"); List seen = WatchPaneOutput.run(socket.path(), Duration.ofSeconds(30), output -> {}); assertTrue(WatchPaneOutput.sawTheEcho(seen), "attaching is what makes tmux push output: " + seen); @@ -114,7 +117,8 @@ void servingOverMcpReportsTheFixedToolSurface(TmuxSocketPath socket) { } @Test - void watchingAServerIsToldWhenAWindowAppears(TmuxSocketPath socket) { + void watchingAServerIsToldWhenAWindowAppears(Server server, TmuxSocketPath socket) { + assumeTrue(ControlAcquisition.supports(server.version()), "managed control needs tmux 3.7 or newer"); List seen = WatchWhatChanges.run(socket.path(), Duration.ofSeconds(30), event -> {}); assertTrue( diff --git a/examples/src/test/java/io/github/libtmux/examples/MainsTest.java b/examples/src/test/java/io/github/libtmux/examples/MainsTest.java index 2fee3e139..55f0b99b6 100644 --- a/examples/src/test/java/io/github/libtmux/examples/MainsTest.java +++ b/examples/src/test/java/io/github/libtmux/examples/MainsTest.java @@ -6,8 +6,10 @@ import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assumptions.assumeTrue; import io.github.libtmux.Server; +import io.github.libtmux.internal.ControlAcquisition; import io.github.libtmux.junit5.TmuxExtension; import io.github.libtmux.junit5.TmuxSocketPath; import java.io.IOException; @@ -84,21 +86,24 @@ void serveTmuxOverMcp(TmuxSocketPath socket) throws Exception { } @Test - void watchPaneOutput(TmuxSocketPath socket) throws Exception { + void watchPaneOutput(Server server, TmuxSocketPath socket) throws Exception { + assumeTrue(ControlAcquisition.supports(server.version()), "managed control needs tmux 3.7 or newer"); String out = launch("WatchPaneOutput", socket.path().toString()); assertTrue(WatchPaneOutput.printedLine(out, "watched"), out); } @Test - void watchWhatChanges(TmuxSocketPath socket) throws Exception { + void watchWhatChanges(Server server, TmuxSocketPath socket) throws Exception { + assumeTrue(ControlAcquisition.supports(server.version()), "managed control needs tmux 3.7 or newer"); String out = launch("WatchWhatChanges", socket.path().toString()); assertTrue(out.contains("watched-into-existence"), out); } @Test - void watchWithFlow(TmuxSocketPath socket) throws Exception { + void watchWithFlow(Server server, TmuxSocketPath socket) throws Exception { + assumeTrue(ControlAcquisition.supports(server.version()), "managed control needs tmux 3.7 or newer"); String out = launch("WatchWithFlowKt", socket.path().toString()); assertEquals("echoed=true\ncancelled=true\n", out); diff --git a/examples/src/test/scala/io/github/libtmux/scaladsl/examples/RunnableExamplesSuite.scala b/examples/src/test/scala/io/github/libtmux/scaladsl/examples/RunnableExamplesSuite.scala index ef9e09dc1..9e9f417f9 100644 --- a/examples/src/test/scala/io/github/libtmux/scaladsl/examples/RunnableExamplesSuite.scala +++ b/examples/src/test/scala/io/github/libtmux/scaladsl/examples/RunnableExamplesSuite.scala @@ -57,6 +57,9 @@ final class RunnableExamplesSuite extends FunSuite { test("runs actual main: " + name.stripPrefix(packageName)) { assert(expectedMains(name), "unregistered executable example") OwnedTmux.use { fixture => + // Watching is managed control, which needs tmux 3.7 or newer. + if (Set("ObserveChanges", "WatchWithOx")(name.stripPrefix(packageName))) + assume(fixture.supportsControl, "managed control needs tmux 3.7+") val sessionsBefore = fixture.server.sessions().asScala.map(_.id()).toVector val clientsBefore = diff --git a/gradle/clojure-runner/agent/libtmux/internal/loadguard/LoadGuard.java b/gradle/clojure-runner/agent/libtmux/internal/loadguard/LoadGuard.java new file mode 100644 index 000000000..192760e40 --- /dev/null +++ b/gradle/clojure-runner/agent/libtmux/internal/loadguard/LoadGuard.java @@ -0,0 +1,43 @@ +package libtmux.internal.loadguard; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Bootstrap-visible checkpoint used only by the namespace-load test agent. */ +public final class LoadGuard { + private static final List violations = new ArrayList<>(); + private static Map coverage = Map.of(); + private static boolean armed; + + private LoadGuard() {} + + public static synchronized void install(Map intercepted) { + coverage = Map.copyOf(intercepted); + } + + public static synchronized void begin() { + if (coverage.isEmpty() || armed) { + throw new IllegalStateException("namespace load guard is unavailable or already armed"); + } + violations.clear(); + armed = true; + } + + public static synchronized List end() { + armed = false; + return List.copyOf(violations); + } + + public static synchronized Map coverage() { + return coverage; + } + + public static synchronized void check(String kind, String site) { + if (armed) { + String violation = kind + ":" + site; + violations.add(violation); + throw new IllegalStateException("NAMESPACE_LOAD_EFFECT: " + violation); + } + } +} diff --git a/gradle/clojure-runner/agent/libtmux/internal/loadguard/NamespaceAgent.java b/gradle/clojure-runner/agent/libtmux/internal/loadguard/NamespaceAgent.java new file mode 100644 index 000000000..7e323c9d0 --- /dev/null +++ b/gradle/clojure-runner/agent/libtmux/internal/loadguard/NamespaceAgent.java @@ -0,0 +1,121 @@ +package libtmux.internal.loadguard; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.Instrumentation; +import java.security.ProtectionDomain; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.jar.JarFile; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +/** Rejects resource creation while the native runner requires public namespaces. */ +public final class NamespaceAgent { + private static final String GUARD = "libtmux/internal/loadguard/LoadGuard"; + private static final Map TARGETS = Map.ofEntries( + Map.entry("java/util/concurrent/ThreadPoolExecutor", "executor"), + Map.entry("java/util/concurrent/ForkJoinPool", "executor"), + Map.entry("java/util/concurrent/ThreadPerTaskExecutor", "executor"), + Map.entry("java/lang/Thread", "thread"), + Map.entry("java/lang/VirtualThread", "thread"), + Map.entry("java/lang/ProcessBuilder", "process"), + Map.entry("java/net/Socket", "socket"), + Map.entry("java/net/ServerSocket", "socket"), + Map.entry("java/net/DatagramSocket", "socket"), + Map.entry("sun/nio/ch/SocketChannelImpl", "socket"), + Map.entry("sun/nio/ch/ServerSocketChannelImpl", "socket"), + Map.entry("sun/nio/ch/DatagramChannelImpl", "socket")); + + private NamespaceAgent() {} + + public static void premain(String bootstrapJars, Instrumentation instrumentation) throws Exception { + if (!instrumentation.isRetransformClassesSupported()) { + throw new IllegalStateException("namespace load guard requires class retransformation"); + } + if (bootstrapJars == null || bootstrapJars.isBlank()) { + throw new IllegalStateException("namespace load guard needs bootstrap helper and ASM JARs"); + } + var location = NamespaceAgent.class.getProtectionDomain().getCodeSource().getLocation().toURI(); + String[] jars = bootstrapJars.split(java.util.regex.Pattern.quote(java.io.File.pathSeparator)); + if (jars.length != 2 || jars[0].isBlank() || jars[1].isBlank()) { + throw new IllegalStateException("namespace load guard needs bootstrap helper and ASM JARs"); + } + for (String jar : jars) { + instrumentation.appendToBootstrapClassLoaderSearch(new JarFile(jar)); + } + Class guard = Class.forName(GUARD.replace('/', '.'), true, null); + instrumentation.redefineModule( + Object.class.getModule(), Set.of(guard.getModule()), Map.of(), Map.of(), Set.of(), Map.of()); + Map coverage = new HashMap<>(); + instrumentation.addTransformer(new ClassFileTransformer() { + @Override + public byte[] transform( + Module module, + ClassLoader loader, + String className, + Class redefining, + ProtectionDomain domain, + byte[] bytes) { + String kind = className == null ? null : TARGETS.get(className); + if (kind == null) { + return null; + } + ClassReader reader = new ClassReader(bytes); + ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS); + int[] count = {0}; + reader.accept(new ClassVisitor(Opcodes.ASM9, writer) { + @Override + public MethodVisitor visitMethod( + int access, String name, String descriptor, String signature, String[] exceptions) { + MethodVisitor original = super.visitMethod(access, name, descriptor, signature, exceptions); + boolean target = switch (kind) { + case "thread" -> name.equals("start"); + case "process" -> name.equals("start") || name.equals("startPipeline"); + default -> name.equals(""); + }; + if (!target || (access & (Opcodes.ACC_NATIVE | Opcodes.ACC_ABSTRACT)) != 0) { + return original; + } + count[0]++; + return new MethodVisitor(Opcodes.ASM9, original) { + @Override + public void visitCode() { + super.visitCode(); + visitLdcInsn(kind); + visitLdcInsn(className + "." + name + descriptor); + visitMethodInsn( + Opcodes.INVOKESTATIC, + GUARD, + "check", + "(Ljava/lang/String;Ljava/lang/String;)V", + false); + } + }; + } + }, 0); + byte[] transformed = writer.toByteArray(); + coverage.put(className, count[0]); + return transformed; + } + }, true); + for (String target : TARGETS.keySet()) { + Class type = Class.forName(target.replace('/', '.'), false, null); + if (!instrumentation.isModifiableClass(type)) { + throw new IllegalStateException("namespace load guard cannot transform " + target); + } + coverage.remove(target); + instrumentation.retransformClasses(type); + if (coverage.getOrDefault(target, 0) == 0) { + throw new IllegalStateException("namespace load guard intercepted no methods on " + target); + } + } + guard.getMethod("install", Map.class).invoke(null, coverage); + System.setProperty( + "libtmux.clojure.guard.agent", "-javaagent:" + new java.io.File(location) + "=" + bootstrapJars); + System.setProperty("libtmux.clojure.guard.classpath", new java.io.File(location).toString()); + } +} diff --git a/gradle/clojure-runner/agent/libtmux/internal/loadguard/ResourceProbe.java b/gradle/clojure-runner/agent/libtmux/internal/loadguard/ResourceProbe.java new file mode 100644 index 000000000..e1cad7791 --- /dev/null +++ b/gradle/clojure-runner/agent/libtmux/internal/loadguard/ResourceProbe.java @@ -0,0 +1,49 @@ +package libtmux.internal.loadguard; + +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.Socket; +import java.net.StandardProtocolFamily; +import java.nio.channels.DatagramChannel; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.concurrent.Executors; +import java.util.concurrent.ForkJoinPool; + +/** Isolates JDK class initialization while checking each additional interception path. */ +public final class ResourceProbe { + private ResourceProbe() {} + + @SuppressWarnings("try") // Construction is the probe; close resources if interception fails. + public static void main(String[] arguments) throws Exception { + LoadGuard.begin(); + Throwable rejected = null; + try { + switch (arguments[0]) { + case "fork-join" -> { try (var resource = new ForkJoinPool(1)) {} } + case "virtual-executor" -> { try (var resource = Executors.newVirtualThreadPerTaskExecutor()) {} } + case "thread" -> { + var thread = new Thread(() -> {}); + thread.start(); + thread.join(); + } + case "virtual-thread" -> Thread.startVirtualThread(() -> {}).join(); + case "client-socket" -> { try (var resource = new Socket()) {} } + case "datagram" -> { try (var resource = new DatagramSocket(0, InetAddress.getLoopbackAddress())) {} } + case "nio-socket" -> { try (var resource = SocketChannel.open()) {} } + case "unix-server-socket" -> { + try (var resource = ServerSocketChannel.open(StandardProtocolFamily.UNIX)) {} + } + case "nio-datagram" -> { try (var resource = DatagramChannel.open()) {} } + default -> throw new IllegalArgumentException("unknown resource probe"); + } + } catch (Throwable failure) { + rejected = failure; + } + var effects = LoadGuard.end(); + if (effects.isEmpty()) { + throw new AssertionError("namespace guard did not reject " + arguments[0], rejected); + } + System.out.println("NAMESPACE_LOAD_EFFECTS " + effects); + } +} diff --git a/gradle/clojure-runner/libtmux/internal/guard_probe.clj b/gradle/clojure-runner/libtmux/internal/guard_probe.clj new file mode 100644 index 000000000..16e97a1a6 --- /dev/null +++ b/gradle/clojure-runner/libtmux/internal/guard_probe.clj @@ -0,0 +1,15 @@ +(ns libtmux.internal.guard-probe + (:import [java.net InetAddress ServerSocket] + [java.util.concurrent ExecutorService Executors TimeUnit])) + +(case (System/getProperty "libtmux.clojure.guard-probe") + "executor" (with-open [^ExecutorService executor (Executors/newSingleThreadExecutor)] nil) + "process" (let [^java.util.List argv [(str (System/getProperty "java.home") "/bin/java") "-version"] + process (.start (ProcessBuilder. argv))] + (try + (.readAllBytes (.getErrorStream process)) + (when-not (.waitFor process 900 TimeUnit/MILLISECONDS) + (throw (ex-info "guard probe child did not exit" {}))) + (finally (.destroyForcibly process)))) + "socket" (with-open [socket (ServerSocket. 0 0 (InetAddress/getLoopbackAddress))] nil) + nil) diff --git a/gradle/clojure-runner/libtmux/internal/test_runner.clj b/gradle/clojure-runner/libtmux/internal/test_runner.clj new file mode 100644 index 000000000..667b2e065 --- /dev/null +++ b/gradle/clojure-runner/libtmux/internal/test_runner.clj @@ -0,0 +1,126 @@ +(ns libtmux.internal.test-runner + (:require [clojure.java.io :as io] + [clojure.string :as str] + [clojure.test :as test]) + (:import [java.io File] + [java.lang.reflect Method] + [java.util.concurrent TimeUnit])) + +(defn- configured-namespaces [property] + (let [names (some-> (System/getProperty property) (str/split #","))] + (when-not (seq names) + (throw (ex-info (str "missing namespace list: " property) {:property property}))) + (mapv symbol names))) + +(defn- source-namespaces [roots] + (let [namespaces + (mapcat + (fn [root] + (let [^File directory (io/file root)] + (when-not (.isDirectory directory) + (throw (ex-info "Clojure test source root does not exist" {:root root}))) + (let [names (->> (file-seq directory) + (filter (fn [^File source] + (and (.isFile source) + (str/ends-with? (.getName source) "_test.clj")))) + (map (fn [^File source] + (-> (str (.relativize (.toPath directory) (.toPath source))) + (str/replace File/separator ".") + (str/replace "_" "-") + (str/replace #"\.clj$" "") + symbol))) + sort + vec)] + (when (empty? names) + (throw (ex-info "Clojure test source root contains no test namespaces" {:root root}))) + names))) + roots)] + (when-not (= (count namespaces) (count (set namespaces))) + (throw (ex-info "Clojure test source roots contain duplicate namespaces" + {:roots roots}))) + (vec namespaces))) + +(defn- require-without-reflection [namespaces] + (let [warnings (java.io.StringWriter.)] + (binding [*warn-on-reflection* true + *err* warnings] + (doseq [namespace namespaces] + (require namespace :reload))) + (when-not (str/blank? (str warnings)) + (throw (ex-info "Clojure namespace load emitted compiler warnings" + {:warnings (str warnings)}))))) + +(defn- run-tests! [namespaces] + (require-without-reflection namespaces) + (let [{:keys [test fail error]} (apply test/run-tests namespaces)] + (when (zero? test) + (throw (ex-info "Clojure runner discovered no tests" {:namespaces namespaces}))) + (when (pos? (+ fail error)) + (throw (ex-info "Clojure tests failed" {:fail fail :error error}))))) + +(defn- guarded-load! [namespaces] + (let [^Class guard (try (Class/forName "libtmux.internal.loadguard.LoadGuard") + (catch ClassNotFoundException cause + (throw (ex-info "Namespace loading requires the internal load-guard agent" {} cause)))) + ^Method begin (.getMethod guard "begin" (make-array Class 0)) + ^Method end (.getMethod guard "end" (make-array Class 0)) + ^Method coverage (.getMethod guard "coverage" (make-array Class 0))] + (.invoke begin nil (object-array 0)) + (let [failure (try (require-without-reflection namespaces) nil + (catch Throwable thrown thrown)) + effects (vec (.invoke end nil (object-array 0)))] + (when (seq effects) + (binding [*out* *err*] (println "NAMESPACE_LOAD_EFFECTS" (pr-str effects))) + (throw (ex-info "Namespace load created a forbidden resource" {:effects effects} failure))) + (when failure (throw failure)) + (println "Namespace load guard:" (into (sorted-map) (.invoke coverage nil (object-array 0))))))) + +(defn- guard-probes! [] + (let [agent (System/getProperty "libtmux.clojure.guard.agent")] + (when-not agent + (throw (ex-info "Namespace guard probes require the internal agent" {}))) + (let [property "libtmux.clojure.guard-probe" previous (System/getProperty property)] + (try + (doseq [probe ["executor" "process" "socket"]] + (System/setProperty property probe) + (let [failure (try (guarded-load! '[libtmux.internal.guard-probe]) nil + (catch clojure.lang.ExceptionInfo thrown thrown))] + (when-not (seq (:effects (ex-data failure))) + (throw (ex-info "Namespace guard did not reject the injected resource" {:probe probe} failure))) + (println "Namespace guard rejected:" probe))) + (finally + (if previous (System/setProperty property previous) (System/clearProperty property))))) + (doseq [probe ["fork-join" "virtual-executor" "thread" "virtual-thread" + "client-socket" "datagram" "nio-socket" + "unix-server-socket" "nio-datagram"]] + (let [^java.util.List argv + [(str (System/getProperty "java.home") "/bin/java") + "-XX:TieredStopAtLevel=1" "-Xshare:off" agent + "-cp" (System/getProperty "libtmux.clojure.guard.classpath") + "libtmux.internal.loadguard.ResourceProbe" probe] + process (.start (.redirectErrorStream (ProcessBuilder. argv) true))] + (try + (when-not (.waitFor process 900 TimeUnit/MILLISECONDS) + (throw (ex-info "Namespace guard probe did not finish within its budget" {:probe probe}))) + (let [output (slurp (.getInputStream process))] + (when-not (and (zero? (.exitValue process)) + (str/includes? output "NAMESPACE_LOAD_EFFECTS")) + (throw (ex-info "Namespace guard did not reject the injected resource" + {:probe probe :output output}))) + (println "Namespace guard rejected:" probe + (first (filter #(str/starts-with? % "NAMESPACE_LOAD_EFFECTS") + (str/split-lines output))))) + (finally + (when (.isAlive process) + (.destroyForcibly process) + (.waitFor process 900 TimeUnit/MILLISECONDS)))))))) + +(defn -main [& [mode & roots]] + (case mode + "load" (guarded-load! + (configured-namespaces "libtmux.clojure.main-namespaces")) + "guard-probes" (guard-probes!) + "test" (run-tests! (if (seq roots) + (source-namespaces roots) + (configured-namespaces "libtmux.clojure.test-namespaces"))) + (throw (ex-info "expected runner mode load, guard-probes, or test" {:mode mode})))) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bb90071a7..b47384603 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,6 +17,9 @@ maven-publish-plugin = "0.37.0" palantir-format = "2.97.0" cyclonedx-plugin = "3.4.1" japicmp = "0.23.1" +clojure = "1.12.4" +core-async = "1.8.741" +manifold = "0.5.0" picocli = "4.7.7" reactive-streams = "1.0.4" snakeyaml = "2.5" @@ -53,6 +56,9 @@ asm = { module = "org.ow2.asm:asm", version = "9.9.1" } errorprone-core = { module = "com.google.errorprone:error_prone_core", version.ref = "errorprone" } nullaway = { module = "com.uber.nullaway:nullaway", version.ref = "nullaway" } japicmp = { module = "com.github.siom79.japicmp:japicmp", version.ref = "japicmp" } +clojure = { module = "org.clojure:clojure", version.ref = "clojure" } +core-async = { module = "org.clojure:core.async", version.ref = "core-async" } +manifold = { module = "manifold:manifold", version.ref = "manifold" } picocli = { module = "info.picocli:picocli", version.ref = "picocli" } reactive-streams-tck-flow = { module = "org.reactivestreams:reactive-streams-tck-flow", version.ref = "reactive-streams" } snakeyaml = { module = "org.yaml:snakeyaml", version.ref = "snakeyaml" } diff --git a/gradle/test-fixtures/io/github/libtmux/fixtures/pty.py b/gradle/test-fixtures/io/github/libtmux/fixtures/pty.py new file mode 100644 index 000000000..20fceda7f --- /dev/null +++ b/gradle/test-fixtures/io/github/libtmux/fixtures/pty.py @@ -0,0 +1,21 @@ +import errno, os, select, subprocess, time +def drain_pty(fd): + chunks = [] + while select.select([fd], [], [], 0)[0]: + try: chunk = os.read(fd, 65536) + except OSError as error: + if error.errno == errno.EIO: break + raise + if not chunk: break + chunks.append(chunk) + return b''.join(chunks) +def wait_pty(child, master, timeout): + until = time.monotonic() + timeout + while child.poll() is None: + drain_pty(master) + remaining = until - time.monotonic() + if remaining <= 0: + raise subprocess.TimeoutExpired(child.args, timeout) + select.select([master], [], [], min(remaining, .025)) + drain_pty(master) + return child.returncode diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index b7d5cfff4..cb34bd6d0 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -13,6 +13,14 @@ + + + + + + + + @@ -1608,6 +1616,14 @@ + + + + + + + + @@ -2018,6 +2034,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -2478,6 +2608,14 @@ + + + + + + + + @@ -2518,6 +2656,14 @@ + + + + + + + + @@ -2542,6 +2688,14 @@ + + + + + + + + @@ -2574,6 +2728,14 @@ + + + + + + + + @@ -2598,6 +2760,14 @@ + + + + + + + + @@ -2622,6 +2792,14 @@ + + + + + + + + @@ -2654,6 +2832,14 @@ + + + + + + + + @@ -2886,6 +3072,14 @@ + + + + + + + + @@ -3009,6 +3203,14 @@ + + + + + + + + @@ -3149,6 +3351,14 @@ + + + + + + + + @@ -3600,11 +3810,24 @@ + + + + + + + + + + + + + @@ -4332,6 +4555,22 @@ + + + + + + + + + + + + + + + + diff --git a/integration-tests/build.gradle.kts b/integration-tests/build.gradle.kts index ccf734e2e..d5a88ae53 100644 --- a/integration-tests/build.gradle.kts +++ b/integration-tests/build.gradle.kts @@ -12,6 +12,13 @@ plugins { `java-test-fixtures` } +val integrationClojureVersion = + providers.gradleProperty("libtmuxClojureVersion").getOrElse(libs.versions.clojure.get()) + +repositories { + maven { url = uri("https://repo.clojars.org") } +} + dependencies { testImplementation(project(":libtmux")) testImplementation(project(":libtmux-jackson")) @@ -19,6 +26,10 @@ dependencies { testImplementation(project(":libtmux-scala")) testImplementation(project(":libtmux-scala-cats")) testImplementation(project(":libtmux-scala-ox")) + testImplementation(project(":libtmux-clojure")) + testImplementation(project(":libtmux-clojure-core-async")) + testImplementation("org.clojure:clojure:$integrationClojureVersion") + testImplementation(libs.manifold) testFixturesImplementation(project(":libtmux")) testFixturesImplementation(project(":libtmux-junit5")) @@ -31,6 +42,85 @@ providers.gradleProperty("libtmux.scala.fixture.mutant").orNull?.let { mutant -> tasks.withType().configureEach { systemProperty("libtmux.scala.fixture.mutant", mutant) } } +sourceSets.test { + resources.srcDir("src/test/clojure") + resources.srcDir(rootProject.file("gradle/clojure-runner")) + resources.exclude("agent/**") +} + +val clojureIntegrationTest = + tasks.register("clojureIntegrationTest") { + group = "verification" + description = "Runs the native Clojure real-tmux integration suite." + dependsOn(tasks.named("testClasses")) + classpath = sourceSets.test.get().runtimeClasspath + mainClass.set("clojure.main") + args("-m", "libtmux.internal.test-runner", "test") + systemProperty("libtmux.clojure.expected", integrationClojureVersion) + val namespaces = + provider { + fileTree("src/test/clojure") { include("**/*_test.clj") } + .files + .sorted() + .joinToString(",") { source -> + source.relativeTo(file("src/test/clojure")) + .invariantSeparatorsPath + .removeSuffix(".clj") + .replace('_', '-') + .replace('/', '.') + } + } + doFirst { + systemProperty( + "libtmux.clojure.test-namespaces", + providers.gradleProperty("libtmuxClojureTestNamespaces").orNull ?: namespaces.get(), + ) + } + jvmArgs("-Dclojure.main.report=stderr") + javaLauncher.set(javaToolchains.launcherFor { + languageVersion.set( + JavaLanguageVersion.of( + providers.gradleProperty("libtmuxJavaVersion").getOrElse("25").toInt(), + ), + ) + }) + environment.remove("TMUX") + environment.remove("TMUX_PANE") + environment("TMUX_TMPDIR", "/tmp/libtmux-java-test") + systemProperty("libtmux.docs.root", rootProject.projectDir.path) + systemProperty( + "libtmux.tmux", + providers.gradleProperty("libtmuxTmuxBinary").getOrElse("tmux"), + ) + providers.gradleProperty("libtmuxTmuxExpected").orNull?.let { + systemProperty("libtmux.tmux.expected", it) + } + inputs.files( + rootProject.fileTree(rootProject.projectDir) { + include("README.md", "*/README.md", "docs/guide/*.md") + }, + ).withPropertyName("clojureDocumentation") + .withPathSensitivity(PathSensitivity.RELATIVE) + } + +val prepareClojureIntegrationClasspath = + tasks.register("prepareClojureIntegrationClasspath") { + group = "build setup" + description = "Writes the reusable native Clojure integration classpath." + dependsOn(tasks.named("testClasses")) + val destination = layout.buildDirectory.file("clojure/integration-classpath.txt") + inputs.files(sourceSets.test.map { it.runtimeClasspath }) + outputs.file(destination) + doLast { + destination.get().asFile.apply { + parentFile.mkdirs() + writeText(sourceSets.test.get().runtimeClasspath.asPath + "\n") + } + } + } + +tasks.named("check") { dependsOn(clojureIntegrationTest) } + // The locale lane needs a JVM whose platform encoding is not UTF-8, which is a process-wide choice // read before main runs. So it gets its own fork rather than a flag, and the ordinary run excludes // it: under UTF-8 every assertion in it would pass for the wrong reason. @@ -66,3 +156,6 @@ val localeTest = } tasks.check { dependsOn(localeTest) } + +// Shared native terminal driver; test resources never enter a published artifact. +sourceSets.test { resources.srcDir(rootProject.file("gradle/test-fixtures")) } diff --git a/integration-tests/src/test/clojure/libtmux/async_integration_test.clj b/integration-tests/src/test/clojure/libtmux/async_integration_test.clj new file mode 100644 index 000000000..324bb4ed0 --- /dev/null +++ b/integration-tests/src/test/clojure/libtmux/async_integration_test.clj @@ -0,0 +1,90 @@ +(ns libtmux.async-integration-test + (:require [clojure.test :refer [deftest is]] + [libtmux.async :as async] + [libtmux.core :as core] + [libtmux.internal.fixture :as fixture]) + (:import [io.github.libtmux Server WakeReason] + [java.time Duration] + [java.util UUID])) + +(deftest reserved-wait-does-not-head-block-its-release + (fixture/with-owned-server + (fn [{:keys [^Server server]}] + (with-open [^libtmux.async.AsyncRuntime runtime (async/runtime! {:max-running 2})] + (let [channel (.channel server (str "clj-async-" (UUID/randomUUID))) + waiting (async/submit! runtime {:kind :wait} + #(.awaitReservingCapacity channel + (Duration/ofMillis 800))) + release (async/submit! runtime #(.signal channel))] + (is (nil? (deref release 500 ::timeout))) + (is (= WakeReason/SIGNALLED (deref waiting 900 ::timeout)))))))) + + +(deftest saturated-waiting-groups-retain-runtime-and-transport-release-capacity + (fixture/with-owned-server + (fn [{:keys [^Server server]}] + (with-open [transport (io.github.libtmux.transport.ProcessTransport. 2) + shared (Server/using (.config server) transport)] + (doseq [group [core/batch! core/chain!]] + (with-open [^libtmux.async.AsyncRuntime runtime + (async/runtime! {:max-running 2 :queue-limit 1})] + (let [channel (str "group-release-" (UUID/randomUUID)) + started (str channel "-started") + waiting (async/submit! runtime {:kind :wait} + #(group shared [["wait-for" "-S" started] + ["wait-for" channel]] + {:kind :wait}))] + (try + (is (= :signalled (core/await-channel! server started (Duration/ofMillis 800)))) + (let [queued (async/submit! runtime {:kind :wait} + #(group shared [["display-message" "-p" "queued-result"]] + {:kind :wait})) + excess (async/submit! runtime {:kind :wait} + #(throw (AssertionError. "excess waiting work ran")))] + (is (= :queued (async/task-state queued))) + (is (= {:status :busy :kind :wait} excess)) + (let [release (async/submit! runtime #(core/signal-channel! shared channel))] + (is (not (map? release)) "ordinary release lost its reserved admission") + (when-not (map? release) + (is (nil? (deref release 800 ::timeout))) + (is (= [:complete :complete] + (mapv :operation/outcome (deref waiting 800 ::timeout)))) + (let [result (deref queued 800 ::timeout)] + (is (= [:complete] (mapv :operation/outcome result))) + (is (= [["queued-result"]] (mapv :operation/stdout result)))) + (is (= [:terminal :terminal :terminal] + (mapv async/task-state [waiting queued release])))))) + (finally + (core/signal-channel! server channel) + (async/cancel! waiting)))))))))) + +(deftest cancelled-async-groups-retain-positional-data-and-transport-cause + (fixture/with-owned-server + (fn [{:keys [^Server server]}] + (doseq [group [core/batch! core/chain!]] + (with-open [^libtmux.async.AsyncRuntime runtime (async/runtime! {:max-running 2})] + (let [id (str "async-group-cancel-" (UUID/randomUUID)) + ready (str id "-ready") + release (str id "-release") + waiting (async/submit! runtime {:kind :wait} + #(group server [["set-buffer" "-b" id "applied"] + ["wait-for" "-S" ready] + ["wait-for" release] + ["set-buffer" "-b" (str id "-after") "uncertain"]] + {:kind :wait}))] + (try + (is (= :signalled (core/await-channel! server ready (Duration/ofMillis 800)))) + (is (true? (async/cancel! waiting))) + (let [failure (try (deref waiting 800 ::timeout) (catch Exception e e))] + (is (= :cancelled (:tmux/error (ex-data failure)))) + (is (= :unknown (:tmux/dispatch (ex-data failure)))) + (is (= [:unknown :unknown :unknown :unknown] + (mapv :operation/outcome (:tmux/results (ex-data failure))))) + (is (and (instance? Throwable failure) + (some #(instance? io.github.libtmux.exception.DispatchException %) + (take-while some? (iterate #(.getCause ^Throwable %) failure))))) + (is (= :terminal (async/task-state waiting)))) + (is (= "applied" (core/show-buffer! server id))) + (finally + (core/signal-channel! server release) + (async/cancel! waiting))))))))) diff --git a/integration-tests/src/test/clojure/libtmux/control_integration_test.clj b/integration-tests/src/test/clojure/libtmux/control_integration_test.clj new file mode 100644 index 000000000..236e9084d --- /dev/null +++ b/integration-tests/src/test/clojure/libtmux/control_integration_test.clj @@ -0,0 +1,266 @@ +(ns libtmux.control-integration-test + (:require [clojure.string :as string] + [clojure.test :refer [deftest is]] + [libtmux.control :as control] + [libtmux.core :as tmux] + [libtmux.internal.control-gate :as gate] + [libtmux.internal.fixture :as fixture]) + (:import [java.lang AutoCloseable ProcessHandle] + [java.nio.file Files Path] + [java.time Duration] + [java.util.concurrent CountDownLatch TimeUnit])) + +(def ^:private ^Duration limit (Duration/ofMillis 900)) + +(defn- shell-quote [s] (str "'" (string/replace (str s) "'" "'\\''") "'")) + +(defn- pane-event! [stream pane] + (let [deadline (+ (System/nanoTime) (.toNanos limit))] + (loop [] + (let [event (control/next! stream (Duration/ofNanos (max 0 (- deadline (System/nanoTime))))) ] + (when-not (= :event (:stream/status event)) + (throw (ex-info "Expected pane output before terminal or timeout" event))) + (if (= pane (:pane/id event)) event (recur)))))) + +(deftest protocol-reply-precedes-a-deferred-completion-marker + (gate/with-control-server + (fn [{:keys [server socket binary]}] + (let [session (first (tmux/sessions! server)) + marker (.resolveSibling ^Path socket "completed") + command (str (shell-quote binary) " -S " (shell-quote socket))] + (try + (with-open [^AutoCloseable connection (control/attach! (:tmux/ref session))] + (let [reply (control/send! connection + ["run-shell" (str command " wait-for release; : > " + (shell-quote marker) "; " command " wait-for -S finished")])] + (is (= :complete (:reply/status reply))) + (is (= :unverified (:operation/completion reply))) + (is (not (Files/exists marker (make-array java.nio.file.LinkOption 0)))) + (tmux/raw! server ["wait-for" "-S" "release"]) + (tmux/raw! server ["wait-for" "finished"]) + (is (Files/exists marker (make-array java.nio.file.LinkOption 0))) + (is (= ["aligned"] (:reply/lines (control/send! connection ["display-message" "-p" "aligned"])))))) + (finally (Files/deleteIfExists marker))))))) + +(deftest stream-preserves-split-utf8-bytes-and-text + (gate/with-control-server + (fn [{:keys [server]}] + (with-open [^AutoCloseable connection (control/attach! (:tmux/ref (first (tmux/sessions! server)))) + ^AutoCloseable raw (control/subscribe! connection {:capacity 32}) + ^AutoCloseable text (control/subscribe! connection {:encoding :utf-8 :capacity 32})] + (let [reply (control/send! connection + ["new-window" "-P" "-F" "#{pane_id}" + "stty -echo; printf 'READY'; read -r start; printf '\\303'; read -r more; printf '\\251\\377'; read -r finish"]) + pane (first (:reply/lines reply))] + (is (= "READY" (String. ^bytes (:output/bytes (pane-event! raw pane)) "UTF-8"))) + (is (= "READY" (:output/text (pane-event! text pane)))) + (control/send! connection ["send-keys" "-t" pane "Enter"]) + (is (= [195] (mapv #(bit-and 255 %) (:output/bytes (pane-event! raw pane))))) + (control/send! connection ["send-keys" "-t" pane "Enter"]) + (is (= [169 255] (mapv #(bit-and 255 %) (:output/bytes (pane-event! raw pane))))) + ;; The text subscription decodes per pane: the cut character is withheld, then arrives + ;; whole with its tail. + (is (= "" (:output/text (pane-event! text pane)))) + (let [event (pane-event! text pane)] + (is (= "é\\xff" (:output/text event))) + (is (= pane (get-in event [:tmux/identity :tmux/id])))) + (control/close! text) + (is (= :closed (get-in (control/next! text limit) [:stream/terminal :reason]))) + (is (= :complete (:reply/status (control/send! connection ["display-message" "-p" "alive"]))))))))) + +(deftest close-wakes-reader-and-preserves-borrowed-connection + (gate/with-control-server + (fn [{:keys [server]}] + (with-open [^AutoCloseable connection (control/attach! (:tmux/ref (first (tmux/sessions! server)))) + ^AutoCloseable stream (control/subscribe! connection)] + (let [entered (CountDownLatch. 1) + exited (CountDownLatch. 1) + outcome (atom nil) + worker (.start (Thread/ofVirtual) + ^Runnable (fn [] + (.countDown entered) + (try (reset! outcome (control/next! stream)) + (finally (.countDown exited)))))] + (is (.await entered 900 TimeUnit/MILLISECONDS)) + (control/close! stream) + (is (.await exited 900 TimeUnit/MILLISECONDS)) + (.join worker 900) + (is (not (.isAlive worker))) + (is (= :closed (get-in @outcome [:stream/terminal :reason]))) + (is (= (control/terminal stream) (:stream/terminal @outcome))) + (is (= :complete (:reply/status (control/send! connection ["display-message" "-p" "still-alive"]))))))))) + +(deftest stalled-subscription-reports-a-gap-and-keeps-the-newest-events + (gate/with-control-server + (fn [{:keys [server socket binary]}] + (with-open [^AutoCloseable connection (control/attach! (:tmux/ref (first (tmux/sessions! server)))) + ^AutoCloseable anchor (control/subscribe! connection {:capacity 16}) + ^AutoCloseable stalled (control/subscribe! connection {:capacity 2})] + (let [tmux-command (str (shell-quote binary) " -S " (shell-quote socket)) + script (str "stty -echo; printf aa; " tmux-command " wait-for one; " + "printf bbb; " tmux-command " wait-for two; " + "printf cccc; " tmux-command " wait-for three; " + "printf 12345; " tmux-command " wait-for four; printf z; read stop") + pane (first (:reply/lines + (control/send! connection ["new-window" "-P" "-F" "#{pane_id}" script])))] + ;; Reading the anchor after every release proves each push reached the + ;; control reader before the next one can be emitted. + (is (= "aa" (String. ^bytes (:output/bytes (pane-event! anchor pane)) "UTF-8"))) + (doseq [[gate expected] [["one" "bbb"] ["two" "cccc"] + ["three" "12345"] ["four" "z"]]] + (control/send! connection ["wait-for" "-S" gate]) + (is (= expected (String. ^bytes (:output/bytes (pane-event! anchor pane)) "UTF-8")))) + ;; The stalled reader missed the oldest events: first the gap, then what remains. + (let [gap (control/next! stalled limit)] + (is (= :gap (:stream/status gap))) + (is (pos? (:gap/events gap)))) + ;; What the buffer kept is the newest events: drain it, and the last is the final write. + (let [drained (loop [out []] + (let [event (control/next! stalled (Duration/ofMillis 100))] + (if (= :event (:stream/status event)) + (recur (conj out event)) + out))) + text (mapv #(String. ^bytes (:output/bytes %) "UTF-8") + (filter #(= pane (:pane/id %)) drained))] + (is (<= (count drained) 2)) + (is (= "z" (last text)) "the newest event is kept"))))))) + +(deftest pane-death-does-not-end-session-output-stream + (gate/with-control-server + (fn [{:keys [server]}] + (with-open [^AutoCloseable connection (control/attach! (:tmux/ref (first (tmux/sessions! server)))) + ^AutoCloseable stream (control/subscribe! connection)] + (let [anchor (first (:reply/lines + (control/send! connection + ["new-window" "-P" "-F" "#{pane_id}" + "stty -echo; read -r start; printf alive; read -r stop"]))) + pane (first (:reply/lines + (control/send! connection + ["new-window" "-P" "-F" "#{pane_id}" + "stty -echo; printf gone; read -r stop"])))] + (is (= "gone" (String. ^bytes (:output/bytes (pane-event! stream pane)) "UTF-8"))) + ;; Keep the source pane alive until its output arrives. A later + ;; target lookup proves its removal before the anchor emits output. + (is (= :complete (:reply/status (control/send! connection ["kill-window" "-t" pane])))) + (is (= :failed (:reply/status (control/send! connection ["list-panes" "-t" pane])))) + (is (= :complete (:reply/status (control/send! connection ["send-keys" "-t" anchor "Enter"])))) + (is (loop [seen ""] + (let [seen (str seen (String. ^bytes (:output/bytes (pane-event! stream anchor)) "UTF-8"))] + (cond (.contains ^String seen "alive") true + (< 64 (count seen)) false + :else (recur seen))))) + (is (= :complete (:reply/status + (control/send! connection ["display-message" "-p" "connection-alive"]))))))))) + +(deftest remote-server-loss-drains-buffer-and-preserves-terminal-cause + (gate/with-control-server + (fn [{:keys [server]}] + (with-open [^AutoCloseable connection (control/attach! (:tmux/ref (first (tmux/sessions! server)))) + ^AutoCloseable anchor (control/subscribe! connection) + ^AutoCloseable buffered (control/subscribe! connection)] + (let [pane (first (:reply/lines + (control/send! connection + ["new-window" "-P" "-F" "#{pane_id}" + "printf buffered; read stop"])))] + (is (= "buffered" (String. ^bytes (:output/bytes (pane-event! anchor pane)) "UTF-8"))) + ;; The anchor is an offer fence: the independent buffered subscriber + ;; has received the same event before the daemon is stopped. + (tmux/raw! server ["kill-server"]) + (is (= "buffered" (String. ^bytes (:output/bytes (pane-event! buffered pane)) "UTF-8"))) + (let [terminal (:stream/terminal (control/next! buffered limit))] + (is (= :ended (:reason terminal))) + (is (some? (:cause terminal))) + (is (= terminal (control/terminal buffered))))))))) + + +(deftest closing-control-reclaims-only-its-authenticated-client + (gate/with-control-server + (fn [{:keys [server socket]}] + (doseq [exceptional? [false true]] + (let [owned (atom nil) + expected (ex-info "leave connection scope" {})] + (try + (with-open [^AutoCloseable connection + (control/attach! (:tmux/ref (first (tmux/sessions! server))))] + (let [pid (Long/parseLong ^String (first (:reply/lines + (control/send! connection + ["display-message" "-p" "#{client_pid}"])))) + ^ProcessHandle process (.orElseThrow (ProcessHandle/of pid)) + info (.info process) + parent (.orElseThrow (.parent process)) + started (.orElseThrow (.startInstant info)) + argv (vec (.orElse (.arguments info) (make-array String 0)))] + (is (.isAlive process)) + (is (= (.pid (ProcessHandle/current)) (.pid ^ProcessHandle parent))) + (is (some #{"-C"} argv)) + (is (some #{"attach-session"} argv)) + (reset! owned {:process process :started started :argv argv + :exit (.onExit process)}) + (when exceptional? (throw expected)))) + (catch clojure.lang.ExceptionInfo failure + (is (identical? expected failure)))) + (let [{:keys [^ProcessHandle process ^java.util.concurrent.CompletableFuture exit started]} @owned] + (is (some? started)) + (.get exit 900 TimeUnit/MILLISECONDS) + (is (not (.isAlive process))) + (is (seq (tmux/sessions! server))))))))) + +(deftest live-competing-readers-deliver-each-payload-once-and-share-termination + (gate/with-control-server + (fn [{:keys [server socket binary]}] + (with-open [^AutoCloseable connection (control/attach! (:tmux/ref (first (tmux/sessions! server)))) + ^AutoCloseable anchor (control/subscribe! connection) + ^AutoCloseable shared (control/subscribe! connection)] + (let [command (str (shell-quote binary) " -S " (shell-quote socket)) + script (str "printf aa; " command " wait-for chunk-two; " + "printf bb; " command " wait-for chunk-three; " + "printf cc; " command " wait-for chunk-four; " + "printf dd; read done") + pane (first (:reply/lines + (control/send! connection ["new-window" "-P" "-F" "#{pane_id}" script]))) + entered (CountDownLatch. 2) + start (CountDownLatch. 1) + results [(promise) (promise)] + workers (mapv (fn [result] + (Thread/startVirtualThread + ^Runnable + (fn [] + (.countDown entered) + (deliver result + (try + (.await start) + (loop [events []] + (let [event (control/next! shared)] + (if (= :terminal (:stream/status event)) + {:events events :terminal (:stream/terminal event)} + (recur (if (= pane (:pane/id event)) + (conj events event) events))))) + (catch Exception failure failure)))))) + results)] + (try + (is (.await entered 900 TimeUnit/MILLISECONDS)) + (.countDown start) + (is (= "aa" (String. ^bytes (:output/bytes (pane-event! anchor pane)) "UTF-8"))) + (doseq [[gate payload] [["chunk-two" "bb"] ["chunk-three" "cc"] ["chunk-four" "dd"]]] + (control/send! connection ["wait-for" "-S" gate]) + (is (= payload (String. ^bytes (:output/bytes (pane-event! anchor pane)) "UTF-8")))) + (tmux/raw! server ["kill-server"]) + (let [finished (mapv #(deref % 900 ::timeout) results) + events (sort-by :event/sequence (mapcat :events finished)) + sequences (mapv :event/sequence events)] + (is (every? map? finished)) + (is (= ["aa" "bb" "cc" "dd"] + (mapv #(String. ^bytes (:output/bytes %) "UTF-8") events))) + (is (= 4 (count (set sequences)))) + (doseq [reader finished] + (is (apply < 0 (map :event/sequence (:events reader))))) + (is (= (:terminal (first finished)) (:terminal (second finished)))) + (is (= (control/terminal shared) (:terminal (first finished)))) + (is (= :ended (:reason (:terminal (first finished)))))) + (finally + (.countDown start) + (control/close! shared) + (doseq [^Thread worker workers] + (.interrupt worker) + (.join worker 900) + (is (not (.isAlive worker))))))))))) diff --git a/integration-tests/src/test/clojure/libtmux/core_async_integration_test.clj b/integration-tests/src/test/clojure/libtmux/core_async_integration_test.clj new file mode 100644 index 000000000..941e397c4 --- /dev/null +++ b/integration-tests/src/test/clojure/libtmux/core_async_integration_test.clj @@ -0,0 +1,47 @@ +(ns libtmux.core-async-integration-test + (:require [clojure.core.async :as async] + [clojure.test :refer [deftest is]] + [libtmux.control :as control] + [libtmux.core :as tmux] + [libtmux.core-async :as adapter] + [libtmux.internal.control-gate :as gate]) + (:import [java.lang AutoCloseable] + [java.time Duration])) + +(defn- take-within [channel] + (let [timeout (async/timeout 900) + [value port] (async/alts!! [channel timeout] :priority true)] + (if (identical? port timeout) ::timeout value))) + +(defn- pane-event! [stream pane] + (loop [] + (let [event (control/next! stream (Duration/ofMillis 900))] + (if (= pane (:pane/id event)) event (recur))))) + +(deftest raw-bytes-consumer-close-and-borrowed-connection-lifecycle + (gate/with-control-server + (fn [{:keys [server]}] + (with-open [^AutoCloseable connection + (control/attach! (:tmux/ref (first (tmux/sessions! server)))) + ^AutoCloseable anchor (control/subscribe! connection)] + (let [owner (adapter/observe! connection) + pane (first (:reply/lines + (control/send! connection + ["new-window" "-P" "-F" "#{pane_id}" + "stty -echo; printf raw-bytes; read more; printf blocked; read stop"]))) + event (take-within (adapter/events owner))] + (is (= pane (:pane/id event))) + (is (= (mapv int (.getBytes "raw-bytes" "UTF-8")) + (mapv #(bit-and 255 %) (:output/bytes event)))) + (is (= (mapv int (.getBytes "raw-bytes" "UTF-8")) + (mapv #(bit-and 255 %) (:output/bytes (pane-event! anchor pane))))) + (control/send! connection ["send-keys" "-t" pane "Enter"]) + ;; The independent anchor proves the second push reached every Java + ;; subscription before the adapter consumer closes its channel. + (is (= "blocked" + (String. ^bytes (:output/bytes (pane-event! anchor pane)) "UTF-8"))) + (async/close! (adapter/events owner)) + (is (= :adapter-stopped (:reason (take-within (adapter/terminal owner))))) + (is (= :complete + (:reply/status + (control/send! connection ["display-message" "-p" "borrowed-alive"]))))))))) diff --git a/integration-tests/src/test/clojure/libtmux/core_integration_test.clj b/integration-tests/src/test/clojure/libtmux/core_integration_test.clj new file mode 100644 index 000000000..5ffa737a2 --- /dev/null +++ b/integration-tests/src/test/clojure/libtmux/core_integration_test.clj @@ -0,0 +1,277 @@ +(ns libtmux.core-integration-test + (:require [clojure.test :refer [deftest is]] + [libtmux.control :as control] + [libtmux.core :as tmux] + [libtmux.data :as data] + [libtmux.internal.control-gate :as gate] + [libtmux.internal.fixture :as fixture]) + (:import [java.lang AutoCloseable])) + +(deftest typed-effects-preserve-captured-data-and-explicit-reference + (fixture/with-owned-server + (fn [{:keys [server]}] + (let [session (tmux/new-session! server {:name "effects" :window-name "first"}) + window (first (data/windows session)) + pane (first (data/panes window)) + renamed (tmux/rename! (:tmux/ref session) "renamed")] + (is (thrown? clojure.lang.ExceptionInfo (tmux/rename! (assoc session :session/name "edited") "rejected"))) + (is (= "effects" (:session/name session))) + (is (= "renamed" (:session/name renamed))) + (is (nil? (tmux/find-session! server "missing"))) + (is (= "renamed" (:session/name (tmux/refresh! (:tmux/ref session))))) + (tmux/set-option! (:tmux/ref session) "@clj" "literal #{pane_id}") + (is (= "literal #{pane_id}" (get (tmux/options! (:tmux/ref session)) "@clj"))) + (tmux/unset-option! (:tmux/ref session) "@clj") + (is (nil? (get (tmux/options! (:tmux/ref session)) "@clj"))) + (tmux/set-environment! (:tmux/ref session) "CLJ_TEST" "literal") + (is (= "literal" (get-in (tmux/environment! (:tmux/ref session)) [:values "CLJ_TEST"]))) + (tmux/remove-environment! (:tmux/ref session) "CLJ_TEST") + (is (contains? (:removed (tmux/environment! (:tmux/ref session))) "CLJ_TEST")) + (tmux/unset-environment! (:tmux/ref session) "CLJ_TEST") + (is (not (contains? (:removed (tmux/environment! (:tmux/ref session))) "CLJ_TEST"))) + (tmux/set-hook! (:tmux/ref session) "after-new-window" ["display-message" "first"]) + (tmux/append-hook! (:tmux/ref session) "after-new-window" ["display-message" "second"]) + (is (= 2 (count (get (tmux/hooks! (:tmux/ref session)) "after-new-window")))) + (tmux/unset-hook! (:tmux/ref session) "after-new-window") + (tmux/set-buffer! server "clj" "literal\ntext") + (is (= "literal\ntext" (tmux/show-buffer! server "clj"))) + (is (some #(= "clj" (:buffer/name %)) (tmux/buffers! server))) + (if (.atLeast (.version ^io.github.libtmux.Server server) + (io.github.libtmux.TmuxVersion. 3 4 "")) + (tmux/delete-buffer! server "clj") + (let [error (try (tmux/delete-buffer! server "clj") + (catch clojure.lang.ExceptionInfo e e))] + (is (= :tmux/unsupported-feature (:tmux/error (ex-data error)))) + (is (= :not-dispatched (:tmux/dispatch (ex-data error)))))) + (is (= (:pane/id pane) (tmux/expand! (:tmux/ref pane) "#{pane_id}"))) + (is (vector? (tmux/capture! (:tmux/ref pane) {:from :history}))) + (let [created (tmux/new-window! (:tmux/ref session) {:name "second" :detached? true}) + split (tmux/split-pane! (:tmux/ref (first (data/panes created))) + {:direction :right :percent 30})] + (is (= "second" (:window/name created))) + (is (string? (:pane/id split))) + (tmux/select! (:tmux/ref split)) + (is (:pane/active? (tmux/refresh! (:tmux/ref split)))) + (tmux/resize! (:tmux/ref split) 20 10) + (tmux/kill! (:tmux/ref split)) + (is (nil? (tmux/refresh! (:tmux/ref split))))) + (tmux/kill! (:tmux/ref renamed)) + (is (nil? (tmux/find-session! server "renamed"))))))) + +(deftest contextual-refresh-does-not-escape-unlinked-session + (fixture/with-owned-server + (fn [{:keys [server]}] + (let [first-session (tmux/new-session! server {:name "link-first"}) + second-session (tmux/new-session! server {:name "link-second"}) + window (first (data/windows first-session))] + (tmux/link-window! (:tmux/ref window) (:tmux/ref second-session)) + (let [linked (first (filter #(= (:window/id window) (:window/id %)) + (data/windows (tmux/refresh! (:tmux/ref second-session))))) + pane (first (data/panes linked))] + (is (data/same-entity? (first (data/panes window)) pane)) + (is (not (data/same-link? (first (data/panes window)) pane))) + (tmux/unlink-window! (:tmux/ref linked)) + (is (nil? (tmux/refresh! (:tmux/ref linked)))) + (is (nil? (tmux/refresh! (:tmux/ref pane)))) + (is (some? (tmux/refresh! (:tmux/ref window))))))))) + +(deftest typed-client-operations-switch-and-detach-the-captured-client + (gate/with-control-server + (fn [{:keys [server]}] + (let [source (tmux/new-session! server {:name "client-source"}) + target (tmux/new-session! server {:name "client-target"})] + (with-open [^AutoCloseable connection (control/attach! (:tmux/ref source))] + (let [client (data/one-or-none (tmux/clients! server))] + (is (some? client)) + (is (nil? (tmux/switch-client! (:tmux/ref client) (:tmux/ref target)))) + (is (= "client-target" + (get-in (tmux/refresh! (:tmux/ref client)) + [:client/session :session/name]))) + (is (nil? (tmux/detach-client! (:tmux/ref client)))) + (is (nil? (tmux/refresh! (:tmux/ref client)))) + (is (seq (tmux/sessions! server))))))))) + +(deftest closing-resources-does-not-kill-borrowed-server + (fixture/with-owned-server + (fn [{:keys [server socket binary]}] + (let [owned (tmux/open! {:socket-path socket :binary binary}) + captured (tmux/snapshot! owned)] + (tmux/close! owned) + (is (seq (:tmux/sessions captured))) + (is (seq (tmux/sessions! server))) + (let [error (try (tmux/snapshot! owned) (catch clojure.lang.ExceptionInfo e e))] + (is (= :tmux/closed (:tmux/error (ex-data error))))))))) + +(deftest mutation-followed-by-capture-failure-is-not-safe-to-retry + (fixture/with-owned-server + (fn [{:keys [server]}] + (let [cause (ex-info "capture failed" {:tmux/error :tmux/transport :tmux/dispatch :not-dispatched}) + error (with-redefs [data/from-capture (fn [_] (throw cause))] + (try (tmux/new-session! server {:name "mutation-applied"}) + (catch clojure.lang.ExceptionInfo e e)))] + (is (identical? cause (.getCause ^Throwable error))) + (is (= :unknown (:tmux/dispatch (ex-data error)))) + (is (= :possible (:tmux/effects (ex-data error)))) + (is (some? (tmux/find-session! server "mutation-applied"))))))) + +(deftest refresh-fences-reindex-and-replacement-slots + (fixture/with-owned-server + (fn [{:keys [server]}] + (let [session (tmux/new-session! server {:name "reindex"}) + original (first (data/windows session)) + original-pane (first (data/panes original))] + (tmux/move-window! (:tmux/ref original) (:tmux/ref session) 7) + (is (nil? (tmux/refresh! (:tmux/ref original)))) + (is (nil? (tmux/refresh! (:tmux/ref original-pane)))) + (let [moved (first (data/windows (tmux/refresh! (:tmux/ref session)))) + replacement (tmux/new-window! (:tmux/ref session) + {:name "replacement" :index (:window/index original)}) + error (try (tmux/select! (:tmux/ref original)) + (catch clojure.lang.ExceptionInfo e e))] + (is (data/same-entity? original moved)) + (is (not (data/same-link? original moved))) + (is (not (data/same-entity? original replacement))) + (is (nil? (tmux/refresh! (:tmux/ref original)))) + (is (= :tmux/not-found (:tmux/error (ex-data error)))) + (is (:window/active? (tmux/refresh! (:tmux/ref replacement))))))))) + +(deftest split-retains-the-callers-linked-window-context + (fixture/with-owned-server + (fn [{:keys [server]}] + (let [session (tmux/new-session! server {:name "split-links"}) + window (first (data/windows session)) + target (str (:session/id session) ":9")] + (is (zero? (:exit-code (tmux/raw! server ["link-window" "-s" (:window/id window) "-t" target])))) + (let [linked (first (filter #(= 9 (:window/index %)) (tmux/windows! server))) + pane (first (data/panes linked)) + created (tmux/split-pane! (:tmux/ref pane) {:direction :right})] + (is (= (:tmux/link pane) (:tmux/link created))) + (is (not= (:pane/id pane) (:pane/id created))) + (tmux/unlink-window! (:tmux/ref linked)) + (is (nil? (tmux/refresh! (:tmux/ref pane)))) + (is (nil? (tmux/refresh! (:tmux/ref created)))) + (is (zero? (:exit-code (tmux/raw! server ["link-window" "-s" (:window/id window) "-t" target])))) + (let [restored (tmux/refresh! (:tmux/ref created))] + (is (data/same-link? created restored)))))))) + +(deftest two-handle-effects-reject-cross-endpoint-references + (fixture/with-owned-server + (fn [{left :server}] + (fixture/with-owned-server + (fn [{right :server}] + (let [left-session (first (tmux/sessions! left)) + left-window (first (data/windows left-session)) + left-pane (first (data/panes left-window)) + right-session (first (tmux/sessions! right)) + right-window (first (data/windows right-session))] + (doseq [operation [#(tmux/link-window! (:tmux/ref left-window) (:tmux/ref right-session)) + #(tmux/move-window! (:tmux/ref left-window) (:tmux/ref right-session) 9) + #(tmux/join-pane! (:tmux/ref left-pane) (:tmux/ref right-window))]] + (let [error (try (operation) (catch clojure.lang.ExceptionInfo e e))] + (is (= :tmux/validation (:tmux/error (ex-data error)))) + (is (= :not-dispatched (:tmux/dispatch (ex-data error)))) + (is (instance? IllegalArgumentException (.getCause ^Throwable error))))) + (is (= 1 (count (tmux/windows! left)))) + (is (= 1 (count (tmux/windows! right)))))))))) + +(deftest singleton-command-is-a-literal-executable-with-no-arguments + (fixture/with-owned-server + (fn [{:keys [server socket binary]}] + (let [script (.resolve (.getParent ^java.nio.file.Path socket) "literal executable;name")] + (java.nio.file.Files/writeString script "#!/bin/sh\n[ \"$#\" -eq 0 ] || exit 1\n\"$CLJ_TMUX\" -S \"$CLJ_SOCKET\" wait-for -S singleton-started\nexec /bin/cat\n" + (make-array java.nio.file.OpenOption 0)) + (java.nio.file.Files/setPosixFilePermissions + script (java.nio.file.attribute.PosixFilePermissions/fromString "rwx------")) + (try + (let [session (tmux/new-session! server {:name "literal-executable" :command [(str script)] + :environment {"CLJ_TMUX" binary "CLJ_SOCKET" (str socket)}})] + (is (= :signalled (tmux/await-channel! server "singleton-started" + (java.time.Duration/ofMillis 500)))) + (is (= "literal-executable" (:session/name session))) + (is (= 1 (count (data/panes session))))) + (finally (java.nio.file.Files/deleteIfExists script))))))) + +(deftest command-groups-attribute-reads-partial-failure-and-dependent-layout + (fixture/with-owned-server + (fn [{:keys [server]}] + (let [session (first (tmux/sessions! server)) + pane (first (data/panes session)) + reads (mapv (fn [index] ["display-message" "-p" "-t" (:pane/id pane) + (str index ":#{pane_id}")]) (range 4))] + (doseq [run [tmux/batch! tmux/chain!]] + (let [results (run server reads)] + (is (= [:complete :complete :complete :complete] (mapv :operation/outcome results))) + (is (= (mapv #(vector (str % ":" (:pane/id pane))) (range 4)) + (mapv :operation/stdout results)))) + (let [results (run server [["set-buffer" "-b" "group-before" "applied"] + ["select-pane" "-t" "%999999999"] + ["set-buffer" "-b" "group-after" "skipped"]])] + (is (= [:complete :failed :skipped] (mapv :operation/outcome results))) + (is (= "applied" (tmux/show-buffer! server "group-before"))) + (is (not-any? #(= "group-after" (:buffer/name %)) (tmux/buffers! server))))) + (let [results (tmux/chain! server [["new-window" "-t" (:session/id session) "-n" "group-layout"] + ["split-window" "-h"] + ["select-layout" "even-horizontal"] + ["display-message" "-p" "#{window_name}:#{window_panes}"]])] + (is (= [:complete :complete :complete :complete] (mapv :operation/outcome results))) + (is (= ["group-layout:2"] (:operation/stdout (last results))))))))) + +(deftest waiting-groups-reserve-release-capacity + (fixture/with-owned-server + (fn [{:keys [server]}] + (with-open [transport (io.github.libtmux.transport.ProcessTransport. 2) + shared (io.github.libtmux.Server/using (.config ^io.github.libtmux.Server server) transport)] + (doseq [run [tmux/batch! tmux/chain!]] + (let [result (promise) + worker (Thread/startVirtualThread + ^Runnable (fn [] + (deliver result + (try (run shared [["wait-for" "-S" "group-started"] + ["wait-for" "group-release"]] + {:kind :wait}) + (catch Exception e e)))))] + (try + (is (= :signalled (tmux/await-channel! server "group-started" (java.time.Duration/ofMillis 800)))) + (let [refused (try (run shared [["wait-for" "second-wait"]] {:kind :wait}) + (catch clojure.lang.ExceptionInfo e e))] + (is (= :not-dispatched (:tmux/dispatch (ex-data refused)))) + (is (= [:skipped] (mapv :operation/outcome (:tmux/results (ex-data refused)))))) + (tmux/signal-channel! shared "group-release") + (let [finished (deref result 800 ::timeout)] + (is (vector? finished)) + (is (= [:complete :complete] (mapv :operation/outcome finished)))) + (finally + (.interrupt worker) + (.join worker 800) + (is (not (.isAlive worker))))))))))) + +(deftest cancelled-groups-report-unknown-positions-and-preserve-partial-effects + (fixture/with-owned-server + (fn [{:keys [server]}] + (let [result (promise) + worker (Thread/startVirtualThread + ^Runnable (fn [] + (deliver result + (try (tmux/batch! server + [["set-buffer" "-b" "cancel-before" "applied"] + ["wait-for" "-S" "cancel-started"] + ["wait-for" "cancel-blocked"] + ["set-buffer" "-b" "cancel-after" "uncertain"]] + {:kind :wait}) + (catch Exception e e)))))] + (try + (is (= :signalled (tmux/await-channel! server "cancel-started" (java.time.Duration/ofMillis 800)))) + (.interrupt worker) + (let [error (deref result 800 ::timeout)] + (is (instance? clojure.lang.ExceptionInfo error)) + (is (= :unknown (:tmux/dispatch (ex-data error)))) + (is (= [:unknown :unknown :unknown :unknown] + (mapv :operation/outcome (:tmux/results (ex-data error))))) + (is (instance? io.github.libtmux.exception.DispatchException + (.getCause ^Throwable error)))) + (is (= "applied" (tmux/show-buffer! server "cancel-before"))) + (finally + (tmux/signal-channel! server "cancel-blocked") + (.interrupt worker) + (.join worker 800) + (is (not (.isAlive worker))))))))) diff --git a/integration-tests/src/test/clojure/libtmux/data_integration_test.clj b/integration-tests/src/test/clojure/libtmux/data_integration_test.clj new file mode 100644 index 000000000..876e795e0 --- /dev/null +++ b/integration-tests/src/test/clojure/libtmux/data_integration_test.clj @@ -0,0 +1,119 @@ +(ns libtmux.data-integration-test + (:require [clojure.edn :as edn] + [clojure.test :refer [deftest is]] + [libtmux.data :as data] + [libtmux.internal.control-gate :as gate] + [libtmux.internal.fixture :as fixture]) + (:import [io.github.libtmux Pane_ Server ServerConfig ServerEndpoint SessionId Session_ TmuxVersion Window_] + [java.lang AutoCloseable] + [io.github.libtmux.control ControlClient] + [io.github.libtmux.query FilterExpr] + [io.github.libtmux.transport ProcessTransport TmuxTransport] + [java.nio.file Path] + [java.time Duration] + [java.util.regex Pattern])) + +(set! *warn-on-reflection* true) + +(defn- command! [^Server server & argv] + (is (.succeeded (.cmd server ^java.util.List (vec argv))))) + +(deftest capture-values-preserve-occurrences-and-never-read-live-state + (fixture/with-owned-server + (fn [{:keys [socket binary]}] + (let [config (-> (ServerConfig/builder) + (.binary binary) + (.endpoint (ServerEndpoint/socketPath + ^Path socket)) + (.defaultTimeout (Duration/ofMillis 900)) + .build) + calls (atom 0) + old (atom nil)] + (with-open [server (Server/open config) + processes (ProcessTransport.)] + (command! server "rename-session" "-t" "$0" "alpha") + (command! server "new-session" "-d" "-s" "beta") + (command! server "link-window" "-s" "alpha:0" "-t" "alpha:9") + (command! server "link-window" "-s" "alpha:0" "-t" "beta:9") + (command! server "select-window" "-t" "alpha:9") + (with-open [^AutoCloseable client (if (gate/supported? server) + (ControlClient/attachUnfenced config (SessionId. "$0")) + (reify AutoCloseable (close [_]))) + measured (Server/using + config + (reify TmuxTransport + (execute [_ request] + (swap! calls inc) + (.execute processes request)) + (close [_])))] + (let [captured (.captureServer measured) + entries (data/from-capture captured) + acquired @calls + alpha (data/exactly-one + (filter #(= "alpha" (:session/name %)) (:tmux/sessions entries))) + links (data/windows alpha) + panes (data/panes alpha) + shared (filter #(= (-> links first :window/id) (:window/id %)) + (:tmux/windows entries)) + detached (data/data entries)] + (is (= 3 acquired) "two data requests behind one admission probe") + (is (= 3 (count shared))) + (is (= 2 (count links))) + (is (= 2 (count panes))) + (is (data/same-entity? (first panes) (second panes))) + (is (not (data/same-link? (first panes) (second panes)))) + (is (= 9 (:window/index (data/active-window alpha)))) + (is (= 9 (get-in (data/active-pane alpha) [:tmux/link :window/index]))) + (let [matches (data/predicate (.is (Session_/name) "alpha"))] + (is (matches (assoc alpha :session/name "edited"))) + (is (thrown? clojure.lang.ExceptionInfo (matches (data/data alpha))))) + (let [active (.is (Window_/active) true) + index-zero (.is (Window_/index) 0) + same-child (.any (Session_/windows) (.and ^FilterExpr active index-zero)) + separate-children (.and ^FilterExpr (.any (Session_/windows) active) + (.any (Session_/windows) index-zero))] + (is (false? ((data/predicate same-child) alpha))) + (is (true? ((data/predicate separate-children) alpha)))) + (doseq [[items expressions] + [[(:tmux/sessions entries) + [(FilterExpr/and []) (FilterExpr/or []) + (.is (Session_/attached) false) + (.matches (Session_/name) (Pattern/compile "LPH" Pattern/CASE_INSENSITIVE)) + (.all (Session_/windows) (FilterExpr/and [])) + (.none (Session_/windows) (FilterExpr/or []))]] + [(:tmux/windows entries) + [(.is (Window_/session) (.is (Session_/name) "alpha")) + (.any (Window_/panes) (.is (Pane_/active) true))]] + [(:tmux/panes entries) + [(.is (Pane_/active) false) (.greaterThan (Pane_/width) 0)]]] + ^FilterExpr expression expressions] + (let [duplicates (into (vec items) (take 1 items)) + direct (filterv #(.test expression %) (mapv :tmux/ref duplicates)) + lazy-result (mapv :tmux/ref (data/matching expression duplicates)) + reduced-result (into [] (comp (data/matching expression) (map :tmux/ref)) duplicates)] + (is (= direct lazy-result reduced-result)) + (is (every? true? (map identical? direct reduced-result))))) + (is (thrown? IllegalArgumentException (.is (Session_/name) nil))) + (is (every? (if (.atLeast (TmuxVersion/parse (:tmux/version entries)) + (TmuxVersion. 3 7 "")) + false? nil?) + (map :pane/floating? (:tmux/panes entries)))) + ;; A client exists only where managed control can attach (tmux 3.7+). + (when (instance? ControlClient client) + (is (seq (:tmux/clients entries))) + (is (= "$0" (get-in entries [:tmux/clients 0 :client/session :session/id])))) + (is (= detached (edn/read-string (pr-str detached)))) + (dotimes [_ 20] + (hash entries) + (pr-str entries) + (= entries entries) + (into [] (comp (filter :pane/active?) (map :pane/id)) (data/panes alpha)) + (group-by :window/id (:tmux/windows entries))) + (command! server "rename-window" "-t" "alpha:0" "new-name") + (is (not= "new-name" (:window/name (first links)))) + (is (= detached (data/data entries))) + (is (= acquired @calls)) + (reset! old entries))) + (is (= (data/data @old) (edn/read-string (pr-str (data/data @old))))) + (is (thrown? IllegalStateException + (.captureServer ^Server (:tmux/ref @old))))))))) diff --git a/integration-tests/src/test/clojure/libtmux/documentation_examples_test.clj b/integration-tests/src/test/clojure/libtmux/documentation_examples_test.clj new file mode 100644 index 000000000..592cefe1f --- /dev/null +++ b/integration-tests/src/test/clojure/libtmux/documentation_examples_test.clj @@ -0,0 +1,224 @@ +(ns libtmux.documentation-examples-test + (:require [clojure.edn :as edn] + [clojure.string :as str] + [clojure.test :refer [deftest is]] + [libtmux.control :as control] + [libtmux.core :as tmux] + [libtmux.internal.control-gate :as gate] + [libtmux.internal.fixture :as fixture]) + (:import [java.lang AutoCloseable] + [java.nio.file Files LinkOption Path] + [java.util UUID])) + +(def ^:private fence + #"(?ms)(?:\s*\n)?^```clojure\s*\n(.*?)^```") +(def ^:private output-line #"(?m)^\s*;;\s*=>\s*(.+?)\s*$") +(def ^:private required-documents ["libtmux-clojure/README.md" + "docs/guide/clojure.md" + "docs/guide/clojure-reference.md"]) +(def ^:private socket-property "libtmux.docs.socket") +(def ^:private binary-property "libtmux.docs.binary") +(def ^:private fields-marker #"(?s)") + +(defn- document-root [] + (Path/of (System/getProperty "libtmux.docs.root") (make-array String 0))) + +(defn- regular-file? [^Path path] + (Files/isRegularFile path (make-array LinkOption 0))) + +(defn- listed [^Path directory] + (with-open [entries (Files/list directory)] + (vec (.toList entries)))) + +(defn- require-documents! [^Path root] + (doseq [relative required-documents + :let [^String relative relative + ^Path document (.resolve root relative)]] + (when-not (regular-file? document) + (throw (ex-info "required Clojure documentation is missing" + {:docs/error :missing-document :document relative})))) + root) + +(defn- readable-documents [^Path root] + (require-documents! root) + (let [readmes (->> (listed root) + (map (fn [^Path directory] (.resolve directory "README.md"))) + (filter regular-file?)) + guides (->> (listed (.resolve root "docs/guide")) + (filter #(str/ends-with? (str (.getFileName ^Path %)) ".md")))] + (->> (concat [(.resolve root "README.md")] + readmes + guides) + (filter regular-file?) + (sort-by str) + vec))) + +(defn- line-number [text offset] + (inc (count (filter #(= \newline %) (subs text 0 offset))))) + +(defn- snippet! [source line directive code] + (when-not (#{"fixture" "control"} directive) + (throw (ex-info "Clojure documentation snippets require the fixture directive" + {:docs/error :missing-or-unknown-directive + :document source :line line :directive directive}))) + (when-not (re-find #"\(with-open\s+\[server\b" code) + (throw (ex-info "Clojure documentation snippets must close their Server" + {:docs/error :missing-ownership :document source :line line}))) + (let [outputs (re-seq output-line code)] + (when-not (= 1 (count outputs)) + (throw (ex-info "Clojure documentation snippets require one displayed EDN result" + {:docs/error :missing-or-ambiguous-output + :document source :line line :outputs (count outputs)}))) + (let [[_ expected] (first outputs)] + {:document source + :directive directive + :line line + :code (str/replace code output-line "") + :expected (edn/read-string expected)}))) + +(defn- snippets-in [^Path root ^Path document] + (let [text (slurp (str document)) + matcher (re-matcher fence text) + source (str (.relativize root document))] + (loop [snippets []] + (if (.find matcher) + (recur (conj snippets + (snippet! source + (line-number text (.start matcher)) + (.group matcher 1) + (.group matcher 2)))) + snippets)))) + +(defn- documented-snippets [^Path root] + (->> (readable-documents root) + (mapcat #(snippets-in root %)) + vec)) + +(defn- documented-fields! [^Path root] + (let [reference (.resolve root "docs/guide/clojure-reference.md") + markers (re-seq fields-marker (slurp (str reference)))] + (when-not (= 1 (count markers)) + (throw (ex-info "Clojure field reference needs one field marker" + {:docs/error :missing-or-ambiguous-field-marker + :count (count markers)}))) + (let [fields (edn/read-string (second (first markers)))] + (when-not (and (vector? fields) + (every? #(and (keyword? %) (namespace %)) fields)) + (throw (ex-info "Clojure field marker must contain qualified keywords" + {:docs/error :invalid-field-marker}))) + (set fields)))) + +(defn- captured-fields [value] + (cond + (map? value) (into (set (filter #(and (keyword? %) (namespace %)) (keys value))) + (mapcat captured-fields (vals value))) + (sequential? value) (into #{} (mapcat captured-fields value)) + :else #{})) + +(defn- projected-fields! + "The keys a capture projects. A client appears in it only where control can attach." + [] + (fixture/with-owned-server + (fn [{:keys [server]}] + (let [session (first (tmux/sessions! server))] + (if (gate/supported? server) + (with-open [^AutoCloseable connection (control/attach! (:tmux/ref session))] + (captured-fields (tmux/snapshot! server))) + (captured-fields (tmux/snapshot! server))))))) + +(defn- restore-property! [property value] + (if value + (System/setProperty property value) + (System/clearProperty property))) + +(defn- with-fixture-properties [^Path socket f] + (let [socket-before (System/getProperty socket-property) + binary-before (System/getProperty binary-property)] + (try + (System/setProperty socket-property (str socket)) + (System/setProperty binary-property (System/getProperty "libtmux.tmux")) + (f) + (finally + (restore-property! socket-property socket-before) + (restore-property! binary-property binary-before))))) + +(defn- evaluate-snippet! [{:keys [document line code expected]} ^Path socket] + (with-fixture-properties socket + #(let [name (symbol (str "libtmux.docs.generated." (UUID/randomUUID))) + namespace (create-ns name)] + (try + (binding [*ns* namespace] + (refer 'clojure.core) + (let [actual (eval (read-string (str "(do\n" code "\n)")))] + (when-not (= expected actual) + (throw (ex-info "Clojure documentation output differs from the evaluated result" + {:docs/error :output-mismatch + :document document :line line + :expected expected :actual actual}))) + actual)) + (finally + (remove-ns name)))))) + +(defn- failure-data [f] + (try + (f) + nil + (catch clojure.lang.ExceptionInfo failure + (ex-data failure)))) + +(defn- run-snippet! + "Runs a snippet on a fresh server. A control snippet needs tmux 3.7+; older daemons + must refuse it with :tmux/unsupported-feature, which is then the displayed result." + [{:keys [directive expected] :as snippet}] + (fixture/with-owned-server + (fn [{:keys [server socket]}] + (if (and (= "control" directive) (not (gate/supported? server))) + (let [failure (failure-data #(evaluate-snippet! snippet socket))] + (if (= :tmux/unsupported-feature (:tmux/error failure)) expected failure)) + (evaluate-snippet! snippet socket))))) + +(deftest required-documentation-is-present-and-discovered + (let [root (document-root) + snippets (documented-snippets root)] + (is (seq snippets)) + (is (= #{"libtmux-clojure/README.md" "docs/guide/clojure.md"} + (set (map :document snippets)))))) + +(deftest captured-field-reference-matches-the-projection + (let [documented (documented-fields! (document-root)) + projected (projected-fields!)] + ;; Client keys are documented, and projected only where a control client could attach. + (is (= projected + (if (some #{:client/name} projected) + documented + (into #{} (remove #(= "client" (namespace %))) documented)))))) + +(deftest documented-snippets-open-owned-clients-and-match-their-output + (doseq [snippet (documented-snippets (document-root))] + (is (= (:expected snippet) (run-snippet! snippet)) + (str (:document snippet) ":" (:line snippet))))) + +(deftest malformed-documentation-is-rejected-before-it-can-look-executable + (is (= :missing-ownership + (:docs/error (failure-data #(snippet! "guide.md" 1 "fixture" "(+ 1 1)\n;; => 2"))))) + (is (= :missing-or-ambiguous-output + (:docs/error (failure-data #(snippet! "guide.md" 1 "fixture" + "(with-open [server nil] 2)"))))) + (is (= :missing-or-unknown-directive + (:docs/error (failure-data #(snippet! "guide.md" 1 nil + "(with-open [server nil] 2)\n;; => 2"))))) + (let [directory (Files/createTempDirectory "libtmux-docs-" + (make-array java.nio.file.attribute.FileAttribute 0))] + (try + (is (= :missing-document + (:docs/error (failure-data #(require-documents! directory))))) + (finally + (Files/deleteIfExists directory))))) + +(deftest renamed-imports-and-wrong-displayed-output-fail-the-documentation-gate + (let [root (document-root) + snippet (first (documented-snippets root)) + renamed (assoc snippet :code "(require '[libtmux.missing :as tm])")] + (is (thrown? Throwable (run-snippet! renamed))) + (is (= :output-mismatch + (:docs/error (failure-data #(run-snippet! (assoc snippet :expected ::wrong)))))))) diff --git a/integration-tests/src/test/clojure/libtmux/internal/control_gate.clj b/integration-tests/src/test/clojure/libtmux/internal/control_gate.clj new file mode 100644 index 000000000..7a0cbeaf1 --- /dev/null +++ b/integration-tests/src/test/clojure/libtmux/internal/control_gate.clj @@ -0,0 +1,32 @@ +(ns libtmux.internal.control-gate + "Control needs tmux 3.7 or newer. Control examples run there, and every + older daemon must refuse before attaching a client." + (:require [clojure.test :refer [is]] + [libtmux.control :as control] + [libtmux.core :as tmux] + [libtmux.internal.fixture :as fixture]) + (:import [io.github.libtmux Server] + [io.github.libtmux.internal ControlAcquisition])) + +(defn supported? + "Whether the server's daemon qualifies for control: tmux 3.7 or newer, where a development or + candidate build of 3.7 itself ranks below it." + [^Server server] + (ControlAcquisition/supports (.version server))) + +(defn refused! + "Asserts the refusal an older daemon gives, and that no client was attached." + [server] + (let [failure (try (control/attach! (:tmux/ref (first (tmux/sessions! server)))) + nil + (catch clojure.lang.ExceptionInfo error error))] + (is (= :tmux/unsupported-feature (:tmux/error (ex-data failure)))) + (is (= :not-dispatched (:tmux/dispatch (ex-data failure)))) + (is (empty? (.clients ^Server server))))) + +(defn with-control-server + "Runs f on a daemon that supports control, and asserts the refusal on any older one." + [f] + (fixture/with-owned-server + (fn [{:keys [server] :as context}] + (if (supported? server) (f context) (refused! server))))) diff --git a/integration-tests/src/test/clojure/libtmux/internal/fixture.clj b/integration-tests/src/test/clojure/libtmux/internal/fixture.clj new file mode 100644 index 000000000..809aead5d --- /dev/null +++ b/integration-tests/src/test/clojure/libtmux/internal/fixture.clj @@ -0,0 +1,196 @@ +(ns libtmux.internal.fixture + (:import [io.github.libtmux Server ServerConfig ServerConfig$Builder ServerEndpoint] + [io.github.libtmux.junit5 NamedServerFixture] + [io.github.libtmux.transport CommandResult] + [java.io IOException] + [java.lang AutoCloseable ProcessHandle ProcessHandle$Info] + [java.nio.file Files Path LinkOption] + [java.nio.file.attribute BasicFileAttributes] + [java.time Duration Instant] + [java.util UUID] + [java.util.concurrent TimeUnit])) + +(def ^:private ^Path socket-root (Path/of "/tmp/libtmux-java-test" (make-array String 0))) +(def ^:dynamic *after-start* (fn [_ _])) + +(defn- open-server [^Path socket ^Path config] + (let [^ServerConfig$Builder builder (ServerConfig/builder)] + (.binary builder (System/getProperty "libtmux.tmux" "tmux")) + (.endpoint builder (ServerEndpoint/socketPath socket)) + (.configFile builder config) + (.defaultTimeout builder (Duration/ofMillis 900)) + (Server/open (.build builder)))) + +(defn- command! [^Server server ^java.util.List argv] + (.cmd server argv)) + +(defn- close-suppressing! [resource primary] + (try + (.close ^AutoCloseable resource) + (catch Throwable cleanup + (if-let [^Throwable failure @primary] + (.addSuppressed failure cleanup) + (throw cleanup))))) + +(defn- remove-scratch! [^Path directory ^Path config] + (Files/deleteIfExists config) + (Files/deleteIfExists directory)) + +(defn- suppress! [primary cleanup] + (if-let [^Throwable failure @primary] + (.addSuppressed failure cleanup) + (throw cleanup))) + +(defn- process-identity [^ProcessHandle process] + (let [^ProcessHandle$Info info (.info process)] + {:process process + :pid (.pid process) + :command (.orElse (.command info) nil) + :arguments (vec (.orElse (.arguments info) (make-array String 0))) + :started (.orElse (.startInstant info) nil)})) + +(defn- basename [^String path] + (when path (str (.getFileName (Path/of path (make-array String 0)))))) + +(defn same-socket-path? [^Path expected ^String observed] + (try + (let [^"[Ljava.nio.file.LinkOption;" options (make-array LinkOption 0) + ^Path actual (Path/of observed (make-array String 0))] + (= (.toRealPath expected options) (.toRealPath actual options))) + (catch IOException _ false) + (catch java.nio.file.InvalidPathException _ false))) + +(defn- tmux-for-socket? [identity binary ^Path socket] + (let [command (:command identity) + arguments (:arguments identity) + executable (basename command) + expected (basename binary)] + (and (= expected executable) + (some true? + (map-indexed + (fn [index argument] + (and (= "-S" argument) + (< (inc index) (count arguments)) + (same-socket-path? socket (nth arguments (inc index))))) + arguments))))) + +(defn- socket-key [^Path socket] + (let [^"[Ljava.nio.file.LinkOption;" options (into-array LinkOption [LinkOption/NOFOLLOW_LINKS]) + ^BasicFileAttributes attributes (Files/readAttributes socket BasicFileAttributes options)] + (.fileKey attributes))) + +(defn- capture-process [^Server server ^Path socket] + (let [^CommandResult result (command! server ["display-message" "-p" + "#{pid}\t#{socket_path}"]) + rows (.stdout result)] + (when-not (.succeeded result) + (throw (ex-info "could not read fixture tmux identity" {:stderr (.stderr result)}))) + (when-not (= 1 (count rows)) + (throw (ex-info "tmux reported ambiguous fixture identities" {:rows rows}))) + (let [[pid reported-socket] (.split ^String (first rows) "\\t" -1) + process (.orElseThrow (ProcessHandle/of (Long/parseLong pid)))] + (when-not (same-socket-path? socket reported-socket) + (throw (ex-info "tmux reported another fixture socket" + {:socket socket :reported-socket reported-socket}))) + (assoc (process-identity process) :socket-key (socket-key socket))))) + +(defn- same-process? [owned] + (let [^ProcessHandle process (:process owned) + current (process-identity process)] + (= (select-keys owned [:pid :command :arguments :started]) + (select-keys current [:pid :command :arguments :started])))) + +(defn- stop-captured! [owned ^Path socket primary] + (when owned + (let [^ProcessHandle process (:process owned)] + (if-not (same-process? owned) + (suppress! primary + (ex-info "refusing to terminate a replacement tmux process" + {:socket socket :pid (:pid owned)})) + (do + (.destroy process) + (try + (.get (.onExit process) 900 TimeUnit/MILLISECONDS) + (catch Exception cleanup + (suppress! primary cleanup))) + (when (.isAlive process) + (suppress! primary + (ex-info "fixture tmux survived startup cleanup" + {:socket socket :pid (:pid owned)}))) + (when (and (not (.isAlive process)) + (Files/exists socket (make-array LinkOption 0))) + (if (and (:socket-key owned) (= (:socket-key owned) (socket-key socket))) + (Files/delete socket) + (suppress! primary (ex-info "refusing to reclaim a replacement fixture socket" + {:socket socket}))))))))) + +(defn with-owned-server [body] + (Files/createDirectories socket-root (make-array java.nio.file.attribute.FileAttribute 0)) + (let [directory (Files/createTempDirectory socket-root "clj-" + (make-array java.nio.file.attribute.FileAttribute 0)) + socket (.resolve directory "s") + config (.resolve directory "tmux.conf")] + (Files/writeString config "" (make-array java.nio.file.OpenOption 0)) + (let [^Server server + (try + (open-server socket config) + (catch Throwable failure + (try + (remove-scratch! directory config) + (catch Throwable cleanup + (.addSuppressed failure cleanup))) + (throw failure))) + primary (atom nil) + captured (atom nil) + binary (System/getProperty "libtmux.tmux" "tmux")] + (try + (let [^CommandResult created + (command! server ["new-session" "-d" "-s" + (str "clj-" (UUID/randomUUID))])] + (when-not (.succeeded created) + (throw (ex-info "could not start fixture tmux" {:stderr (.stderr created)})))) + (reset! captured (capture-process server socket)) + (*after-start* server socket) + (let [^NamedServerFixture owner (NamedServerFixture/own server socket socket-root)] + (try + (let [expected (System/getProperty "libtmux.tmux.expected") + ^CommandResult version-result + (command! server ["display-message" "-p" "#{version}"]) + actual (first (.stdout version-result))] + (when-not (.succeeded version-result) + (throw (ex-info "could not read fixture tmux version" + {:stderr (.stderr version-result)}))) + (when (and expected (not= expected actual)) + (throw (ex-info "fixture tmux does not match the selected matrix lane" + {:expected expected :actual actual}))) + (body {:server server + :socket (.socket owner) + :binary binary + :expected-version expected + :actual-version actual})) + (catch Throwable failure + (reset! primary failure) + (throw failure)) + (finally + (close-suppressing! owner primary)))) + (catch Throwable failure + (reset! primary failure) + (when-not @captured + (try + (reset! captured (capture-process server socket)) + (catch Throwable cleanup + (.addSuppressed failure cleanup)))) + (when (and (nil? @captured) + (Files/exists socket (make-array java.nio.file.LinkOption 0))) + (.addSuppressed failure + (ex-info "refusing to terminate an unauthenticated tmux process" + {:socket socket}))) + (stop-captured! @captured socket primary) + (throw failure)) + (finally + (close-suppressing! server primary) + (when-not (Files/exists socket (make-array java.nio.file.LinkOption 0)) + (try + (remove-scratch! directory config) + (catch Throwable cleanup + (suppress! primary cleanup))))))))) diff --git a/integration-tests/src/test/clojure/libtmux/internal/fixture_test.clj b/integration-tests/src/test/clojure/libtmux/internal/fixture_test.clj new file mode 100644 index 000000000..e0f4deec1 --- /dev/null +++ b/integration-tests/src/test/clojure/libtmux/internal/fixture_test.clj @@ -0,0 +1,219 @@ +(ns libtmux.internal.fixture-test + (:require [clojure.test :refer [deftest is]] + [libtmux.async :as async] + [libtmux.internal.fixture :as fixture]) + (:import [java.lang ProcessHandle Thread] + [java.nio.file Files Path] + [java.util.concurrent TimeUnit])) + +(defn- exists? [^Path socket] + (Files/exists socket + (make-array java.nio.file.LinkOption 0))) + +(defn- capture-fixture-process [server socket] + (let [capture (ns-resolve 'libtmux.internal.fixture 'capture-process)] + (capture server socket))) + +(defn- tmux-for-socket? [identity binary socket] + (let [matches? (ns-resolve 'libtmux.internal.fixture 'tmux-for-socket?)] + (matches? identity binary socket))) + +(defn- await-exit! [^ProcessHandle process] + (.get (.onExit process) 900 TimeUnit/MILLISECONDS)) + +(defn- replace-fixture-daemon! [server ^Path socket] + (let [binary (System/getProperty "libtmux.tmux" "tmux") + original (capture-fixture-process server socket) + directory (.getParent socket) + config (.resolve directory "foreign.conf")] + (when-not original + (throw (ex-info "could not capture the fixture tmux process" {:socket socket}))) + (.destroy ^ProcessHandle (:process original)) + (await-exit! (:process original)) + (Files/deleteIfExists socket) + (Files/writeString config "" (make-array java.nio.file.OpenOption 0)) + (let [^java.util.List arguments [binary "-f" (str config) "-S" (str socket) + "new-session" "-d" "-s" "foreign-fixture"] + ^Process process (.start (ProcessBuilder. arguments)) + completed? (.waitFor process 900 TimeUnit/MILLISECONDS) + replacement (capture-fixture-process server socket)] + (when-not (and completed? (zero? (.exitValue process)) replacement) + (throw (ex-info "could not start a replacement tmux process" {:socket socket}))) + (assoc replacement :socket socket :config config)))) + +(defn- interrupted-fixture-cleans! [options expected-error] + (let [started (promise) + release (java.util.concurrent.CountDownLatch. 1) + ^libtmux.async.AsyncRuntime runtime (async/runtime! {:close-timeout-ms 900}) + task (async/submit! runtime options + #(fixture/with-owned-server + (fn [{:keys [server socket]}] + (let [owner (capture-fixture-process server socket)] + (deliver started {:socket socket + :directory (.getParent ^Path socket) + :process (:process owner) + :worker (Thread/currentThread)}) + (.await release)))))] + (let [owned + (try + (let [owned (deref started 800 ::not-started)] + (is (not= ::not-started owned)) + (when (map? owned) + (is (instance? ProcessHandle (:process owned))) + (is (instance? Thread (:worker owned))) + (is (exists? (:socket owned)))) + (when (= expected-error :cancelled) + (is (true? (async/cancel! task)))) + (let [failure (try @task (catch Throwable failure failure))] + (is (= expected-error (:tmux/error (ex-data failure))))) + owned) + (finally + (.countDown release) + (try + (deref task 900 ::still-running) + (catch Throwable _)) + (async/close-runtime! runtime)))] + (when (map? owned) + (is (false? (.isAlive ^ProcessHandle (:process owned)))) + ;; Task completion is published before its virtual worker returns to + ;; the executor. Wait for that worker's terminal transition before + ;; asserting fixture ownership cleanup. + (.join ^Thread (:worker owned) 900) + (is (false? (.isAlive ^Thread (:worker owned)))) + (is (not (exists? (:socket owned)))) + (is (not (exists? (:directory owned)))))))) + +(deftest fixture-recognizes-a-socket-through-an-ancestor-link + (let [attributes (make-array java.nio.file.attribute.FileAttribute 0) + root (Files/createTempDirectory (Path/of "/tmp/libtmux-java-test" + (make-array String 0)) + "path-alias-" attributes) + real (.resolve root "real") + socket (.resolve real "s") + alias (.resolve root "alias") + linked-socket (.resolve alias "s")] + (try + (Files/createDirectory real attributes) + (Files/createFile socket attributes) + (Files/createSymbolicLink alias real attributes) + (is (true? + (tmux-for-socket? {:command "/usr/bin/tmux" + :arguments ["-S" (str linked-socket)]} + "/usr/bin/tmux" socket))) + (finally + (Files/deleteIfExists alias) + (Files/deleteIfExists socket) + (Files/deleteIfExists real) + (Files/deleteIfExists root))))) + +(deftest owned-server-is-removed-after-success + (let [seen (atom nil)] + (fixture/with-owned-server + (fn [{:keys [socket] :as owned}] + (reset! seen socket) + (is (instance? io.github.libtmux.Server (:server owned))) + (is (exists? socket)))) + (is (not (exists? @seen))))) + +(deftest owned-server-is-removed-after-exception + (let [seen (atom nil)] + (is (thrown? Exception + (fixture/with-owned-server + (fn [{:keys [socket]}] + (reset! seen socket) + (throw (Exception. "fixture failure")))))) + (is (not (exists? @seen))))) + +(deftest fixture-captures-the-daemon-with-a-live-client + (fixture/with-owned-server + (fn [{:keys [server socket binary]}] + (let [channel (str "fixture-process-" (java.util.UUID/randomUUID)) + ^java.util.List client-arguments [binary "-S" (str socket) + "wait-for" channel] + ^Process client (.start (ProcessBuilder. client-arguments))] + (try + (is (.isAlive client)) + (let [owned (capture-fixture-process server socket)] + (is (instance? ProcessHandle (:process owned))) + (is (not= (.pid client) (:pid owned)))) + (finally + (let [^java.util.List signal-arguments [binary "-S" (str socket) + "wait-for" "-S" channel] + ^Process signal (.start (ProcessBuilder. signal-arguments))] + (.waitFor signal 900 TimeUnit/MILLISECONDS)) + (.waitFor client 900 TimeUnit/MILLISECONDS))))))) + +(deftest server-is-removed-when-setup-fails-after-start + (let [seen (atom nil)] + (is (thrown? Exception + (binding [fixture/*after-start* + (fn [_ socket] + (reset! seen socket) + (throw (Exception. "setup failure")))] + (fixture/with-owned-server (fn [_]))))) + (is (not (exists? @seen))))) + +(deftest selected-version-must-match + (let [property "libtmux.tmux.expected" + previous (System/getProperty property)] + (try + (System/setProperty property "not-a-tmux-version") + (is (thrown-with-msg? clojure.lang.ExceptionInfo + #"does not match" + (fixture/with-owned-server (fn [_])))) + (finally + (if previous + (System/setProperty property previous) + (System/clearProperty property)))))) + +(deftest selected-clojure-version-must-match + (let [expected (System/getProperty "libtmux.clojure.expected")] + (is (some? expected) "the integration runner must name its Clojure runtime") + (is (= expected (clojure-version)) + "the integration runner must use the Clojure version it names"))) + +(deftest changed-process-identity-is-not-owned + (let [process (ProcessHandle/current) + owned {:process process + :pid (.pid process) + :command "tmux" + :arguments ["-S" "/tmp/libtmux-java-test/owned"] + :started nil} + same-process? (ns-resolve 'libtmux.internal.fixture 'same-process?) + identity (ns-resolve 'libtmux.internal.fixture 'process-identity)] + (with-redefs-fn {identity (fn [_] owned)} + #(is (true? (same-process? owned)))) + (with-redefs-fn {identity (fn [_] (assoc owned :pid (inc (:pid owned))))} + #(is (false? (same-process? owned)))))) + +(deftest replacement-daemon-survives-fixture-refusal + (let [replacement (atom nil) + failure (try + (binding [fixture/*after-start* + (fn [server socket] + (reset! replacement (replace-fixture-daemon! server socket)) + (throw (ex-info "fixture process was replaced" {:socket socket})))] + (fixture/with-owned-server (fn [_]))) + nil + (catch Throwable failure failure))] + (try + (is (instance? clojure.lang.ExceptionInfo failure)) + (is (some #(re-find #"replacement" (.getMessage ^Throwable %)) + (.getSuppressed ^Throwable failure))) + (when-let [owned @replacement] + (is (.isAlive ^ProcessHandle (:process owned))) + (is (exists? (:socket owned)))) + (finally + (when-let [owned @replacement] + (.destroy ^ProcessHandle (:process owned)) + (await-exit! (:process owned)) + (Files/deleteIfExists (:socket owned)) + (Files/deleteIfExists (:config owned)) + (Files/deleteIfExists (.resolve (.getParent ^Path (:socket owned)) "tmux.conf")) + (Files/deleteIfExists (.getParent ^Path (:socket owned)))))))) + +(deftest cancellation-reclaims-the-owned-fixture-resources + (interrupted-fixture-cleans! {} :cancelled)) + +(deftest deadline-reclaims-the-owned-fixture-resources + (interrupted-fixture-cleans! {:timeout-ms 800} :timeout)) diff --git a/integration-tests/src/test/java/io/github/libtmux/it/BuffersAndClientIntegrationTest.java b/integration-tests/src/test/java/io/github/libtmux/it/BuffersAndClientIntegrationTest.java index fe8e3c9cb..2f31d7699 100644 --- a/integration-tests/src/test/java/io/github/libtmux/it/BuffersAndClientIntegrationTest.java +++ b/integration-tests/src/test/java/io/github/libtmux/it/BuffersAndClientIntegrationTest.java @@ -11,7 +11,6 @@ import io.github.libtmux.Server; import io.github.libtmux.Session; import io.github.libtmux.TmuxVersion; -import io.github.libtmux.control.ControlClient; import io.github.libtmux.exception.LibTmuxException; import io.github.libtmux.exception.ServerUnavailableException; import io.github.libtmux.exception.TargetGoneException; @@ -238,14 +237,15 @@ void sourcingAFileRunsTheCommandsInIt(Server server, @TempDir Path directory) th // --------------------------------------------------------------------------------- client /** - * The fixture session is detached, so a client has to be made. A control client attaches, which - * is what gives tmux a client to report at all. + * The fixture session is detached, so an ordinary terminal client supplies the attachment + * these reads report. */ @Test void anAttachedClientReportsWhatItIsLookingAt(Server server) throws Exception { Session session = server.sessions().get(0); - try (ControlClient attached = server.control(session)) { - assertTrue(Await.until(() -> !server.clients().isEmpty()), "the control client never appeared as a client"); + try (NativeClient attached = NativeClient.attach(session)) { + assertTrue( + Await.until(() -> !server.clients().isEmpty()), "the ordinary client never appeared as a client"); Client client = server.clients().get(0); ClientAttachment looking = client.attachment().orElseThrow(); @@ -256,15 +256,17 @@ void anAttachedClientReportsWhatItIsLookingAt(Server server) throws Exception { looking.activeWindow().id(), "the window it reports is the session's active one"); assertTrue(looking.activePane().active(), "and the pane is that window's active pane"); - assertEquals(attached.send("display-message", "-p", "ok").lines(), List.of("ok")); + assertTrue(attached.isAlive()); + assertEquals( + List.of("ok"), server.cmd("display-message", "-p", "ok").stdout()); } } @Test void fetchingAnAttachmentTakesAFreshLook(Server server) throws Exception { Session session = server.sessions().get(0); - try (ControlClient attached = server.control(session)) { - assertTrue(attached.send("display-message", "-p", "ready").succeeded()); + try (NativeClient attached = NativeClient.attach(session)) { + assertTrue(attached.isAlive()); assertTrue(Await.until(() -> !server.clients().isEmpty())); Client client = server.clients().get(0); String before = client.attachment().orElseThrow().activeWindow().name(); @@ -288,8 +290,8 @@ void aClientThatHasGoneIsGoneToEveryRead(Server server) throws Exception { Set before = server.clients().stream().map(Client::name).collect(Collectors.toSet()); Client client; - try (ControlClient attached = server.control(session)) { - assertTrue(attached.send("display-message", "-p", "ready").succeeded()); + try (NativeClient attached = NativeClient.attach(session)) { + assertTrue(attached.isAlive()); assertTrue(Await.until(() -> appeared(server, before).isPresent()), "no client ever attached"); client = appeared(server, before).orElseThrow(); } diff --git a/integration-tests/src/test/java/io/github/libtmux/it/CapturedServerIntegrationTest.java b/integration-tests/src/test/java/io/github/libtmux/it/CapturedServerIntegrationTest.java new file mode 100644 index 000000000..0bb949736 --- /dev/null +++ b/integration-tests/src/test/java/io/github/libtmux/it/CapturedServerIntegrationTest.java @@ -0,0 +1,73 @@ +package io.github.libtmux.it; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.github.libtmux.CapturedServer; +import io.github.libtmux.Server; +import io.github.libtmux.Window; +import io.github.libtmux.junit5.TmuxExtension; +import io.github.libtmux.transport.CommandRequest; +import io.github.libtmux.transport.CommandResult; +import io.github.libtmux.transport.ProcessTransport; +import io.github.libtmux.transport.TmuxTransport; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(TmuxExtension.class) +final class CapturedServerIntegrationTest { + + @Test + void capturedHandlesRetainLinkedPlacementsAfterLiveChanges(Server server) { + assertTrue(server.cmd("new-session", "-d", "-s", "other").succeeded()); + assertTrue(server.cmd("link-window", "-s", "libtmux:0", "-t", "other:9").succeeded()); + AtomicInteger commands = new AtomicInteger(); + try (ProcessTransport processes = new ProcessTransport()) { + TmuxTransport counting = new TmuxTransport() { + @Override + public CommandResult execute(CommandRequest request) { + commands.incrementAndGet(); + return processes.execute(request); + } + + @Override + public void close() {} + }; + try (Server measured = Server.using(server.config(), counting)) { + CapturedServer captured = measured.captureServer(); + assertEquals(2, commands.get()); + Window first = captured.sessions().stream() + .filter(session -> session.name().equals("libtmux")) + .findFirst() + .orElseThrow() + .windows() + .getFirst(); + List links = captured.windows().stream() + .filter(window -> window.id().equals(first.id())) + .toList(); + assertEquals(2, links.size()); + assertNotEquals(links.getFirst().context(), links.getLast().context()); + assertEquals( + links.getFirst().panes().getFirst().id(), + links.getLast().panes().getFirst().id()); + assertTrue(server.cmd("rename-window", "-t", "libtmux:0", "changed-after-capture") + .succeeded()); + assertEquals(first.name(), links.getLast().name()); + assertNotEquals("changed-after-capture", first.name()); + assertEquals( + captured.snapshot().serverPid(), captured.identity().processId()); + assertEquals( + captured.snapshot().clients().size(), captured.clients().size()); + captured.clients().forEach(client -> client.attachment()); + captured.panes().forEach(pane -> pane.window().session().windows()); + assertEquals(2, commands.get(), "captured clients and relations issue no further commands"); + assertEquals( + "changed-after-capture", + server.window(first.context()).orElseThrow().name()); + } + } + } +} diff --git a/integration-tests/src/test/java/io/github/libtmux/it/ClientOperationsIntegrationTest.java b/integration-tests/src/test/java/io/github/libtmux/it/ClientOperationsIntegrationTest.java index a617dbd89..b57fdda6b 100644 --- a/integration-tests/src/test/java/io/github/libtmux/it/ClientOperationsIntegrationTest.java +++ b/integration-tests/src/test/java/io/github/libtmux/it/ClientOperationsIntegrationTest.java @@ -9,7 +9,6 @@ import io.github.libtmux.Server; import io.github.libtmux.Session; import io.github.libtmux.SessionId; -import io.github.libtmux.control.ControlClient; import io.github.libtmux.exception.LibTmuxException; import io.github.libtmux.junit5.TmuxExtension; import java.util.List; @@ -38,8 +37,8 @@ void aClientCanBeDetachedAndTheSessionSurvives(Server server) throws Exception { // and detaching must take that one rather than whichever tmux happens to list first. Set before = server.clients().stream().map(Client::name).collect(Collectors.toSet()); - try (ControlClient attached = server.control(session)) { - assertTrue(attached.send("display-message", "-p", "ready").succeeded()); + try (NativeClient attached = NativeClient.attach(session)) { + assertTrue(attached.isAlive()); assertTrue(Await.until(() -> appeared(server, before).isPresent()), "no client ever attached"); Client client = appeared(server, before).orElseThrow(); @@ -58,8 +57,8 @@ void aClientCanBeMovedToAnotherSession(Server server) throws Exception { Session first = server.sessions().get(0); Session second = server.newSession(s -> s.named("elsewhere")); - try (ControlClient attached = server.control(first)) { - assertTrue(attached.send("display-message", "-p", "ready").succeeded()); + try (NativeClient attached = NativeClient.attach(first)) { + assertTrue(attached.isAlive()); assertTrue(Await.until(() -> !server.clients().isEmpty())); Client client = server.clients().get(0); @@ -79,8 +78,8 @@ void aClientCanBeMovedToAnotherSession(Server server) throws Exception { void redrawingIsNotTheSameAsRecapturing(Server server) throws Exception { Session session = server.sessions().get(0); - try (ControlClient attached = server.control(session)) { - assertTrue(attached.send("display-message", "-p", "ready").succeeded()); + try (NativeClient attached = NativeClient.attach(session)) { + assertTrue(attached.isAlive()); assertTrue(Await.until(() -> !server.clients().isEmpty())); Client client = server.clients().get(0); @@ -94,10 +93,10 @@ void redrawingIsNotTheSameAsRecapturing(Server server) throws Exception { void detachingEveryOtherClientLeavesThisOneAttached(Server server) throws Exception { Session session = server.sessions().get(0); - try (ControlClient one = server.control(session); - ControlClient two = server.control(session)) { - assertTrue(one.send("display-message", "-p", "ready").succeeded()); - assertTrue(two.send("display-message", "-p", "ready").succeeded()); + try (NativeClient one = NativeClient.attach(session); + NativeClient two = NativeClient.attach(session)) { + assertTrue(one.isAlive()); + assertTrue(two.isAlive()); assertTrue(Await.until(() -> server.clients().size() >= 2), "two clients never attached"); Client survivor = server.clients().get(0); @@ -119,8 +118,8 @@ void onlyTheSessionsSomebodyIsAttachedToAreListed(Server server) throws Exceptio assertEquals(List.of(), server.attachedSessions(), "nothing is attached yet"); - try (ControlClient attached = server.control(watched)) { - assertTrue(attached.send("display-message", "-p", "ready").succeeded()); + try (NativeClient attached = NativeClient.attach(watched)) { + assertTrue(attached.isAlive()); assertTrue(Await.until(() -> !server.attachedSessions().isEmpty()), "no session ever became attached"); List listed = diff --git a/integration-tests/src/test/java/io/github/libtmux/it/CommandChainIntegrationTest.java b/integration-tests/src/test/java/io/github/libtmux/it/CommandChainIntegrationTest.java index 8b9d2d496..44e1934d0 100644 --- a/integration-tests/src/test/java/io/github/libtmux/it/CommandChainIntegrationTest.java +++ b/integration-tests/src/test/java/io/github/libtmux/it/CommandChainIntegrationTest.java @@ -14,7 +14,6 @@ import io.github.libtmux.batch.OperationOutcome; import io.github.libtmux.batch.OperationResult; import io.github.libtmux.exception.UnsupportedFeatureException; -import io.github.libtmux.format.RowFormat; import io.github.libtmux.junit5.TmuxExtension; import io.github.libtmux.transport.CommandRequest; import io.github.libtmux.transport.CommandResult; @@ -192,15 +191,13 @@ void arrangeAcceptsAJsonLayoutOnlyFromTheVersionThatWritesIt() { } } - /** A server that answers its own identity without a real tmux process behind it. */ + /** A server that answers its version without a real tmux process behind it. */ private static Server fakeServer(String version) { - String separator = RowFormat.of("field").separator(); TmuxTransport transport = new TmuxTransport() { @Override public CommandResult execute(CommandRequest request) { - if (request.commands().get(0).get(0).equals("display-message")) { - return new CommandResult( - 0, List.of(String.join(separator, "4242", version, "1790000000")), List.of()); + if (VersionProbes.is(request)) { + return VersionProbes.reply(request, version); } return new CommandResult(0, List.of(), List.of()); } diff --git a/integration-tests/src/test/java/io/github/libtmux/it/ControlAcquisitionIntegrationTest.java b/integration-tests/src/test/java/io/github/libtmux/it/ControlAcquisitionIntegrationTest.java new file mode 100644 index 000000000..3f442eef2 --- /dev/null +++ b/integration-tests/src/test/java/io/github/libtmux/it/ControlAcquisitionIntegrationTest.java @@ -0,0 +1,220 @@ +package io.github.libtmux.it; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.github.libtmux.Server; +import io.github.libtmux.ServerConfig; +import io.github.libtmux.ServerEndpoint; +import io.github.libtmux.control.ControlClient; +import io.github.libtmux.exception.LibTmuxException; +import io.github.libtmux.internal.ControlAcquisition; +import io.github.libtmux.junit5.NamedServerFixture; +import io.github.libtmux.junit5.TmuxExtension; +import io.github.libtmux.transport.CommandRequest; +import io.github.libtmux.transport.ControlCarrier; +import io.github.libtmux.transport.ProcessTransport; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +/** Native acquisition must retain its socket before starting a control process. */ +@Tag("control") +@ExtendWith(TmuxExtension.class) +final class ControlAcquisitionIntegrationTest { + private static final Duration LIMIT = Duration.ofSeconds(5); + + @Test + void replacingTheOriginalSocketCannotAcquireTheReplacement(Server server) throws Exception { + var snapshot = server.snapshot(); + var session = server.sessions().getFirst(); + AtomicReference alias = new AtomicReference<>(); + AtomicBoolean attempted = new AtomicBoolean(); + try (ProcessTransport transport = new ProcessTransport(); + Server replacement = Server.open(server.config())) { + ControlCarrier local = transport.controlCarrier().orElseThrow(); + ControlCarrier racing = new ControlCarrier() { + @Override + public PinnedSocket pinSocket(String socket, Duration timeout) throws IOException { + PinnedSocket pin = local.pinSocket(socket, timeout); + alias.set(Path.of(pin.socketPath())); + return pin; + } + + @Override + public Process start(List command) throws IOException { + if (command.contains("-C")) { + attempted.set(true); + server.killServer(); + replacement.newSession("replacement"); + replacement + .hooks() + .set("client-attached", List.of("set-option", "-g", "@unexpected-control", "yes")); + } + return local.start(command); + } + }; + try { + assertThrows( + LibTmuxException.class, + () -> ControlClient.attach( + racing, + server.config(), + session.id(), + snapshot.serverPid().orElseThrow(), + snapshot.serverStartTime(), + server.version().toString(), + LIMIT)); + assertTrue(attempted.get(), "the socket was not replaced immediately before -C"); + assertTrue(replacement.isAlive()); + assertTrue(replacement.clients().isEmpty()); + assertTrue( + replacement.globalOptions().get("@unexpected-control").isEmpty(), + "the replacement registered a control client before the post-attach fence"); + assertTrue(Files.notExists(alias.get()), "the refused attachment retained its alias"); + assertTrue(Files.notExists(alias.get().getParent())); + assertTrue( + transport + .execute(CommandRequest.of( + replacement.config().endpointCommand(), + List.of("display-message", "-p", "borrowed"), + LIMIT)) + .succeeded(), + "closing the client closed its borrowed carrier"); + } finally { + replacement.killServer(); + } + } + } + + @Test + void daemonExitRemovesThePinBeforeExplicitClientClose(Server server) throws Exception { + AtomicReference alias = new AtomicReference<>(); + try (ProcessTransport transport = new ProcessTransport()) { + ControlCarrier local = transport.controlCarrier().orElseThrow(); + ControlCarrier counted = new ControlCarrier() { + @Override + public Process start(List command) throws IOException { + return local.start(command); + } + + @Override + public PinnedSocket pinSocket(String socket, Duration timeout) throws IOException { + PinnedSocket pin = local.pinSocket(socket, timeout); + alias.set(Path.of(pin.socketPath())); + return pin; + } + }; + var snapshot = server.snapshot(); + try (ControlClient client = ControlClient.attach( + counted, + server.config(), + server.sessions().getFirst().id(), + snapshot.serverPid().orElseThrow(), + snapshot.serverStartTime(), + server.version().toString(), + LIMIT)) { + assertTrue(Files.exists(alias.get())); + server.killServer(); + assertTrue(Await.until(() -> Files.notExists(alias.get())), "natural EOF leaked the pin"); + assertTrue(Files.notExists(alias.get().getParent())); + assertFalse(client.isAlive()); + } + } + } + + @Test + void aValidLongSocketCanUseAShortAlias(Server fixture) throws Exception { + Path root = Files.createTempDirectory(Path.of("/tmp/libtmux-java-test"), "long-pin-"); + int padding = 100 - root.toString().getBytes(StandardCharsets.UTF_8).length - 3; + Path socket = Files.createDirectory(root.resolve("x".repeat(padding))).resolve("s"); + assertEquals(100, socket.toString().getBytes(StandardCharsets.UTF_8).length); + ServerConfig config = ServerConfig.builder() + .binary(fixture.config().binary()) + .endpoint(ServerEndpoint.socketPath(socket)) + .configFile(Path.of("/dev/null")) + .build(); + try (Server server = Server.open(config)) { + try { + server.newSession("long-socket"); + try (NamedServerFixture owned = NamedServerFixture.own(server, socket, root); + ControlClient client = server.control(server.sessions().getFirst())) { + assertEquals(socket.toRealPath(), owned.socket()); + assertEquals( + List.of("long-path"), + client.send("display-message", "-p", "long-path").lines()); + } + } finally { + server.killServer(); + } + } finally { + Files.deleteIfExists(root); + } + } + + /** The identity travels in one word, so a socket path with a space and a quote must survive it. */ + @Test + void anIdentityKeepsSpacesAndQuotesInTheSocketPath(Server fixture) throws Exception { + Path root = Files.createTempDirectory(Path.of("/tmp/libtmux-java-test"), "quoted-"); + Path directory = Files.createDirectory(root.resolve("it's a dir")); + Path socket = directory.resolve("a socket, with comma"); + ServerConfig config = ServerConfig.builder() + .binary(fixture.config().binary()) + .endpoint(ServerEndpoint.socketPath(socket)) + .configFile(Path.of("/dev/null")) + .build(); + try (Server server = Server.open(config)) { + try { + server.newSession("quoted-socket"); + try (ProcessTransport processes = new ProcessTransport(); + ControlAcquisition acquisition = ControlAcquisition.acquire( + processes.controlCarrier().orElseThrow(), config, Duration.ofSeconds(30))) { + assertEquals(socket.toString(), acquisition.identity().socket()); + } + } finally { + server.killServer(); + } + } finally { + Files.deleteIfExists(socket); + Files.deleteIfExists(directory); + Files.deleteIfExists(root); + } + } + + /** + * Qualifying a daemon for control reads its version and identity, rechecks both through the + * pin, and a user's {@code after-display-message} hook must not run for any of it. (Control + * replies are framed with {@code display-message} markers once a client is attached; that is + * the client's own traffic and is not part of acquisition.) The last step fires the hook the + * ordinary way, so a quiet record means the hook was live and not run. + */ + @Test + void qualifyingTheDaemonRunsNoAfterDisplayMessageHook(Server server) throws Exception { + assertTrue(server.cmd("set-hook", "-g", "after-display-message", "set-option -g @acquire-hook fired") + .succeeded()); + try (ProcessTransport processes = new ProcessTransport(); + ControlAcquisition acquisition = ControlAcquisition.acquire( + processes.controlCarrier().orElseThrow(), server.config(), Duration.ofSeconds(30))) { + acquisition.recheck(processes.controlCarrier().orElseThrow()); + assertTrue(acquisition.identity().pid() > 0); + } + + assertEquals( + List.of(), server.cmd("show-options", "-gqv", "@acquire-hook").stdout(), "acquisition ran the hook"); + assertTrue(server.cmd("display-message", "-p", "control").succeeded()); + assertEquals( + List.of("fired"), + server.cmd("show-options", "-gqv", "@acquire-hook").stdout(), + "the hook never fires, so the check above proves nothing"); + } +} diff --git a/integration-tests/src/test/java/io/github/libtmux/it/ControlFloorIntegrationTest.java b/integration-tests/src/test/java/io/github/libtmux/it/ControlFloorIntegrationTest.java new file mode 100644 index 000000000..f7d0dddef --- /dev/null +++ b/integration-tests/src/test/java/io/github/libtmux/it/ControlFloorIntegrationTest.java @@ -0,0 +1,59 @@ +package io.github.libtmux.it; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.github.libtmux.Pane; +import io.github.libtmux.Server; +import io.github.libtmux.TmuxVersion; +import io.github.libtmux.control.ControlClient; +import io.github.libtmux.exception.UnsupportedFeatureException; +import io.github.libtmux.internal.ControlAcquisition; +import io.github.libtmux.junit5.TmuxExtension; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +/** Control has its own floor; below it every lane asserts the refusal, above it the attach works. */ +@ExtendWith(TmuxExtension.class) +final class ControlFloorIntegrationTest { + private static final TmuxVersion CONTROL = new TmuxVersion(3, 7, ""); + + @Test + void controlRequiresItsOwnFloorAndLiteralBreakRequiresRepair(Server server) { + Pane pane = server.panes().getFirst(); + TmuxVersion running = server.version(); + if (!ControlAcquisition.supports(running)) { + assertThrows( + UnsupportedFeatureException.class, + () -> server.control(server.sessions().getFirst())); + assertThrows( + UnsupportedFeatureException.class, + () -> ControlClient.attachUnfenced( + server.config(), server.sessions().getFirst().id())); + assertTrue(server.clients().isEmpty()); + return; + } + try (ControlClient client = server.control(server.sessions().getFirst())) { + assertTrue(client.isAlive()); + if (running.equals(CONTROL)) { + assertThrows( + UnsupportedFeatureException.class, + () -> client.send( + "break-pane", "-n", "bad", "-s", pane.id().value())); + assertThrows( + UnsupportedFeatureException.class, + () -> server.cmd("breakp", "-n", "bad", "-s", pane.id().value())); + assertTrue(client.isAlive()); + } + } + try (ControlClient client = ControlClient.attachUnfenced( + server.config(), server.sessions().getFirst().id())) { + assertEquals( + List.of("roundtrip"), + client.send("display-message", "-p", "roundtrip").lines()); + } + assertTrue(server.isAlive()); + } +} diff --git a/integration-tests/src/test/java/io/github/libtmux/it/ControlModeIntegrationTest.java b/integration-tests/src/test/java/io/github/libtmux/it/ControlModeIntegrationTest.java index 5d059bbbf..bea924ccd 100644 --- a/integration-tests/src/test/java/io/github/libtmux/it/ControlModeIntegrationTest.java +++ b/integration-tests/src/test/java/io/github/libtmux/it/ControlModeIntegrationTest.java @@ -27,6 +27,7 @@ import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -37,6 +38,7 @@ * failed and which never ran. Control-mode requests are independent: the request after a failure * still runs, and every reply carries the request that produced it. */ +@Tag("control") @ExtendWith(TmuxExtension.class) final class ControlModeIntegrationTest { diff --git a/integration-tests/src/test/java/io/github/libtmux/it/ControlSoakIntegrationTest.java b/integration-tests/src/test/java/io/github/libtmux/it/ControlSoakIntegrationTest.java index 54bd36c45..b25a3f827 100644 --- a/integration-tests/src/test/java/io/github/libtmux/it/ControlSoakIntegrationTest.java +++ b/integration-tests/src/test/java/io/github/libtmux/it/ControlSoakIntegrationTest.java @@ -16,6 +16,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -26,6 +27,7 @@ * longer usable, or the transport says how far the request got. Nothing may hang, and the server * must answer afterwards. */ +@Tag("control") @ExtendWith(TmuxExtension.class) final class ControlSoakIntegrationTest { diff --git a/integration-tests/src/test/java/io/github/libtmux/it/ControlWatchIntegrationTest.java b/integration-tests/src/test/java/io/github/libtmux/it/ControlWatchIntegrationTest.java index a139908a4..f42ff125f 100644 --- a/integration-tests/src/test/java/io/github/libtmux/it/ControlWatchIntegrationTest.java +++ b/integration-tests/src/test/java/io/github/libtmux/it/ControlWatchIntegrationTest.java @@ -17,6 +17,7 @@ import java.time.Duration; import java.util.Optional; import java.util.function.Predicate; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -27,6 +28,7 @@ * differs, so a client that subscribes does nothing at all between changes. That is what makes * watching a server cost nothing while it is idle, and it is measured here rather than assumed. */ +@Tag("control") @ExtendWith(TmuxExtension.class) final class ControlWatchIntegrationTest { diff --git a/integration-tests/src/test/java/io/github/libtmux/it/CreationIntegrationTest.java b/integration-tests/src/test/java/io/github/libtmux/it/CreationIntegrationTest.java index 6e0edf29f..e7704515c 100644 --- a/integration-tests/src/test/java/io/github/libtmux/it/CreationIntegrationTest.java +++ b/integration-tests/src/test/java/io/github/libtmux/it/CreationIntegrationTest.java @@ -11,11 +11,9 @@ import io.github.libtmux.Server; import io.github.libtmux.Session; import io.github.libtmux.SessionSpec; -import io.github.libtmux.TmuxVersion; import io.github.libtmux.Window; import io.github.libtmux.WindowSpec; import io.github.libtmux.exception.TargetGoneException; -import io.github.libtmux.exception.UnsupportedFeatureException; import io.github.libtmux.junit5.TmuxExtension; import java.nio.file.Files; import java.nio.file.Path; @@ -29,15 +27,11 @@ /** * Making windows and sessions against a real tmux, on whichever release the lane is running. * - *

Neither command changed its flags across the supported range, so what is version-dependent here - * is behaviour 3.2a accepts and then ignores: a detached session's size, and a relative start - * directory. Both branches assert. + *

Sizes and working directories must describe the created process on every supported daemon. */ @ExtendWith(TmuxExtension.class) final class CreationIntegrationTest { - private static final TmuxVersion SIZE_SINCE = new TmuxVersion(3, 3, ""); - // ------------------------------------------------------------------------------ new-window @ParameterizedTest @@ -236,23 +230,11 @@ void aSessionCanNameItsFirstWindowAndRunSomethingInIt(Server server) throws Inte assertTrue(Await.until(() -> "sleep".equals(pane.refresh().currentCommand()))); } - /** 3.2a accepts {@code -x}/{@code -y} for a detached session and gives it the default size. */ @Test - void aSizeIsHonouredOrRefusedDependingOnTheRelease(Server server) { + void aDetachedSessionHonoursItsSize(Server server) { Dimensions wanted = new Dimensions(100, 40); - - if (server.version().atLeast(SIZE_SINCE)) { - Session sized = server.newSession(s -> s.named("sized").sized(wanted)); - - assertEquals(wanted, sized.windows().get(0).size()); - } else { - assertThrows( - UnsupportedFeatureException.class, - () -> server.newSession(s -> s.named("sized").sized(wanted))); - assertTrue( - server.sessions().stream().noneMatch(session -> "sized".equals(session.name())), - "a refused spec must not have reached tmux"); - } + Session sized = server.newSession(s -> s.named("sized").sized(wanted)); + assertEquals(wanted, sized.windows().getFirst().size()); } @Test @@ -273,34 +255,21 @@ void aSessionCanBeMadeBeforeAnythingHasAskedTheServerItsVersion(@TempDir Path di } } - /** - * Sizing is exactly the one thing {@link SessionSpec#argv} needs the version for, and a - * fresh socket has no daemon yet to ask {@link Server#version()}. The version has to come from - * somewhere that does not need one already running — here, the binary itself. - */ @Test - void aSizedSessionCanBeMadeBeforeAnythingHasAskedTheServerItsVersion(Server server, @TempDir Path directory) - throws Exception { + void aSizedSessionCanBeMadeOnAFreshEndpoint(@TempDir Path directory) throws Exception { Path config = directory.resolve("empty.conf"); Files.writeString(config, ""); - Path socket = directory.resolve("s"); Dimensions wanted = new Dimensions(120, 40); - try (Server fresh = Server.open(io.github.libtmux.ServerConfig.builder() .binary(System.getProperty("libtmux.tmux", "tmux")) - .endpoint(io.github.libtmux.ServerEndpoint.socketPath(socket)) + .endpoint(io.github.libtmux.ServerEndpoint.socketPath(directory.resolve("s"))) .configFile(config) .build())) { - if (server.version().atLeast(SIZE_SINCE)) { + try { Session sized = fresh.newSession(s -> s.named("sized").sized(wanted)); - - assertEquals(wanted, sized.windows().get(0).size()); + assertEquals(wanted, sized.windows().getFirst().size()); + } finally { fresh.killServer(); - } else { - assertThrows( - UnsupportedFeatureException.class, - () -> fresh.newSession(s -> s.named("sized").sized(wanted))); - assertTrue(Files.notExists(socket), "a refused spec must not have started the daemon"); } } } diff --git a/integration-tests/src/test/java/io/github/libtmux/it/FieldsIntegrationTest.java b/integration-tests/src/test/java/io/github/libtmux/it/FieldsIntegrationTest.java index 15f292d82..94cbd5920 100644 --- a/integration-tests/src/test/java/io/github/libtmux/it/FieldsIntegrationTest.java +++ b/integration-tests/src/test/java/io/github/libtmux/it/FieldsIntegrationTest.java @@ -46,21 +46,23 @@ void aNameThatIsNotATmuxVariableIsRefusedBeforeItIsSent(Server server) { } /** - * The defect this pins: reading eight names cost eight tmux processes, each at its own moment. - * Counted against the fake, which records every command it was sent. + * Names are read together, after one version probe for the daemon. Reading one name per process would observe + * them at different moments. */ @Test - void eachReadIsOneCommandHoweverManyNames() { + void eachReadIsOneDataCommandHoweverManyNames() { FakeTmux tmux = new FakeTmux(); tmux.addSession("work"); try (Server server = tmux.server()) { int before = tmux.sent().size(); server.variables(List.of("pid", "version", "session_name", "window_name")); - assertEquals(1, tmux.sent().size() - before, "four names, one command: " + tmux.sent()); + assertEquals(2, tmux.sent().size() - before, "one version probe and one combined field read"); + assertTrue(VersionProbes.is(tmux.sent().get(before))); before = tmux.sent().size(); server.paneFields(List.of("pane_tty", "pane_pid", "pane_current_command")); - assertEquals(1, tmux.sent().size() - before, "every pane, one command"); + assertEquals(1, tmux.sent().size() - before, "the daemon is known, so one read for every pane"); + assertTrue(!VersionProbes.is(tmux.sent().get(before))); } } } diff --git a/integration-tests/src/test/java/io/github/libtmux/it/HookIntegrationTest.java b/integration-tests/src/test/java/io/github/libtmux/it/HookIntegrationTest.java index 669a5bbb0..13e508fea 100644 --- a/integration-tests/src/test/java/io/github/libtmux/it/HookIntegrationTest.java +++ b/integration-tests/src/test/java/io/github/libtmux/it/HookIntegrationTest.java @@ -16,7 +16,7 @@ /** * Binding commands to tmux events, at each scope that holds them. * - *

{@code set-hook} declares the same flags from 3.2a to 3.7b, so nothing here is version-gated. + *

{@code set-hook} declares the same flags across the supported range, so nothing here is version-gated. * What is easy to get wrong is the scope: a hook set where it does not live is accepted and dropped. */ @ExtendWith(TmuxExtension.class) diff --git a/integration-tests/src/test/java/io/github/libtmux/it/NameValidationIntegrationTest.java b/integration-tests/src/test/java/io/github/libtmux/it/NameValidationIntegrationTest.java index 5d8efb957..c89e39338 100644 --- a/integration-tests/src/test/java/io/github/libtmux/it/NameValidationIntegrationTest.java +++ b/integration-tests/src/test/java/io/github/libtmux/it/NameValidationIntegrationTest.java @@ -29,13 +29,13 @@ private static boolean refuses(Server server) { } /** - * Measured on every supported build: rewritten to {@code a_b} through 3.6, refused on 3.7, kept + * Measured on every supported build: rewritten to {@code a_b} through 3.6, refused on 3.7 before dispatch, kept * as written from 3.7a. A caller who needs one answer has to keep delimiters out of the name. */ @Test void aDelimiterInASessionNameIsRewrittenRefusedOrKeptDependingOnTheTmux(Server server) { if (refuses(server)) { - assertThrows(LibTmuxException.class, () -> server.newSession("a:b")); + assertThrows(IllegalArgumentException.class, () -> server.newSession("a:b")); return; } @@ -109,7 +109,7 @@ void aDelimiterInAWindowNameIsKeptEverywhereExceptTheOneReleaseThatRefusesIt(Ser Window window = server.sessions().get(0).windows().get(0); if (refuses(server)) { - assertThrows(LibTmuxException.class, () -> window.rename("w:x")); + assertThrows(IllegalArgumentException.class, () -> window.rename("w:x")); return; } diff --git a/integration-tests/src/test/java/io/github/libtmux/it/NativeClient.java b/integration-tests/src/test/java/io/github/libtmux/it/NativeClient.java new file mode 100644 index 000000000..bfc577032 --- /dev/null +++ b/integration-tests/src/test/java/io/github/libtmux/it/NativeClient.java @@ -0,0 +1,160 @@ +package io.github.libtmux.it; + +import io.github.libtmux.Client; +import io.github.libtmux.Server; +import io.github.libtmux.Session; +import io.github.libtmux.internal.CommandStrings; +import io.github.libtmux.internal.ProcessTree; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** Owns an ordinary terminal client for a local test server. */ +final class NativeClient implements AutoCloseable { + private static final String DRIVER = helper() + """ + import fcntl, pty, signal, sys, termios + master, slave = pty.openpty() + os.set_blocking(master, False) + def terminal(): + os.setsid() + fcntl.ioctl(slave, termios.TIOCSCTTY, 0) + signal.signal(signal.SIGTERM, lambda *_: sys.exit(0)) + child = None + trace = bytearray() + try: + child = subprocess.Popen(sys.argv[1:], stdin=slave, stdout=slave, + stderr=slave, env=dict(os.environ, TERM='xterm'), preexec_fn=terminal) + os.close(slave) + slave = None + while child.poll() is None: + chunk = drain_pty(master) + trace.extend(chunk[:max(0, 65536-len(trace))]) + if 0 in select.select([0, master], [], [], .025)[0]: + data = os.read(0, 65536) + if not data: break + os.write(master, data) + finally: + if child is not None: + if child.poll() is None: child.terminate() + try: wait_pty(child, master, 2) + except subprocess.TimeoutExpired: + child.kill() + wait_pty(child, master, 2) + if child.returncode not in (0, -signal.SIGTERM): + sys.stderr.buffer.write(trace) + if slave is not None: os.close(slave) + os.close(master) + """; + private final Server server; + private final Process process; + private final ProcessTree tree; + private final Path log; + private String name = ""; + + private NativeClient(Server server, Process process, Path log) { + this.server = server; + this.process = process; + this.tree = new ProcessTree(process); + this.log = log; + } + + static NativeClient attach(Session session) throws Exception { + Server server = session.server(); + Set before = server.clients().stream().map(Client::name).collect(Collectors.toSet()); + Path log = Files.createTempFile(Path.of(System.getProperty("java.io.tmpdir")), "ordinary-client-", ".log"); + List command = new ArrayList<>(List.of("python3", "-u", "-c", DRIVER)); + command.addAll(server.config().endpointCommand()); + command.addAll(List.of("attach-session", "-t", session.id().value())); + ProcessBuilder builder = + new ProcessBuilder(command).redirectErrorStream(true).redirectOutput(log.toFile()); + builder.environment().remove("TMUX"); + builder.environment().remove("TMUX_PANE"); + Process process; + try { + process = builder.start(); + } catch (IOException failure) { + try { + Files.deleteIfExists(log); + } catch (IOException cleanup) { + failure.addSuppressed(cleanup); + } + throw failure; + } + NativeClient client = new NativeClient(server, process, log); + try { + if (!Await.until(() -> !client.process.isAlive() + || server.clients().stream().anyMatch(seen -> !before.contains(seen.name()))) + || !client.process.isAlive()) { + throw new IOException("ordinary client did not attach: " + Files.readString(log)); + } + client.name = server.clients().stream() + .map(Client::name) + .filter(seen -> !before.contains(seen)) + .findFirst() + .orElseThrow(() -> new IOException("ordinary client disappeared after attaching")); + return client; + } catch (Exception | Error failure) { + try { + client.close(); + } catch (IOException cleanup) { + failure.addSuppressed(cleanup); + } + throw failure; + } + } + + boolean isAlive() { + return process.isAlive(); + } + + void run(String... argv) throws Exception { + String marker = "@ordinary-client-" + UUID.randomUUID(); + String command = CommandStrings.group(List.of(List.of(argv), List.of("set-option", "-g", marker, "done"))); + try { + var prompt = server.cmd("command-prompt", "-b", "-t", name); + if (!prompt.succeeded()) throw new IOException("ordinary client prompt failed: " + prompt.stderr()); + process.getOutputStream().write((command + "\r").getBytes(StandardCharsets.UTF_8)); + process.getOutputStream().flush(); + if (!Await.until(() -> server.globalOptions().get(marker).isPresent())) { + throw new IOException("ordinary client did not complete its command: " + Files.readString(log)); + } + } finally { + server.globalOptions().unset(marker); + } + } + + @Override + public void close() throws IOException { + tree.captureDescendants(); + boolean reclaimed; + try { + process.getOutputStream().close(); + if (!process.waitFor(5, TimeUnit.SECONDS)) throw new IOException("ordinary client did not stop"); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw new IOException("interrupted while closing ordinary client", interrupted); + } finally { + reclaimed = tree.terminate(); + } + if (!reclaimed) throw new IOException("ordinary client process tree survived"); + if (process.exitValue() != 0) throw new IOException("ordinary client failed: " + Files.readString(log)); + Files.deleteIfExists(log); + } + + private static String helper() { + try (var source = NativeClient.class.getResourceAsStream("/io/github/libtmux/fixtures/pty.py")) { + if (source == null) throw new IllegalStateException("missing PTY test helper"); + return new String(source.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException failure) { + throw new UncheckedIOException(failure); + } + } +} diff --git a/integration-tests/src/test/java/io/github/libtmux/it/NavigationIntegrationTest.java b/integration-tests/src/test/java/io/github/libtmux/it/NavigationIntegrationTest.java index a140c6052..15f609ad6 100644 --- a/integration-tests/src/test/java/io/github/libtmux/it/NavigationIntegrationTest.java +++ b/integration-tests/src/test/java/io/github/libtmux/it/NavigationIntegrationTest.java @@ -11,7 +11,6 @@ import io.github.libtmux.Session; import io.github.libtmux.Window; import io.github.libtmux.WindowId; -import io.github.libtmux.control.ControlClient; import io.github.libtmux.exception.LibTmuxException; import io.github.libtmux.junit5.TmuxExtension; import org.junit.jupiter.api.Test; @@ -86,8 +85,8 @@ void lastWithNowhereToGoBackToIsReported(Server server) { @Test void detachingLeavesTheSessionRunning(Server server) throws Exception { Session session = server.sessions().get(0); - try (ControlClient attached = server.control(session)) { - assertTrue(attached.send("display-message", "-p", "ready").succeeded()); + try (NativeClient attached = NativeClient.attach(session)) { + assertTrue(attached.isAlive()); assertTrue(Await.until(() -> !server.clients().isEmpty())); session.detachClients(); diff --git a/integration-tests/src/test/java/io/github/libtmux/it/OptionWritesIntegrationTest.java b/integration-tests/src/test/java/io/github/libtmux/it/OptionWritesIntegrationTest.java index d12fb2add..2b36a3926 100644 --- a/integration-tests/src/test/java/io/github/libtmux/it/OptionWritesIntegrationTest.java +++ b/integration-tests/src/test/java/io/github/libtmux/it/OptionWritesIntegrationTest.java @@ -15,7 +15,7 @@ /** * Writing options in the ways tmux offers beyond replacing a value outright. * - *

{@code set-option} and {@code show-options} declare byte-identical flags from 3.2a to 3.7b, so + *

{@code set-option} and {@code show-options} declare byte-identical flags across the supported range, so * nothing here is version-gated. What varies between releases is how many options exist, which is * why no count below is asserted exactly. */ diff --git a/integration-tests/src/test/java/io/github/libtmux/it/OptionalFeatureIntegrationTest.java b/integration-tests/src/test/java/io/github/libtmux/it/OptionalFeatureIntegrationTest.java new file mode 100644 index 000000000..8b0f04af0 --- /dev/null +++ b/integration-tests/src/test/java/io/github/libtmux/it/OptionalFeatureIntegrationTest.java @@ -0,0 +1,205 @@ +package io.github.libtmux.it; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.github.libtmux.Pane; +import io.github.libtmux.Server; +import io.github.libtmux.TmuxVersion; +import io.github.libtmux.Window; +import io.github.libtmux.exception.UnsupportedFeatureException; +import io.github.libtmux.junit5.TmuxExtension; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +/** Selective refusals preserve ordinary operations at the core floor. */ +@ExtendWith(TmuxExtension.class) +final class OptionalFeatureIntegrationTest { + private static final TmuxVersion BUFFER = new TmuxVersion(3, 4, ""); + + @Test + void malformedLiteralLayoutsCannotApplyAnEarlierBatchCommand(Server server) { + Pane pane = server.panes().getFirst(); + if (!server.version().atLeast(BUFFER)) { + for (List command : List.of( + List.of("select-layout", "not-a-layout"), + List.of("selectl", "-t" + pane.id().value(), "not-a-layout"), + List.of("select-l", "--", "-n"), + List.of("select-layout", "-ot", pane.id().value(), "tile"), + List.of("select-layout", "0000,80x24,0,0,0"))) { + assertThrows(IllegalArgumentException.class, () -> server.cmd(command), command.toString()); + } + assertThrows( + IllegalArgumentException.class, + () -> server.batch() + .add("set-option", "-g", "@layout-effect", "changed") + .add("select-layout", "not-a-layout") + .run()); + assertThrows( + IllegalArgumentException.class, + () -> pane.batch() + .add("set-option", "-g", "@layout-effect", "changed") + .add("select-layout", "-o", "tiled") + .run()); + assertEquals( + List.of(), + server.cmd("show-options", "-gqv", "@layout-effect").stdout()); + } else { + assertFalse(server.cmd("select-layout", "not-a-layout").succeeded()); + } + assertTrue(server.isAlive()); + } + + @Test + void literalLayoutsKeepPresetsSerializedTreesAndIgnoredOperands(Server server) { + String target = server.panes().getFirst().id().value(); + assertTrue(server.cmd("select-l", "-t", target, "tile").succeeded()); + assertTrue(server.cmd("selectl", "-t" + target, "even-h").succeeded()); + String serialized = + server.cmd("display-message", "-p", "#{window_layout}").stdout().getFirst(); + assertTrue(server.cmd("select-layout", "-ot" + target, "--", serialized).succeeded()); + for (String flag : List.of("-n", "-p", "-E")) { + assertTrue(server.cmd("select-layout", flag, "not-a-layout").succeeded(), flag); + } + assertTrue(server.cmd("select-layout", "-o").succeeded()); + assertTrue(server.cmd("select-layout").succeeded()); + assertTrue(server.isAlive()); + } + + @Test + void namedBufferRefusalPrecedesEveryRawAndCapturedBatchEffect(Server server) { + server.buffers().set("user", "keep"); + Pane pane = server.panes().getFirst(); + if (!server.version().atLeast(BUFFER)) { + assertThrows(UnsupportedFeatureException.class, () -> server.cmd("del", "-bmissing")); + assertThrows(UnsupportedFeatureException.class, () -> server.cmd("setb", "-bmissing", "-nrenamed")); + assertThrows( + UnsupportedFeatureException.class, + () -> server.batch() + .add("set-option", "-g", "@forbidden", "changed") + .add("delete-buffer", "-b", "missing") + .run()); + assertThrows( + UnsupportedFeatureException.class, + () -> pane.batch() + .add("set-option", "-g", "@forbidden", "changed") + .add("set-buffer", "-b", "missing", "-n", "renamed") + .run()); + assertEquals( + List.of(), server.cmd("show-options", "-gqv", "@forbidden").stdout()); + assertEquals("keep", server.buffers().show("user")); + } else { + pane.batch().add("set-buffer", "-b", "user", "-n", "renamed").run(); + assertEquals("keep", server.buffers().show("renamed")); + assertFalse(server.cmd("delete-buffer", "-b", "missing").succeeded()); + assertEquals("keep", server.buffers().show("renamed")); + } + } + + @Test + void optionAliasesAndFlagValuesRetainTheirNativeMeaning(Server server) { + assertTrue(server.cmd("set", "-g", "@plain", "kept").succeeded()); + assertTrue(server.cmd("setw", "-g", "@window", "kept").succeeded()); + assertTrue(server.cmd("setb", "-b", "-n", "literal").succeeded()); + assertEquals("literal", server.buffers().show("-n")); + } + + @Test + void directNamedPasteWithDeleteRemainsAvailableAtTheCoreFloor(Server server) throws Exception { + Pane pane = server.panes().getFirst(); + pane.respawn("cat"); + assertTrue(Await.until(() -> pane.expand("#{pane_current_command}").equals("cat"))); + server.buffers().set("exact", "direct-retained"); + assertTrue( + server.cmd("paste-buffer", "-d", "-b", "exact", "-t", pane.id().value()) + .succeeded()); + assertTrue(Await.until(() -> String.join("\n", pane.capture()).contains("direct-retained"))); + assertTrue(server.buffers().list().isEmpty()); + } + + @Test + void managedPasteRequiresCleanupBeforeStagingOrCallingUserCode(Server server) throws Exception { + Pane pane = server.panes().getFirst(); + pane.respawn("cat"); + assertTrue(Await.until(() -> pane.expand("#{pane_current_command}").equals("cat"))); + server.buffers().set("user", "keep"); + AtomicBoolean callback = new AtomicBoolean(); + if (!server.version().atLeast(BUFFER)) { + assertThrows(UnsupportedFeatureException.class, () -> pane.paste("managed-forbidden")); + assertThrows( + UnsupportedFeatureException.class, () -> pane.paste("managed-forbidden", () -> callback.set(true))); + assertFalse(callback.get()); + assertEquals("keep", server.buffers().show("user")); + assertEquals(1, server.buffers().list().size()); + assertFalse(String.join("\n", pane.capture()).contains("managed-forbidden")); + } else { + pane.paste("managed-retained", () -> callback.set(true)); + assertTrue(callback.get()); + assertTrue(Await.until(() -> String.join("\n", pane.capture()).contains("managed-retained"))); + assertEquals(1, server.buffers().list().size()); + } + } + + @Test + void anUnnamedSinglePaneBreakPreservesTheExistingWindowName(Server server) { + Pane pane = server.panes().getFirst(); + assertEquals("preserved", pane.window().rename("preserved").name()); + if (server.version().equals(new TmuxVersion(3, 7, ""))) { + assertThrows(UnsupportedFeatureException.class, pane::breakOut); + assertEquals("preserved", server.windows().getFirst().name()); + } else assertEquals("preserved", pane.breakOut().name()); + assertTrue(server.isAlive()); + } + + @Test + void aNamedBreakRetainsItsNameOnSingleAndMultiplePaneWindows(Server server) { + Pane pane = server.panes().getFirst(); + Window single = pane.breakOut("single"); + assertEquals("single", single.name()); + Pane sibling = single.panes().getFirst().split(); + assertEquals("multiple", sibling.breakOut("multiple").name()); + assertEquals(2, server.windows().size()); + assertTrue(server.isAlive()); + } + + @Test + void percentageSplitsUseTheWorkingTypedFormOnExact34(Server server) { + Pane pane = server.panes().getFirst(); + if (server.version().equals(BUFFER)) { + assertThrows( + UnsupportedFeatureException.class, + () -> server.cmd("sp", "-p", "30", "-t", pane.id().value())); + assertEquals(1, server.panes().size()); + } else + assertTrue( + server.cmd("split-window", "-d", "-p", "30", "-t", pane.id().value()) + .succeeded()); + assertFalse(pane.split(spec -> spec.percent(30)).id().equals(pane.id())); + } + + @Test + void exactly37RefusesADelimiterInANameBeforeAnyEarlierCommandRuns(Server server) { + String name = "split.name"; + if (server.version().sameRelease(new TmuxVersion(3, 7, ""))) { + assertThrows( + IllegalArgumentException.class, + () -> server.batch() + .add("set-option", "-g", "@name-effect", "changed") + .add("new-window", "-d", "-n", name) + .run()); + assertThrows(IllegalArgumentException.class, () -> server.cmd("rename-session", "colon:name")); + assertThrows(IllegalArgumentException.class, () -> server.cmd("new-session", "-d", "-s", name)); + assertThrows(IllegalArgumentException.class, () -> server.cmd("new-window", "-dn" + name)); + assertEquals( + List.of(), + server.cmd("show-options", "-gqv", "@name-effect").stdout()); + assertEquals(1, server.windows().size()); + } else { + assertTrue(server.cmd("new-window", "-d", "-n", name).succeeded()); + } + } +} diff --git a/integration-tests/src/test/java/io/github/libtmux/it/PaneOperationsIntegrationTest.java b/integration-tests/src/test/java/io/github/libtmux/it/PaneOperationsIntegrationTest.java index 4ecde1686..21151685c 100644 --- a/integration-tests/src/test/java/io/github/libtmux/it/PaneOperationsIntegrationTest.java +++ b/integration-tests/src/test/java/io/github/libtmux/it/PaneOperationsIntegrationTest.java @@ -2,12 +2,15 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import io.github.libtmux.Pane; import io.github.libtmux.Server; import io.github.libtmux.Session; +import io.github.libtmux.TmuxVersion; import io.github.libtmux.Window; +import io.github.libtmux.exception.UnsupportedFeatureException; import io.github.libtmux.junit5.TmuxExtension; import java.util.List; import org.junit.jupiter.api.Test; @@ -24,8 +27,8 @@ final class PaneOperationsIntegrationTest { /** * On tmux 3.7 exactly, letting tmux name the window it breaks a pane into ends the server and - * every session on the socket. The window still has to appear, and the server still has to be - * there afterwards, on every supported release. + * every session on the socket. The unnamed call is refused there before effects; other + * supported releases retain native naming. */ @Test void breakingAPaneOutLeavesTheServerStanding(Server server) { @@ -33,6 +36,13 @@ void breakingAPaneOutLeavesTheServerStanding(Server server) { Window window = session.windows().get(0); Pane split = window.split(); + if (server.version().equals(new TmuxVersion(3, 7, ""))) { + assertThrows(UnsupportedFeatureException.class, split::breakOut); + assertTrue(server.isAlive()); + assertEquals(1, server.windows().size()); + assertEquals(2, server.panes().size()); + return; + } Window broken = split.breakOut(); assertTrue(server.isAlive(), "break-pane must not take the server with it"); @@ -65,9 +75,16 @@ void theUnnamedBreakIsNamedWhatTmuxWouldHaveNamedIt(Server server) throws Except Pane split = server.sessions().get(0).windows().get(0).split(); String command = awaitSettledCommand(split); + if (server.version().equals(new TmuxVersion(3, 7, ""))) { + assertThrows(UnsupportedFeatureException.class, split::breakOut); + assertTrue(server.isAlive()); + assertEquals(1, server.windows().size()); + assertEquals(2, server.panes().size()); + return; + } Window broken = split.breakOut(); - assertEquals(command, broken.name(), "the workaround must not change what the window is called"); + assertEquals(command, broken.name(), "native naming must be retained"); } /** Polls until two consecutive readings agree, so the startup transient is over. */ diff --git a/integration-tests/src/test/java/io/github/libtmux/it/PaneProcessIntegrationTest.java b/integration-tests/src/test/java/io/github/libtmux/it/PaneProcessIntegrationTest.java index f91aead07..6beefd1b7 100644 --- a/integration-tests/src/test/java/io/github/libtmux/it/PaneProcessIntegrationTest.java +++ b/integration-tests/src/test/java/io/github/libtmux/it/PaneProcessIntegrationTest.java @@ -22,7 +22,7 @@ * Restarting what a pane runs, watching what it prints, and asking tmux things a snapshot does not * carry. * - *

None of these commands changed a flag between 3.2a and 3.7b, so there is no version rule here — + *

None of these commands changed a flag across the supported range, so there is no version rule here — * only behaviour, checked on whichever release the lane runs. */ @ExtendWith(TmuxExtension.class) diff --git a/integration-tests/src/test/java/io/github/libtmux/it/PushdownIntegrationTest.java b/integration-tests/src/test/java/io/github/libtmux/it/PushdownIntegrationTest.java index 97287b22f..cd9a2add1 100644 --- a/integration-tests/src/test/java/io/github/libtmux/it/PushdownIntegrationTest.java +++ b/integration-tests/src/test/java/io/github/libtmux/it/PushdownIntegrationTest.java @@ -12,12 +12,10 @@ import io.github.libtmux.Window; import io.github.libtmux.WindowSpec; import io.github.libtmux.Window_; -import io.github.libtmux.exception.LibTmuxException; import io.github.libtmux.junit5.TmuxExtension; import io.github.libtmux.query.FilterExpr; import io.github.libtmux.query.TmuxFilters; import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; import java.util.regex.Pattern; import java.util.stream.Stream; @@ -45,9 +43,10 @@ void aPushedFilterSelectsWhatTheLocalFilterSelects(Server server) { for (String name : NAMES) { try { server.newSession(name); - } catch (LibTmuxException refused) { - // 3.7 rejects . and : in a name; earlier releases store them as _. - assertTrue(String.valueOf(refused.getMessage()).contains("invalid session name"), refused::toString); + } catch (IllegalArgumentException refused) { + // 3.7 rejects . and : in a name, and the library says so before dispatch; earlier + // releases store them as _. + assertTrue(String.valueOf(refused.getMessage()).contains("tmux 3.7 refuses"), refused::toString); continue; } accepted.add(name); @@ -126,16 +125,17 @@ void aPushedFilterSelectsWhatTheLocalFilterSelects(Server server) { */ @Test void aFilterThatMatchesNothingCostsOneFilteredRead(Server server) { - AtomicInteger dispatches = new AtomicInteger(); + List> verbs = new java.util.ArrayList<>(); try (Server counted = Server.open(server.config().toBuilder() - .observer(report -> dispatches.incrementAndGet()) + .observer(report -> verbs.add(report.verbs())) .build())) { List none = counted.sessions(Session_.name().is("no-such-session")); List nothing = counted.panes(Pane_.command().is("no-such-command")); assertEquals(List.of(), none); assertEquals(List.of(), nothing); - assertEquals(4, dispatches.get(), "two reads of two processes each, and no full capture after either"); + assertEquals(5, verbs.size(), "four data requests and the one version probe"); + assertEquals(1, verbs.stream().filter(List.of("run-shell")::equals).count()); } } diff --git a/integration-tests/src/test/java/io/github/libtmux/it/QuotingPropertyIntegrationTest.java b/integration-tests/src/test/java/io/github/libtmux/it/QuotingPropertyIntegrationTest.java index 2b42878a4..62280e727 100644 --- a/integration-tests/src/test/java/io/github/libtmux/it/QuotingPropertyIntegrationTest.java +++ b/integration-tests/src/test/java/io/github/libtmux/it/QuotingPropertyIntegrationTest.java @@ -1,6 +1,8 @@ package io.github.libtmux.it; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import io.github.libtmux.Server; import io.github.libtmux.TmuxVersion; @@ -9,6 +11,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Random; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -35,40 +38,54 @@ final class QuotingPropertyIntegrationTest { private static final String ALPHABET = "abcXYZ019 !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~é中😀"; @Test - void aValueSurvivesTheArgumentVectorAndTheControlLine(Server server) { + void aValueSurvivesTheArgumentVector(Server server) { + checkValues(server, null); + } + + @Test + @Tag("control") + void aValueSurvivesTheControlLine(Server server) { + try (ControlClient client = server.control(server.sessions().getFirst())) { + checkValues(server, client); + } + } + + private static void checkValues(Server server, @org.jspecify.annotations.Nullable ControlClient client) { List lost = new ArrayList<>(); - try (ControlClient client = server.control(server.sessions().get(0))) { - String alphabet = server.version().atLeast(new TmuxVersion(3, 4, "")) - && !server.version().atLeast(new TmuxVersion(3, 6, "")) - ? ALPHABET - : CONTROLS + ALPHABET; - for (String value : values(new Random(20260924L), 200, alphabet)) { - server.globalOptions().set("@vector", value); - String viaVector = server.globalOptions().get("@vector").orElse(""); - if (!viaVector.equals(value)) { - lost.add("vector " + quoted(value) + " -> " + quoted(viaVector)); - } - String viaFormat = server.expand("#{@vector}"); - if (!viaFormat.equals(value)) { - lost.add("format " + quoted(value) + " -> " + quoted(viaFormat)); - } - String viaListing = server.globalOptions().all().getOrDefault("@vector", ""); - if (!viaListing.equals(value)) { - lost.add("listing " + quoted(value) + " -> " + quoted(viaListing)); - } + String alphabet = server.version().atLeast(new TmuxVersion(3, 4, "")) + && !server.version().atLeast(new TmuxVersion(3, 6, "")) + ? ALPHABET + : CONTROLS + ALPHABET; + for (String value : values(new Random(20260924L), 200, alphabet)) { + server.globalOptions().set("@vector", value); + String viaVector = server.globalOptions().get("@vector").orElse(""); + if (!viaVector.equals(value)) { + lost.add("vector " + quoted(value) + " -> " + quoted(viaVector)); + } + String viaFormat = server.expand("#{@vector}"); + if (!viaFormat.equals(value)) { + lost.add("format " + quoted(value) + " -> " + quoted(viaFormat)); + } + String viaListing = server.globalOptions().all().getOrDefault("@vector", ""); + if (!viaListing.equals(value)) { + lost.add("listing " + quoted(value) + " -> " + quoted(viaListing)); + } + if (client != null) { client.send(List.of("set-option", "-g", "@line", value)); String viaLine = server.globalOptions().get("@line").orElse(""); if (!viaLine.equals(value)) { lost.add("line " + quoted(value) + " -> " + quoted(viaLine)); } + } - server.buffers().set("vector", value); - String viaBuffer = server.buffers().show("vector"); - String inBuffer = value.replaceAll("\n+$", ""); - if (!viaBuffer.equals(inBuffer)) { - lost.add("buffer " + quoted(value) + " -> " + quoted(viaBuffer)); - } + server.buffers().set("vector", value); + String viaBuffer = server.buffers().show("vector"); + String inBuffer = value.replaceAll("\n+$", ""); + if (!viaBuffer.equals(inBuffer)) { + lost.add("buffer " + quoted(value) + " -> " + quoted(viaBuffer)); + } + if (client != null) { client.send(List.of("set-buffer", "-b", "line", "--", value)); String viaLineBuffer = server.buffers().show("line"); if (!viaLineBuffer.equals(inBuffer)) { @@ -79,6 +96,39 @@ void aValueSurvivesTheArgumentVectorAndTheControlLine(Server server) { assertEquals(List.of(), lost); } + @Test + void aDelimiterWordDoesNotAddASecondCommand(Server server) { + server.buffers().set("user", "keep"); + assertFalse(server.cmd("set-option", "-g", "@delimiter", "changed", ";", "delete-buffer", "-bmissing") + .succeeded()); + assertTrue(server.globalOptions().get("@delimiter").isEmpty()); + assertEquals("keep", server.buffers().show("user")); + } + + @Test + @Tag("control") + void aDelimiterWordDoesNotAddAControlCommand(Server server) { + String name = server.windows().getFirst().name(); + try (ControlClient client = server.control(server.sessions().getFirst())) { + assertFalse(client.send( + "display-message", + "-p", + "literal", + ";", + "break-pane", + "-n", + "bad", + "-s", + server.panes().getFirst().id().value()) + .succeeded()); + assertEquals(name, server.windows().getFirst().name()); + assertEquals( + List.of("literal;"), + client.send("display-message", "-p", "literal;").lines()); + } + assertTrue(server.isAlive()); + } + private static List values(Random random, int count, String alphabet) { List values = new ArrayList<>(); int[] points = alphabet.codePoints().toArray(); diff --git a/integration-tests/src/test/java/io/github/libtmux/it/ServerControlIntegrationTest.java b/integration-tests/src/test/java/io/github/libtmux/it/ServerControlIntegrationTest.java index ec9ffce1b..7eb9c0e21 100644 --- a/integration-tests/src/test/java/io/github/libtmux/it/ServerControlIntegrationTest.java +++ b/integration-tests/src/test/java/io/github/libtmux/it/ServerControlIntegrationTest.java @@ -12,10 +12,11 @@ import io.github.libtmux.control.ControlClient; import io.github.libtmux.exception.LibTmuxException; import io.github.libtmux.exception.TargetGoneException; -import io.github.libtmux.exception.UnsupportedFeatureException; import io.github.libtmux.junit5.TmuxExtension; +import io.github.libtmux.transport.ProcessTransport; import java.time.Duration; import java.util.List; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -30,8 +31,6 @@ @ExtendWith(TmuxExtension.class) final class ServerControlIntegrationTest { - private static final TmuxVersion PROMPT_HISTORY_SINCE = new TmuxVersion(3, 3, ""); - /** Before 3.6, show-messages wants a client attached and refuses without one. */ private static final TmuxVersion MESSAGES_WITHOUT_CLIENT_SINCE = new TmuxVersion(3, 6, ""); @@ -75,6 +74,7 @@ void aFalseConditionWithNoOtherCommandDoesNothing(Server server) { } @Test + @Tag("control") void controlAttachesToTheCapturedServer(Server server) { Session session = server.sessions().get(0); @@ -90,6 +90,7 @@ void controlAttachesToTheCapturedServer(Server server) { /** A new server on the same socket reuses {@code $0}. The old handle must not attach to it. */ @Test + @Tag("control") void controlRefusesAServerThatHasBeenReplaced(Server server) { Session session = server.sessions().get(0); ServerConfig config = server.config(); @@ -107,6 +108,7 @@ void controlRefusesAServerThatHasBeenReplaced(Server server) { * with the old one, or its next command would be answered by the replacement. */ @Test + @Tag("control") void anAttachedClientEndsWhenItsServerIsReplaced(Server server) { Session session = server.sessions().get(0); String id = session.id().value(); @@ -133,9 +135,9 @@ void anAttachedClientEndsWhenItsServerIsReplaced(Server server) { * until every client has gone, so a server started straight afterwards could reach the old one. */ @Test - void killingTheServerWaitsForItsDaemonToExit(Server server) { + void killingTheServerWaitsForItsDaemonToExit(Server server) throws Exception { long pid = Long.parseLong(server.expand("#{pid}")); - ControlClient client = server.control(server.sessions().get(0)); + NativeClient client = NativeClient.attach(server.sessions().get(0)); try { server.killServer(); @@ -147,8 +149,9 @@ void killingTheServerWaitsForItsDaemonToExit(Server server) { } } - /** The check runs after the process is up, so a lie about the pid has to detach again. */ + /** A captured pid mismatch is refused before control acquisition. */ @Test + @Tag("control") void controlDetachesWhenTheIncarnationDoesNotMatch(Server server) { Session session = server.sessions().get(0); @@ -156,9 +159,7 @@ void controlDetachesWhenTheIncarnationDoesNotMatch(Server server) { TargetGoneException.class, () -> ControlClient.attach(server.config(), session.id(), 1, "0.0", Duration.ofSeconds(5))); - assertTrue( - Await.until(() -> noClients(server)), - "the control client stayed attached after the incarnation check failed"); + assertTrue(Await.until(() -> noClients(server)), "the refused identity left an attachment"); } /** @@ -166,22 +167,25 @@ void controlDetachesWhenTheIncarnationDoesNotMatch(Server server) { * restarted on its old pid shows a capture from before the restart. */ @Test - void controlDetachesFromAServerStartedAtAnotherTime(Server server) { + @Tag("control") + void controlRefusesAServerStartedAtAnotherTime(Server server) { Session session = server.sessions().get(0); var captured = server.snapshot(); String version = server.cmd("display-message", "-p", "#{version}").stdout().get(0); - assertThrows( - TargetGoneException.class, - () -> ControlClient.attach( - command -> new ProcessBuilder(command).start(), - server.config(), - session.id(), - captured.serverPid().orElseThrow(), - java.util.OptionalLong.of(captured.serverStartTime().orElseThrow() - 1), - version, - Duration.ofSeconds(5))); + try (ProcessTransport transport = new ProcessTransport()) { + assertThrows( + TargetGoneException.class, + () -> ControlClient.attach( + transport.controlCarrier().orElseThrow(), + server.config(), + session.id(), + captured.serverPid().orElseThrow(), + java.util.OptionalLong.of(captured.serverStartTime().orElseThrow() - 1), + version, + Duration.ofSeconds(5))); + } assertTrue( Await.until(() -> noClients(server)), @@ -225,54 +229,27 @@ void theMessageLogIsReadableDetachedFromThirtySixOnwards(Server server) { } } - /** The reason it is not gated: attach a client and the older releases answer too. */ @Test - void anOlderReleaseAnswersOnceAClientIsAttached(Server server) throws Exception { - if (server.version().atLeast(MESSAGES_WITHOUT_CLIENT_SINCE)) { - return; // nothing to prove; it already answers without one - } - Session session = server.sessions().get(0); - - try (ControlClient attached = server.control(session)) { - assertTrue(attached.send("display-message", "-p", "ready").succeeded()); - assertTrue(Await.until(() -> !server.clients().isEmpty()), "no client ever attached"); - - assertTrue(!server.messageLog().lines().isEmpty(), "with a client attached the log is readable after all"); + void theMessageLogIsReadableWithAnOrdinaryClientAttached(Server server) throws Exception { + try (NativeClient attached = NativeClient.attach(server.sessions().getFirst())) { + assertEquals( + List.of("0"), + server.cmd("list-clients", "-F", "#{client_control_mode}").stdout()); + attached.run("display-message", "ordinary-client-log-marker"); + assertTrue( + server.messageLog().lines().stream().anyMatch(line -> line.contains("ordinary-client-log-marker")), + "an ordinary client's message must remain readable"); } + assertTrue(Await.until(() -> noClients(server)), "closing the PTY left an attached client"); + assertTrue(server.isAlive(), "closing a client must preserve its server"); } // ------------------------------------------------------------------------------ prompt history - /** 3.2a has no such command; from 3.3 it answers, empty until something has been typed. */ - @Test - void thePromptHistoryIsReadableOrRefusedDependingOnTheRelease(Server server) { - if (server.version().atLeast(PROMPT_HISTORY_SINCE)) { - assertTrue(server.prompt().history() != null, "a readable history is a list, even when empty"); - server.prompt().clear(); - assertTrue(server.isAlive(), "clearing it is not a reason to lose the server"); - } else { - UnsupportedFeatureException refused = assertThrows( - UnsupportedFeatureException.class, () -> server.prompt().history()); - - assertTrue( - String.valueOf(refused.getMessage()).contains("3.3"), - "the refusal names the release that has it: " + refused.getMessage()); - assertThrows( - UnsupportedFeatureException.class, () -> server.prompt().clear()); - } - } - - /** The floor is exactly 3.2a, and every other lane has to take the working path. */ @Test - void exactlyTheOldestReleaseRefuses(Server server) { - String lane = System.getProperty("libtmux.tmux.expected"); - if (lane == null) { - return; // not a matrix lane; the ordinary suite runs whichever tmux is on PATH - } - - assertEquals( - "3.2a".equals(lane), - !server.version().atLeast(PROMPT_HISTORY_SINCE), - "lane " + lane + " disagrees with the version rule"); + void thePromptHistoryIsReadableAndClearable(Server server) { + assertTrue(server.prompt().history() != null, "a readable history is a list, even when empty"); + server.prompt().clear(); + assertTrue(server.isAlive(), "clearing it must keep the server alive"); } } diff --git a/integration-tests/src/test/java/io/github/libtmux/it/ServerMirrorIntegrationTest.java b/integration-tests/src/test/java/io/github/libtmux/it/ServerMirrorIntegrationTest.java index cedd0838e..f6a9446da 100644 --- a/integration-tests/src/test/java/io/github/libtmux/it/ServerMirrorIntegrationTest.java +++ b/integration-tests/src/test/java/io/github/libtmux/it/ServerMirrorIntegrationTest.java @@ -23,10 +23,12 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Predicate; import java.util.stream.Collectors; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; /** A mirror follows what tmux announces, and says so when it can no longer follow. */ +@Tag("control") @ExtendWith(TmuxExtension.class) final class ServerMirrorIntegrationTest { @@ -101,19 +103,23 @@ void aRebuildThatFindsNothingNewPublishesNothing(Server fixture) throws Exceptio ServerConfig counted = fixture.config().toBuilder() .observer(report -> dispatches.incrementAndGet()) .build(); - try (Server server = Server.open(counted); - ServerMirror mirror = ServerMirror.open(server.sessions().get(0))) { - Window window = server.sessions().get(0).windows().get(0); - long epoch = mirror.current().epoch(); - int before = dispatches.get(); - - server.cmd("rename-window", "-t", window.id().value(), window.name()); - - // The rename is one command; a rebuild is two more. - assertTrue( - Await.until(() -> dispatches.get() >= before + 3), - "tmux announced nothing, so this proves nothing"); - assertEquals(Optional.empty(), mirror.awaitNewer(epoch, Duration.ofMillis(300))); + // No shell, so no prompt retitles the pane and bumps the epoch under the test. + fixture.newSession(s -> s.named("quiet").firstWindowNamed("quiet").running("sleep", "600")); + try (Server server = Server.open(counted)) { + Session quiet = server.session("quiet").orElseThrow(); + try (ServerMirror mirror = ServerMirror.open(quiet)) { + Window window = quiet.windows().get(0); + long epoch = mirror.current().epoch(); + int before = dispatches.get(); + + server.cmd("rename-window", "-t", window.id().value(), window.name()); + + // The rename and the two rebuild requests, each on a daemon the version check already knows. + assertTrue( + Await.until(() -> dispatches.get() >= before + 3), + "tmux announced nothing, so this proves nothing"); + assertEquals(Optional.empty(), mirror.awaitNewer(epoch, Duration.ofMillis(300))); + } } } diff --git a/integration-tests/src/test/java/io/github/libtmux/it/ServerScriptingIntegrationTest.java b/integration-tests/src/test/java/io/github/libtmux/it/ServerScriptingIntegrationTest.java index 9f40903b5..4e776fcce 100644 --- a/integration-tests/src/test/java/io/github/libtmux/it/ServerScriptingIntegrationTest.java +++ b/integration-tests/src/test/java/io/github/libtmux/it/ServerScriptingIntegrationTest.java @@ -8,7 +8,6 @@ import io.github.libtmux.Server; import io.github.libtmux.TmuxFormats; import io.github.libtmux.TmuxVersion; -import io.github.libtmux.control.ControlClient; import io.github.libtmux.exception.LibTmuxException; import io.github.libtmux.exception.UnsupportedFeatureException; import io.github.libtmux.junit5.TmuxExtension; @@ -99,18 +98,12 @@ void aFormatInAnInterpolatedValueRunsUnlessItIsMadeLiteral(Server server, @TempD Path literal = directory.resolve("literal"); Path probe = directory.resolve("probe"); - // A control for #() dispatch itself, isolated from run-shell's own argument expansion: this - // tmux was observed on macOS never running the job at all, which no caller can work around. - server.expand("#(touch " + probe + ")"); - Assumptions.assumeTrue(Await.until(() -> Files.exists(probe)), "this tmux never dispatches a #() job"); - - // tmux cancels a client's asynchronous format jobs when that client disconnects. - // Keep the client alive through observation; a short-lived command races job cleanup. - try (ControlClient client = server.control(server.sessions().get(0))) { - assertTrue(client.send("run-shell", TmuxFormats.literal("echo '#(touch " + literal + ")' > /dev/null")) - .succeeded()); - assertTrue(client.send("run-shell", "echo '#(touch " + expanded + ")' > /dev/null") - .succeeded()); + // Keep the ordinary client alive while its asynchronous format jobs are observed. + try (NativeClient client = NativeClient.attach(server.sessions().getFirst())) { + client.run("display-message", "-p", "#(touch " + probe + ")"); + Assumptions.assumeTrue(Await.until(() -> Files.exists(probe)), "this tmux never dispatches a #() job"); + client.run("run-shell", TmuxFormats.literal("echo '#(touch " + literal + ")' > /dev/null")); + client.run("run-shell", "echo '#(touch " + expanded + ")' > /dev/null"); assertTrue(Await.until(() -> Files.exists(expanded)), "tmux expands a format inside shell quotes"); assertFalse(Files.exists(literal), "a literalized value must reach the shell as text"); diff --git a/integration-tests/src/test/java/io/github/libtmux/it/SnapshotIntegrationTest.java b/integration-tests/src/test/java/io/github/libtmux/it/SnapshotIntegrationTest.java index ab9df59a2..b9a9295bb 100644 --- a/integration-tests/src/test/java/io/github/libtmux/it/SnapshotIntegrationTest.java +++ b/integration-tests/src/test/java/io/github/libtmux/it/SnapshotIntegrationTest.java @@ -29,17 +29,21 @@ final class SnapshotIntegrationTest { /** - * Every command is a tmux process, so what a capture costs is how many it takes. One asks who - * the server is; one runs the four listings as a group, fenced against that answer. + * One request asks who the server is; one runs the four listings as a group, fenced against + * that answer. The daemon's version is checked once for both. */ @Test - void aCaptureCostsTwoCommands(Server server) { + void aCaptureCostsTwoDataRequestsAndOneVersionProbe(Server server) { AtomicInteger commands = new AtomicInteger(); + AtomicInteger probes = new AtomicInteger(); try (ProcessTransport processes = new ProcessTransport()) { TmuxTransport counting = new TmuxTransport() { @Override public CommandResult execute(CommandRequest request) { commands.incrementAndGet(); + if (VersionProbes.is(request)) { + probes.incrementAndGet(); + } return processes.execute(request); } @@ -50,7 +54,8 @@ public void close() {} ServerSnapshot captured = measured.snapshot(); assertEquals(1, captured.sessions().size()); - assertEquals(2, commands.get(), "one identity read, then the listings as one group"); + assertEquals(3, commands.get(), "one identity read, grouped listings, and one version probe"); + assertEquals(1, probes.get()); } } } diff --git a/integration-tests/src/test/java/io/github/libtmux/it/TmuxVersionIntegrationTest.java b/integration-tests/src/test/java/io/github/libtmux/it/TmuxVersionIntegrationTest.java index e34d1dde0..8f95b0c86 100644 --- a/integration-tests/src/test/java/io/github/libtmux/it/TmuxVersionIntegrationTest.java +++ b/integration-tests/src/test/java/io/github/libtmux/it/TmuxVersionIntegrationTest.java @@ -23,7 +23,7 @@ final class TmuxVersionIntegrationTest { void theServerReportsItsOwnVersion(Server server) { TmuxVersion version = server.version(); - assertTrue(version.atLeast(new TmuxVersion(3, 2, "a")), "this library supports 3.2a and later: " + version); + assertTrue(version.atLeast(new TmuxVersion(3, 3, "a")), "this library supports 3.3a and later: " + version); } /** diff --git a/integration-tests/src/test/java/io/github/libtmux/it/VersionCheckHooksIntegrationTest.java b/integration-tests/src/test/java/io/github/libtmux/it/VersionCheckHooksIntegrationTest.java new file mode 100644 index 000000000..23ae01de0 --- /dev/null +++ b/integration-tests/src/test/java/io/github/libtmux/it/VersionCheckHooksIntegrationTest.java @@ -0,0 +1,66 @@ +package io.github.libtmux.it; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.github.libtmux.Server; +import io.github.libtmux.TmuxVersion; +import io.github.libtmux.junit5.TmuxExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * The version probe must have no effects a user's hooks can see. + * + *

{@code display-message} fires {@code after-display-message} on every release, so a probe made + * with it ran a user's hook before a refusal could stop anything. Each test arms a hook that records + * that it fired, runs ordinary checked calls, and requires the record to stay empty; a control then + * fires the same hook the ordinary way, so an empty record means the hook was quiet and not that it + * was never armed. + */ +@ExtendWith(TmuxExtension.class) +final class VersionCheckHooksIntegrationTest { + + private static final TmuxVersion COMMAND_ERROR_HOOK = new TmuxVersion(3, 5, ""); + + @Test + void versionCheckFiresNoAfterDisplayMessageHook(Server server) { + assertTrue(server.cmd("set-hook", "-g", "after-display-message", "set-option -g @probe-hook fired") + .succeeded()); + + for (int call = 0; call < 3; call++) { + assertTrue(server.cmd("list-sessions").succeeded()); + } + server.requireCompatibleTmux(); + + assertEquals("", recorded(server), "the version check ran the user's after-display-message hook"); + + assertTrue(server.cmd("display-message", "-p", "control").succeeded()); + assertEquals("fired", recorded(server), "the hook never fires, so the check above proves nothing"); + } + + @Test + void versionCheckFiresNoCommandErrorHook(Server server) { + org.junit.jupiter.api.Assumptions.assumeTrue( + server.version().atLeast(COMMAND_ERROR_HOOK), "command-error is a hook from tmux 3.5"); + assertTrue(server.cmd("set-hook", "-g", "command-error", "set-option -g @probe-hook fired") + .succeeded()); + + for (int call = 0; call < 3; call++) { + assertTrue(server.cmd("list-sessions").succeeded()); + } + server.requireCompatibleTmux(); + + assertEquals("", recorded(server), "the version check ran the user's command-error hook"); + + assertFalse(server.cmd("kill-session", "-t", "=libtmux-no-such-session").succeeded()); + assertEquals("fired", recorded(server), "the hook never fires, so the check above proves nothing"); + } + + private static String recorded(Server server) { + // -q: an option never set is an empty answer, not an error, and raw cmd adds no hook itself. + return String.join( + "\n", server.cmd("show-options", "-gqv", "@probe-hook").stdout()); + } +} diff --git a/integration-tests/src/test/java/io/github/libtmux/it/VersionCheckLaneIntegrationTest.java b/integration-tests/src/test/java/io/github/libtmux/it/VersionCheckLaneIntegrationTest.java new file mode 100644 index 000000000..0b8a78a86 --- /dev/null +++ b/integration-tests/src/test/java/io/github/libtmux/it/VersionCheckLaneIntegrationTest.java @@ -0,0 +1,110 @@ +package io.github.libtmux.it; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.github.libtmux.Server; +import io.github.libtmux.ServerConfig; +import io.github.libtmux.ServerEndpoint; +import io.github.libtmux.TmuxVersion; +import io.github.libtmux.exception.UnsupportedFeatureException; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +/** + * The version check against the real binary of the lane. On a release below the floor every library call is + * refused and a sentinel session, option and buffer made with the binary's own client stays as it + * was; on a supported release the same calls work. The rejection lanes (3.2a and 3.3) run only this + * class, so nothing there is judged by a fake. + */ +@Tag("version-check") +final class VersionCheckLaneIntegrationTest { + + private static final TmuxVersion FLOOR = new TmuxVersion(3, 3, "a"); + + @Test + void theBinaryOfTheLaneIsCheckedOrRefusedBeforeAnyEffect() throws Exception { + String binary = System.getProperty("libtmux.tmux", "tmux"); + Path root = Files.createTempDirectory(Path.of("/tmp/libtmux-java-test"), "version-check-"); + Path socket = root.resolve("s"); + TmuxVersion running = + TmuxVersion.parse(raw(binary, socket, "-V").getFirst().replaceFirst("^\\S+ ", "")); + boolean supported = running.compareTo(FLOOR) >= 0; + try { + raw(binary, socket, "new-session", "-d", "-s", "sentinel"); + raw(binary, socket, "set-option", "-g", "@sentinel", "kept"); + raw(binary, socket, "set-buffer", "-b", "sentinel", "keep"); + + ServerConfig config = ServerConfig.builder() + .binary(binary) + .endpoint(ServerEndpoint.socketPath(socket)) + .configFile(Path.of("/dev/null")) + .build(); + try (Server server = Server.open(config)) { + if (supported) { + server.requireCompatibleTmux(); + assertTrue(server.cmd("set-option", "-g", "@sentinel", "changed") + .succeeded()); + server.newSession("added"); + assertEquals(List.of("changed"), raw(binary, socket, "show-options", "-gqv", "@sentinel")); + assertEquals( + List.of("added", "sentinel"), + raw(binary, socket, "list-sessions", "-F", "#{session_name}").stream() + .sorted() + .toList()); + return; + } + assertThrows(UnsupportedFeatureException.class, server::requireCompatibleTmux); + assertThrows( + UnsupportedFeatureException.class, + () -> server.cmd("set-option", "-g", "@sentinel", "changed")); + assertThrows( + UnsupportedFeatureException.class, + () -> server.batch() + .add("set-option", "-g", "@sentinel", "changed") + .run()); + assertThrows(UnsupportedFeatureException.class, () -> server.newSession("intruder")); + assertThrows(UnsupportedFeatureException.class, server::sessions); + assertThrows( + UnsupportedFeatureException.class, + () -> server.buffers().delete("sentinel")); + assertThrows(UnsupportedFeatureException.class, server::killServer); + } + assertEquals(List.of("kept"), raw(binary, socket, "show-options", "-gqv", "@sentinel")); + assertEquals(List.of("keep"), raw(binary, socket, "show-buffer", "-b", "sentinel")); + assertEquals(List.of("sentinel"), raw(binary, socket, "list-sessions", "-F", "#{session_name}")); + } finally { + try { + raw(binary, socket, "kill-server"); + } catch (IOException | AssertionError gone) { + // The server may already be gone. + } + Files.deleteIfExists(socket); + Files.deleteIfExists(root); + } + } + + /** The binary's own client, outside the library. */ + private static List raw(String binary, Path socket, String... arguments) throws Exception { + List command = new ArrayList<>(List.of(binary)); + if (!arguments[0].equals("-V")) { + command.addAll(List.of("-S", socket.toString(), "-f", "/dev/null")); + } + command.addAll(List.of(arguments)); + Process process = new ProcessBuilder(command).redirectErrorStream(true).start(); + String output = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + assertTrue(process.waitFor(30, TimeUnit.SECONDS), command.toString()); + if (process.exitValue() != 0) { + throw new AssertionError(command + " exited " + process.exitValue() + ": " + output); + } + return output.isEmpty() ? List.of() : List.of(output.stripTrailing().split("\n")); + } +} diff --git a/integration-tests/src/test/java/io/github/libtmux/it/VersionProbes.java b/integration-tests/src/test/java/io/github/libtmux/it/VersionProbes.java new file mode 100644 index 000000000..1265a82ef --- /dev/null +++ b/integration-tests/src/test/java/io/github/libtmux/it/VersionProbes.java @@ -0,0 +1,39 @@ +package io.github.libtmux.it; + +import io.github.libtmux.transport.CommandRequest; +import io.github.libtmux.transport.CommandResult; +import java.util.List; + +/** + * The version probe as a test double sees it, and tmux's answer to it. + * + *

The probe is {@code run-shell -C '#{version}'}: the daemon expands the format, fails to + * parse the result as a command, and spells the version in {@code unknown command: }. + */ +final class VersionProbes { + + private static final String MARKER = "libtmux-version-"; + private static final String FORMAT = "#{version}"; + + private VersionProbes() {} + + /** Whether this request is the version probe and nothing else. */ + static boolean is(CommandRequest request) { + return request.commands().size() == 1 && is(request.commands().getFirst()); + } + + static boolean is(List command) { + return command.size() == 3 + && command.get(0).equals("run-shell") + && command.get(1).equals("-C") + && command.get(2).startsWith(MARKER) + && command.get(2).endsWith(FORMAT); + } + + /** The reply of a daemon that reports this version. */ + static CommandResult reply(CommandRequest request, String version) { + String argument = request.commands().getFirst().get(2); + String marker = argument.substring(0, argument.length() - FORMAT.length()); + return new CommandResult(1, List.of(), List.of("unknown command: " + marker + version)); + } +} diff --git a/integration-tests/src/test/scala/io/github/libtmux/scaladsl/CatsLifecycleSuite.scala b/integration-tests/src/test/scala/io/github/libtmux/scaladsl/CatsLifecycleSuite.scala index 6fdba9285..56d1cdfd3 100644 --- a/integration-tests/src/test/scala/io/github/libtmux/scaladsl/CatsLifecycleSuite.scala +++ b/integration-tests/src/test/scala/io/github/libtmux/scaladsl/CatsLifecycleSuite.scala @@ -108,7 +108,7 @@ final class CatsLifecycleSuite extends FunSuite { } } yield () } - program.timeout(1.second).unsafeRunSync() + program.timeout(5.seconds).unsafeRunSync() } } @@ -160,7 +160,7 @@ final class CatsLifecycleSuite extends FunSuite { } } } - .timeout(1.second) + .timeout(5.seconds) .unsafeRunSync() } } diff --git a/integration-tests/src/test/scala/io/github/libtmux/scaladsl/ControlObservationSuite.scala b/integration-tests/src/test/scala/io/github/libtmux/scaladsl/ControlObservationSuite.scala index a9a3eb19d..5ac927887 100644 --- a/integration-tests/src/test/scala/io/github/libtmux/scaladsl/ControlObservationSuite.scala +++ b/integration-tests/src/test/scala/io/github/libtmux/scaladsl/ControlObservationSuite.scala @@ -94,6 +94,10 @@ final class ControlObservationSuite extends FunSuite { test("registered output retains pane identity across forced marker chunks") { OwnedTmux.use { fixture => + assume( + fixture.supportsControl, + "managed control requires tmux 3.7+" + ) val pane = fixture.server.panes().get(0) val workersBefore = Thread.getAllStackTraces .keySet() @@ -162,6 +166,10 @@ final class ControlObservationSuite extends FunSuite { test("a parked reader cancels and releases the single consumer slot") { OwnedTmux.use { fixture => + assume( + fixture.supportsControl, + "managed control requires tmux 3.7+" + ) attach(fixture) .use { control => control.output(4).use { observation => @@ -196,6 +204,10 @@ final class ControlObservationSuite extends FunSuite { test("attachment is lazy and failed acquisition reclaims its client") { OwnedTmux.use { fixture => + assume( + fixture.supportsControl, + "managed control requires tmux 3.7+" + ) val before = fixture.server.clients().size() val acquisition = Control .attachUnfenced[IO]( @@ -226,6 +238,10 @@ final class ControlObservationSuite extends FunSuite { test("typed notifications report overflow and reconcile current state") { OwnedTmux.use { fixture => + assume( + fixture.supportsControl, + "managed control requires tmux 3.7+" + ) val window = fixture.server.windows().get(0) window.options().set("automatic-rename", "off") attach(fixture) @@ -280,6 +296,10 @@ final class ControlObservationSuite extends FunSuite { "scope close ends a parked stream and server loss fails it with the client's cause" ) { OwnedTmux.use { fixture => + assume( + fixture.supportsControl, + "managed control requires tmux 3.7+" + ) val result = attach(fixture).allocated.flatMap { case (control, close) => control.output(4).allocated.flatMap { case (observation, unsubscribe) => val awaited = parked(observation.underlying) @@ -340,6 +360,10 @@ final class ControlObservationSuite extends FunSuite { "acknowledgement retains raw reply without implying deferred completion" ) { OwnedTmux.use { fixture => + assume( + fixture.supportsControl, + "managed control requires tmux 3.7+" + ) val marker = fixture.directory.resolve("ack-completed") val prefix = fixture.config .endpointCommand() @@ -388,6 +412,10 @@ final class ControlObservationSuite extends FunSuite { "cancelling a queued or a dispatched call ends only that call" ) { OwnedTmux.use { fixture => + assume( + fixture.supportsControl, + "managed control requires tmux 3.7+" + ) attach(fixture) .use { control => for { diff --git a/integration-tests/src/test/scala/io/github/libtmux/scaladsl/JavaBoundarySuite.scala b/integration-tests/src/test/scala/io/github/libtmux/scaladsl/JavaBoundarySuite.scala index 6ad2df1ba..2cb40f13e 100644 --- a/integration-tests/src/test/scala/io/github/libtmux/scaladsl/JavaBoundarySuite.scala +++ b/integration-tests/src/test/scala/io/github/libtmux/scaladsl/JavaBoundarySuite.scala @@ -21,10 +21,12 @@ final class JavaBoundarySuite extends FunSuite { () } - private def control(fixture: OwnedTmux): ControlClient = + private def control(fixture: OwnedTmux): ControlClient = { + assume(fixture.supportsControl, "managed control requires tmux 3.7+") fixture.own( fixture.server.control(fixture.server.sessions().get(0), deadline) ) + } test("pane refresh follows physical identity across linked occurrences") { OwnedTmux.use { fixture => diff --git a/integration-tests/src/test/scala/io/github/libtmux/scaladsl/LiveStateSuite.scala b/integration-tests/src/test/scala/io/github/libtmux/scaladsl/LiveStateSuite.scala index cc7f99674..5d785ea37 100644 --- a/integration-tests/src/test/scala/io/github/libtmux/scaladsl/LiveStateSuite.scala +++ b/integration-tests/src/test/scala/io/github/libtmux/scaladsl/LiveStateSuite.scala @@ -23,6 +23,10 @@ final class LiveStateSuite extends FunSuite { "LiveView observes a real window creation as a newer, materially different snapshot" ) { OwnedTmux.use { fixture => + assume( + fixture.supportsControl, + "managed control requires tmux 3.7+" + ) Using.resource(Server.open(fixture.config)) { server => val session = server.newSession("live-direct") Using.resource(LiveView.attach(session)) { live => @@ -62,6 +66,10 @@ final class LiveStateSuite extends FunSuite { "LiveView snapshots fail with the cause once the anchor session is gone" ) { OwnedTmux.use { fixture => + assume( + fixture.supportsControl, + "managed control requires tmux 3.7+" + ) Using.resource(Server.open(fixture.config)) { server => server.newSession("live-keep") val anchor = server.newSession("live-anchor") diff --git a/integration-tests/src/test/scala/io/github/libtmux/scaladsl/ObservationStressSuite.scala b/integration-tests/src/test/scala/io/github/libtmux/scaladsl/ObservationStressSuite.scala index a3afb622b..24acf004a 100644 --- a/integration-tests/src/test/scala/io/github/libtmux/scaladsl/ObservationStressSuite.scala +++ b/integration-tests/src/test/scala/io/github/libtmux/scaladsl/ObservationStressSuite.scala @@ -35,6 +35,10 @@ final class ObservationStressSuite extends FunSuite { "direct style: Observation's gap accounting is exact, and the marker survives overflow" ) { OwnedTmux.use { fixture => + assume( + fixture.supportsControl, + "managed control requires tmux 3.7+" + ) Using.resource(Server.open(fixture.config)) { server => val session = server.newSession("stress-direct") val pane = session.windows.head.panes.head diff --git a/integration-tests/src/test/scala/io/github/libtmux/scaladsl/QueryInteropSuite.scala b/integration-tests/src/test/scala/io/github/libtmux/scaladsl/QueryInteropSuite.scala index 52747216d..25c461835 100644 --- a/integration-tests/src/test/scala/io/github/libtmux/scaladsl/QueryInteropSuite.scala +++ b/integration-tests/src/test/scala/io/github/libtmux/scaladsl/QueryInteropSuite.scala @@ -72,7 +72,8 @@ final class QueryInteropSuite extends FunSuite { .succeeded() ) } - fixture.own(ControlClient.attachUnfenced(fixture.config, first.id())) + if (fixture.supportsControl) + fixture.own(ControlClient.attachUnfenced(fixture.config, first.id())) val sessions = server.sessions() val windows = sessions.flatMap(_.windows) val panes = windows.flatMap(_.panes) @@ -141,7 +142,7 @@ final class QueryInteropSuite extends FunSuite { ), sessions.filter(_.windows.exists(_.info.context.index().value() == 4)) ) - assert(clients.nonEmpty) + assertEquals(clients.nonEmpty, fixture.supportsControl) assertEquals( clients.matching( Expr(Client_.session().is(Session_.name().is("fixture"))) diff --git a/integration-tests/src/test/scala/io/github/libtmux/scaladsl/cats/CatsLiveServerSuite.scala b/integration-tests/src/test/scala/io/github/libtmux/scaladsl/cats/CatsLiveServerSuite.scala index ede8c949b..64bdcfebc 100644 --- a/integration-tests/src/test/scala/io/github/libtmux/scaladsl/cats/CatsLiveServerSuite.scala +++ b/integration-tests/src/test/scala/io/github/libtmux/scaladsl/cats/CatsLiveServerSuite.scala @@ -38,16 +38,24 @@ final class CatsLiveServerSuite extends FunSuite { } } yield result } - val (initial, observed, failure) = program.unsafeRunSync() - assert( - observed.epoch() > initial.epoch(), - s"expected a newer epoch, got ${observed.epoch()} after ${initial.epoch()}" - ) - assertEquals( - observed.snapshot().windows().size(), - initial.snapshot().windows().size() + 1 - ) - assertEquals(failure, None) + // A live view is managed control: below tmux 3.7 it is refused before attaching. + if (!fixture.supportsControl) { + intercept[io.github.libtmux.exception.UnsupportedFeatureException]( + program.unsafeRunSync() + ) + assert(fixture.server.clients().isEmpty()) + } else { + val (initial, observed, failure) = program.unsafeRunSync() + assert( + observed.epoch() > initial.epoch(), + s"expected a newer epoch, got ${observed.epoch()} after ${initial.epoch()}" + ) + assertEquals( + observed.snapshot().windows().size(), + initial.snapshot().windows().size() + 1 + ) + assertEquals(failure, None) + } } } } diff --git a/integration-tests/src/test/scala/io/github/libtmux/scaladsl/cats/CatsObservationStressSuite.scala b/integration-tests/src/test/scala/io/github/libtmux/scaladsl/cats/CatsObservationStressSuite.scala index 71308c560..731e2d64c 100644 --- a/integration-tests/src/test/scala/io/github/libtmux/scaladsl/cats/CatsObservationStressSuite.scala +++ b/integration-tests/src/test/scala/io/github/libtmux/scaladsl/cats/CatsObservationStressSuite.scala @@ -31,6 +31,10 @@ final class CatsObservationStressSuite extends FunSuite { "Cats: the poll/onReady fs2 bridge's gap accounting is exact, the marker survives overflow" ) { OwnedTmux.use { fixture => + assume( + fixture.supportsControl, + "managed control requires tmux 3.7+" + ) val events = new AtomicLong(0) val gaps = new AtomicLong(0) val gapMissed = new AtomicLong(0) diff --git a/integration-tests/src/test/scala/io/github/libtmux/scaladsl/fixture/OwnedTmuxSuite.scala b/integration-tests/src/test/scala/io/github/libtmux/scaladsl/fixture/OwnedTmuxSuite.scala index 3a842b320..4e2f9fc7d 100644 --- a/integration-tests/src/test/scala/io/github/libtmux/scaladsl/fixture/OwnedTmuxSuite.scala +++ b/integration-tests/src/test/scala/io/github/libtmux/scaladsl/fixture/OwnedTmuxSuite.scala @@ -11,6 +11,10 @@ import scala.util.Using final class OwnedTmuxSuite extends FunSuite { test("owned cleanup removes the daemon, socket, children and workers") { OwnedTmux.use { fixture => + assume( + fixture.supportsControl, + "managed control requires tmux 3.7+" + ) assert(fixture.server.isAlive()) val daemon = fixture.serverProcess val control = fixture.own( diff --git a/integration-tests/src/test/scala/io/github/libtmux/scaladsl/ox/OxFlowsSuite.scala b/integration-tests/src/test/scala/io/github/libtmux/scaladsl/ox/OxFlowsSuite.scala index 553d58d47..46cdf23c7 100644 --- a/integration-tests/src/test/scala/io/github/libtmux/scaladsl/ox/OxFlowsSuite.scala +++ b/integration-tests/src/test/scala/io/github/libtmux/scaladsl/ox/OxFlowsSuite.scala @@ -27,6 +27,10 @@ final class OxFlowsSuite extends FunSuite { "a subscription flow delivers what arrived before it was collected, then stops at take" ) { OwnedTmux.use { fixture => + assume( + fixture.supportsControl, + "managed control requires tmux 3.7+" + ) Using.resource(Server.open(fixture.config)) { server => val session = server.newSession("ox-events") Using.resource(server.control(session)) { control => @@ -47,6 +51,10 @@ final class OxFlowsSuite extends FunSuite { "a second collection of one subscription is refused while the first holds it" ) { OwnedTmux.use { fixture => + assume( + fixture.supportsControl, + "managed control requires tmux 3.7+" + ) Using.resource(Server.open(fixture.config)) { server => val session = server.newSession("ox-claim") Using.resource(server.control(session)) { control => @@ -81,6 +89,10 @@ final class OxFlowsSuite extends FunSuite { "a live-view flow starts from the current view, then sees a change made after it began" ) { OwnedTmux.use { fixture => + assume( + fixture.supportsControl, + "managed control requires tmux 3.7+" + ) Using.resource(Server.open(fixture.config)) { server => val session = server.newSession("ox-live") Using.resource(LiveView.attach(session)) { live => diff --git a/integration-tests/src/testFixtures/scala/io/github/libtmux/scaladsl/fixture/OwnedTmux.scala b/integration-tests/src/testFixtures/scala/io/github/libtmux/scaladsl/fixture/OwnedTmux.scala index bee64876c..30dd0cc51 100644 --- a/integration-tests/src/testFixtures/scala/io/github/libtmux/scaladsl/fixture/OwnedTmux.scala +++ b/integration-tests/src/testFixtures/scala/io/github/libtmux/scaladsl/fixture/OwnedTmux.scala @@ -1,6 +1,12 @@ package io.github.libtmux.scaladsl.fixture -import io.github.libtmux.{Server, ServerConfig, ServerEndpoint, SessionSpec} +import io.github.libtmux.{ + Server, + ServerConfig, + ServerEndpoint, + SessionSpec, + TmuxVersion +} import io.github.libtmux.junit5.NamedServerFixture import java.nio.file.{Files, LinkOption, Path} import java.time.Duration @@ -23,6 +29,10 @@ private[scaladsl] final class OwnedTmux private ( private var process: Option[ProcessHandle] = None private var closed = false + def supportsControl: Boolean = { + io.github.libtmux.internal.ControlAcquisition.supports(server.version()) + } + def serverProcess: ProcessHandle = process.getOrElse( throw new IllegalStateException("fixture has not started") diff --git a/libtmux-bom/README.md b/libtmux-bom/README.md index abfaff54e..fb0205cd0 100644 --- a/libtmux-bom/README.md +++ b/libtmux-bom/README.md @@ -74,6 +74,8 @@ doing its job. [`libtmux`](../libtmux/) · [`libtmux-jackson`](../libtmux-jackson/) · [`libtmux-junit5`](../libtmux-junit5/) · [`libtmux-kotlin`](../libtmux-kotlin/) · +[`libtmux-clojure`](../libtmux-clojure/) · +[`libtmux-clojure-core-async`](../libtmux-clojure-core-async/) · [`libtmux-mcp`](../libtmux-mcp/) · [`libtmux-workspace`](../libtmux-workspace/) · [`libtmux-workspace-cli`](../libtmux-workspace-cli/) · [`libtmux-scala`](../libtmux-scala/) · [`libtmux-scala-cats`](../libtmux-scala-cats/) · diff --git a/libtmux-bom/build.gradle.kts b/libtmux-bom/build.gradle.kts index 37eb6a3d3..e97d8986d 100644 --- a/libtmux-bom/build.gradle.kts +++ b/libtmux-bom/build.gradle.kts @@ -19,6 +19,8 @@ dependencies { api(project(":libtmux")) api(project(":libtmux-jackson")) api(project(":libtmux-kotlin")) + api(project(":libtmux-clojure")) + api(project(":libtmux-clojure-core-async")) api(project(":libtmux-junit5")) api(project(":libtmux-mcp")) api(project(":libtmux-workspace")) diff --git a/libtmux-clojure-core-async/README.md b/libtmux-clojure-core-async/README.md new file mode 100644 index 000000000..f2dc7398e --- /dev/null +++ b/libtmux-clojure-core-async/README.md @@ -0,0 +1,25 @@ +# libtmux-clojure-core-async + +**Optional core.async integration for libtmux-clojure.** + +The Maven coordinate is `io.github.libtmux:libtmux-clojure-core-async`. Add it +only when an application uses `libtmux.core-async`; `libtmux-clojure` itself +does not depend on core.async. + +```xml + + io.github.libtmux + libtmux-clojure-core-async + 0.0.1-alpha.17 + +``` + +> **Alpha.** The API will change without notice. Pin an exact version; it is +> not recommended for production. + +Requires JDK 25 or newer and tmux 3.3a through 3.7c; control needs tmux 3.7 or newer. + +The adapter owns its event and terminal channels. Its stop function closes its +Java subscription and must be called when the application is finished. The +[Clojure guide](../docs/guide/clojure.md) describes the ownership and loss +boundaries. diff --git a/libtmux-clojure-core-async/build.gradle.kts b/libtmux-clojure-core-async/build.gradle.kts new file mode 100644 index 000000000..93652b3d3 --- /dev/null +++ b/libtmux-clojure-core-async/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { id("libtmux.clojure-library") } + +extra["clojureTestNamespaces"] = + "libtmux.core-async-test,libtmux.internal.core-async-package-test" + +dependencies { + api(project(":libtmux-clojure")) + api(libs.core.async) +} + +tasks.withType().configureEach { + systemProperty("libtmux.clojure.main-namespaces", "libtmux.core-async") +} diff --git a/libtmux-clojure-core-async/src/main/clojure/libtmux/core_async.clj b/libtmux-clojure-core-async/src/main/clojure/libtmux/core_async.clj new file mode 100644 index 000000000..3c5198595 --- /dev/null +++ b/libtmux-clojure-core-async/src/main/clojure/libtmux/core_async.clj @@ -0,0 +1,127 @@ +(ns libtmux.core-async + "Closeable core.async observation adapter for libtmux control streams." + (:require [clojure.core.async :as async] + [clojure.core.async.impl.protocols :as protocols] + [libtmux.control :as control]) + (:import [java.lang AutoCloseable])) + +(set! *warn-on-reflection* true) + +(declare stop!) + +(deftype ^:private EventsPort [channel stop-fn] + protocols/ReadPort + (take! [_ handler] (protocols/take! channel handler)) + protocols/Channel + (close! [_] + (stop-fn) + (protocols/close! channel)) + (closed? [_] (protocols/closed? channel))) + +(deftype Adapter [events terminal subscription stop-channel stopped worker limits] + AutoCloseable + (close [this] (stop! this))) + +(defn events + "Returns the read-only events channel. Closing it stops the adapter." + [^Adapter adapter] + (.-events adapter)) + +(defn terminal + "Returns a buffered channel that delivers one outcome after cleanup." + [^Adapter adapter] + (.-terminal adapter)) + +(defn limits + "Returns the adapter's exact retained-event bound." + [^Adapter adapter] + (.-limits adapter)) + +(defn stop! + "Stops observation and closes only the owned subscription and worker." + [adapter] + (when-not (instance? Adapter adapter) + (throw (IllegalArgumentException. "stop! requires a core.async adapter"))) + (io! + (when (compare-and-set! (.-stopped ^Adapter adapter) false true) + (async/close! (.-stop-channel ^Adapter adapter)) + (control/close! (.-subscription ^Adapter adapter))) + nil)) + +(defn- stopped-outcome [] + {:reason :adapter-stopped + :cause nil + :loss {:upstream/events 0 :adapter/events 0}}) + +(defn- terminal-outcome [stopped result] + (if @stopped + (stopped-outcome) + (case (:stream/status result) + :terminal (:stream/terminal result) + (stopped-outcome)))) + +(defn observe! + "Starts an owned virtual worker over a borrowed control connection. + + `:event-capacity` defaults to zero. The worker can retain one pending event; + a fixed buffer retains that many more. `:upstream-capacity` bounds the Java + subscription, which reports overflow as a :gap value ahead of the events that + remain; `:encoding` is :bytes or :utf-8. Closing the events channel or + adapter stops blocked reads and puts. The borrowed connection remains open." + ([connection] (observe! connection {})) + ([connection options] + (when-not (map? options) + (throw (IllegalArgumentException. "adapter options must be a map"))) + (let [allowed #{:event-capacity :upstream-capacity :encoding} + unknown (seq (remove allowed (keys options))) + {:keys [event-capacity upstream-capacity encoding] + :or {event-capacity 0 upstream-capacity 256 encoding :bytes}} options] + (when (or unknown + (not (and (integer? event-capacity) (<= 0 event-capacity Integer/MAX_VALUE))) + (not (and (integer? upstream-capacity) (<= 1 upstream-capacity Integer/MAX_VALUE)))) + (throw (IllegalArgumentException. "invalid core.async adapter options"))) + (io! + (let [subscription (control/subscribe! connection + {:capacity upstream-capacity + :encoding encoding}) + raw-events (async/chan (when (pos? event-capacity) + (async/buffer event-capacity))) + terminal-channel (async/chan 1) + stop-channel (async/chan) + stopped (atom false) + stop-fn #(when (compare-and-set! stopped false true) + (async/close! stop-channel) + (control/close! subscription)) + public-events (EventsPort. raw-events stop-fn) + outcome (atom nil) + worker (.start + (Thread/ofVirtual) + ^Runnable + (fn [] + (try + (loop [] + (let [result (control/next! subscription)] + (case (:stream/status result) + :timeout (recur) + :terminal (reset! outcome (terminal-outcome stopped result)) + (:event :gap) (let [[accepted port] + (async/alts!! [[raw-events result] stop-channel] + :priority true)] + (when (and (identical? port raw-events) accepted) + (recur)))))) + (catch Throwable failure + (reset! outcome + {:reason :adapter-failure :cause failure + :loss {:upstream/events 0 :adapter/events 0}})) + (finally + (try (control/close! subscription) + (catch Throwable cleanup + (if-let [^Throwable failure (:cause @outcome)] + (.addSuppressed failure cleanup) + (reset! outcome {:reason :cleanup-failure :cause cleanup})))) + (async/close! raw-events) + (async/>!! terminal-channel + (or @outcome (terminal-outcome stopped {:stream/status :stopped}))) + (async/close! terminal-channel)))))] + (Adapter. public-events terminal-channel subscription stop-channel stopped worker + {:events (inc event-capacity)})))))) diff --git a/libtmux-clojure-core-async/src/test/clojure/libtmux/core_async_test.clj b/libtmux-clojure-core-async/src/test/clojure/libtmux/core_async_test.clj new file mode 100644 index 000000000..eced5c6b5 --- /dev/null +++ b/libtmux-clojure-core-async/src/test/clojure/libtmux/core_async_test.clj @@ -0,0 +1,87 @@ +(ns libtmux.core-async-test + (:require [clojure.core.async :as async] + [clojure.test :refer [deftest is]] + [libtmux.control :as control] + [libtmux.core-async :as adapter]) + (:import [java.util.concurrent CountDownLatch TimeUnit])) + +(defn- take-within [channel] + (let [timeout (async/timeout 500) + [value port] (async/alts!! [channel timeout] :priority true)] + (if (identical? port timeout) ::timeout value))) + +(deftest forwards-events-and-publishes-terminal-after-cleanup + (let [calls (atom 0) + closed (CountDownLatch. 1)] + (with-redefs [control/subscribe! (fn [_ _] ::subscription) + control/next! (fn [_] + (case (int (swap! calls inc)) + 1 {:stream/status :timeout} + 2 {:stream/status :event :output/bytes (byte-array [1 2]) + :loss {:upstream/events 3 :adapter/events 0}} + {:stream/status :terminal + :stream/terminal {:reason :ended}})) + control/close! (fn [_] (.countDown closed))] + (let [owner (adapter/observe! ::connection)] + (is (= [1 2] (vec (:output/bytes (take-within (adapter/events owner)))))) + (is (= :ended (:reason (take-within (adapter/terminal owner))))) + (is (.await closed 500 TimeUnit/MILLISECONDS)) + (is (= {:events 1} (adapter/limits owner))))))) + +(deftest consumer-close-reports-adapter-stop-after-a-java-read-returns + (let [entered (CountDownLatch. 1) + released (CountDownLatch. 1)] + (with-redefs [control/subscribe! (fn [_ _] ::subscription) + control/next! (fn [_] + (.countDown entered) + (.await released) + {:stream/status :terminal + :stream/terminal {:reason :closed}}) + control/close! (fn [_] (.countDown released))] + (let [owner (adapter/observe! ::connection)] + (is (.await entered 500 TimeUnit/MILLISECONDS)) + (async/close! (adapter/events owner)) + (is (= :adapter-stopped (:reason (take-within (adapter/terminal owner))))))))) + +(deftest consumer-close-stops-worker-blocked-on-event-put + (let [read-count (atom 0) + cleaned (CountDownLatch. 1)] + (with-redefs [control/subscribe! (fn [_ _] ::subscription) + control/next! (fn [_] + (swap! read-count inc) + {:stream/status :event :output/bytes (byte-array [1])}) + control/close! (fn [_] (.countDown cleaned))] + (let [owner (adapter/observe! ::connection)] + ;; No consumer receives the unbuffered event; close must win the alts!!. + (async/close! (adapter/events owner)) + (is (= :adapter-stopped (:reason (take-within (adapter/terminal owner))))) + (is (.await cleaned 500 TimeUnit/MILLISECONDS)) + (is (<= @read-count 1)))))) + +(deftest owner-stop-wins-a-blocked-event-put + (let [offered (CountDownLatch. 1)] + (with-redefs [control/subscribe! (fn [_ _] ::subscription) + control/next! (fn [_] + (.countDown offered) + {:stream/status :event :output/bytes (byte-array [1])}) + control/close! (fn [_])] + (let [owner (adapter/observe! ::connection)] + (is (.await offered 500 TimeUnit/MILLISECONDS)) + (adapter/stop! owner) + (is (= :adapter-stopped (:reason (take-within (adapter/terminal owner))))))))) + +(deftest worker-failure-is-terminal-and-does-not-close-borrowed-connection + (let [subscription-closes (atom 0)] + (with-redefs [control/subscribe! (fn [_ _] ::subscription) + control/next! (fn [_] (throw (ex-info "boom" {:source :test}))) + control/close! (fn [_] (swap! subscription-closes inc))] + (let [owner (adapter/observe! ::borrowed) + terminal (take-within (adapter/terminal owner))] + (is (= :adapter-failure (:reason terminal))) + (is (= :test (:source (ex-data (:cause terminal))))) + (is (= 1 @subscription-closes)))))) + +(deftest validation-precedes-subscription + (is (thrown? IllegalArgumentException (adapter/observe! ::connection {:event-capacity -1}))) + (is (thrown? IllegalArgumentException (adapter/observe! ::connection {:unknown true}))) + (is (thrown? IllegalArgumentException (adapter/stop! nil)))) diff --git a/libtmux-clojure-core-async/src/test/clojure/libtmux/internal/core_async_package_test.clj b/libtmux-clojure-core-async/src/test/clojure/libtmux/internal/core_async_package_test.clj new file mode 100644 index 000000000..7a4cf04ef --- /dev/null +++ b/libtmux-clojure-core-async/src/test/clojure/libtmux/internal/core_async_package_test.clj @@ -0,0 +1,9 @@ +(ns libtmux.internal.core-async-package-test + (:require [clojure.core.async :as async] + [clojure.test :refer [deftest is]])) + +(deftest core-async-is-on-the-runtime-classpath + (let [channel (async/chan 1)] + (is (true? (async/offer! channel :ready))) + (is (= :ready (async/poll! channel))) + (async/close! channel))) diff --git a/libtmux-clojure/README.md b/libtmux-clojure/README.md new file mode 100644 index 000000000..882b7551f --- /dev/null +++ b/libtmux-clojure/README.md @@ -0,0 +1,92 @@ +# libtmux-clojure + +**A Clojure façade over libtmux-java.** + +It returns captured persistent maps and vectors, with the Java handle retained +at `:tmux/ref` for an explicit effect. Ordinary Clojure collection operations +work on the captured values without acquiring new tmux state. + +> **Alpha.** The API will change without notice. Pin an exact version; it is +> not recommended for production. + +Requires JDK 25 or newer and tmux 3.3a through 3.7c; control needs tmux 3.7 or newer. + +## Install + +Add the direct coordinate to `deps.edn`: + +```edn +{:deps {io.github.libtmux/libtmux-clojure + {:mvn/version "0.0.1-alpha.17"}}} +``` + +The Maven coordinate is `io.github.libtmux:libtmux-clojure`. The Java BOM also +manages it when a Java build owns versions: + +```xml + + io.github.libtmux + libtmux-clojure + 0.0.1-alpha.17 + +``` + +## Connect and acquire + +`open!` opens a local client. It does not start or stop the tmux daemon. +Every acquisition function ends in `!`; traversal through a captured map is +local. The example uses the socket property supplied by the executable +documentation fixture. An application supplies its own explicit path. + + +```clojure +(require '[libtmux.core :as tm]) + +(let [socket-path (java.nio.file.Path/of + (System/getProperty "libtmux.docs.socket") + (make-array String 0))] + (with-open [server (tm/open! {:socket-path socket-path})] + (:session/name (tm/new-session! server {:name "docs-connect"})))) +;; => "docs-connect" +``` + +`with-open` releases the client when its body finishes. It never kills the +tmux daemon. `kill-server!` is the explicit operation that does that. + +## Values and effects + +Values are observations. Editing a map changes only that map. An operation +must receive its preserved `:tmux/ref`, and a refresh returns a new observation. + + +```clojure +(require '[libtmux.core :as tm] + '[libtmux.data :as data]) + +(let [socket-path (java.nio.file.Path/of + (System/getProperty "libtmux.docs.socket") + (make-array String 0))] + (with-open [server (tm/open! {:socket-path socket-path})] + (let [session (tm/new-session! server {:name "docs-identity"})] + (data/same-entity? session (tm/refresh! (:tmux/ref session)))))) +;; => true +``` + +Use `data/data` when a value must leave the operational boundary; it removes +every `:tmux/ref` and rejects unsupported JVM objects. + +## Reference + +- [`libtmux.core`](src/main/clojure/libtmux/core.clj): acquisition, effects, + errors, options, buffers and command groups. +- [`libtmux.data`](src/main/clojure/libtmux/data.clj): captured values, + identity, projections, cardinality and Java predicate adapters. +- [`libtmux.async`](src/main/clojure/libtmux/async.clj): bounded owned tasks + and `CompletionStage` interop. +- [`libtmux.control`](src/main/clojure/libtmux/control.clj): explicit control + replies and bounded event observation. + +[`docs/guide/clojure.md`](../docs/guide/clojure.md) covers filtering, links, +error metadata, lifecycle, execution modes, streaming and Java interop. +[`docs/guide/clojure-reference.md`](../docs/guide/clojure-reference.md) lists +captured keys, relations and Java filter support. diff --git a/libtmux-clojure/build.gradle.kts b/libtmux-clojure/build.gradle.kts new file mode 100644 index 000000000..53b38301a --- /dev/null +++ b/libtmux-clojure/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { id("libtmux.clojure-library") } + +extra["clojureTestNamespaces"] = + "libtmux.async-test,libtmux.control-test,libtmux.core-test,libtmux.data-test,libtmux.internal.package-test" + +dependencies { api(project(":libtmux")) } + +tasks.withType().configureEach { + systemProperty( + "libtmux.clojure.main-namespaces", + "libtmux.core,libtmux.data,libtmux.async,libtmux.control", + ) +} diff --git a/libtmux-clojure/src/main/clojure/libtmux/async.clj b/libtmux-clojure/src/main/clojure/libtmux/async.clj new file mode 100644 index 000000000..bc156dcab --- /dev/null +++ b/libtmux-clojure/src/main/clojure/libtmux/async.clj @@ -0,0 +1,339 @@ +(ns libtmux.async + (:require [libtmux.core :as core]) + (:import [java.lang AutoCloseable] + [java.util.concurrent ArrayBlockingQueue Callable CompletableFuture CountDownLatch Executor ExecutorService + Executors Future FutureTask RejectedExecutionException ScheduledExecutorService ScheduledFuture Semaphore ThreadFactory + ScheduledThreadPoolExecutor ThreadPoolExecutor ThreadPoolExecutor$AbortPolicy TimeUnit] + [java.util.function BiConsumer Function])) + +(declare cancel! close-runtime!) + +(deftype Task [state started worker deadline stage kind runtime cancellation permits publication-permit] + clojure.lang.IDeref + (deref [_] + (try (.get ^CompletableFuture stage) + (catch java.util.concurrent.ExecutionException failure (throw (.getCause failure))))) + clojure.lang.IBlockingDeref + (deref [_ timeout-ms timeout-value] + (try (.get ^CompletableFuture stage timeout-ms TimeUnit/MILLISECONDS) + (catch java.util.concurrent.TimeoutException _ timeout-value) + (catch java.util.concurrent.ExecutionException failure (throw (.getCause failure))))) + clojure.lang.IPending + (isRealized [_] (.isDone ^CompletableFuture stage))) + +(deftype AsyncRuntime [^ExecutorService ordinary ^ExecutorService waits + ^ExecutorService callbacks ^ExecutorService completions + ^ScheduledExecutorService deadlines + ^Semaphore admission ^Semaphore wait-admission ^Semaphore callback-admission + ^Semaphore publication-admission + tasks callback-stages lifecycle-lock + closed close-timeout-ms] + AutoCloseable + (close [this] (close-runtime! this))) + +(defn- virtual-factory [prefix] + (.factory (.name (Thread/ofVirtual) (str prefix "-") 0))) + +(defn- executor [workers queue-limit prefix] + (ThreadPoolExecutor. workers workers 0 TimeUnit/MILLISECONDS + (ArrayBlockingQueue. queue-limit) + ^ThreadFactory (virtual-factory prefix) + (ThreadPoolExecutor$AbortPolicy.))) + +(defn- completion-executor [] + (Executors/newThreadPerTaskExecutor (virtual-factory "libtmux-complete"))) + +(defn- deadline-executor [] + (doto (ScheduledThreadPoolExecutor. 1 ^ThreadFactory (virtual-factory "libtmux-deadline")) + (.setRemoveOnCancelPolicy true))) + +(defn- execute! [^ExecutorService executor ^Runnable task] + (.execute executor task)) + +(defn runtime! + "Creates an owned bounded runtime. Close it to reclaim every owned executor." + ([] (runtime! {})) + ([options] + (when-not (map? options) + (throw (IllegalArgumentException. "runtime options must be a map"))) + (let [allowed #{:max-running :queue-limit :close-timeout-ms} + unknown (seq (remove allowed (keys options))) + {:keys [max-running queue-limit close-timeout-ms] + :or {max-running 4 queue-limit 64 close-timeout-ms 900}} options] + (io! + (when unknown (throw (IllegalArgumentException. (str "unknown runtime options: " unknown)))) + (when-not (and (integer? max-running) (<= 2 max-running Integer/MAX_VALUE)) + (throw (IllegalArgumentException. "max-running must be at least 2"))) + (when-not (and (integer? queue-limit) (<= 1 queue-limit Integer/MAX_VALUE) + (integer? close-timeout-ms) (<= 1 close-timeout-ms Long/MAX_VALUE)) + (throw (IllegalArgumentException. "queue-limit and close-timeout-ms must be positive"))) + (AsyncRuntime. (executor (dec max-running) queue-limit "libtmux-work") + (executor 1 queue-limit "libtmux-wait") + (executor 1 queue-limit "libtmux-callback") + (completion-executor) + (deadline-executor) + (Semaphore. (+ max-running queue-limit)) + (Semaphore. (dec (+ max-running queue-limit))) + (Semaphore. (inc queue-limit)) + (Semaphore. (+ max-running queue-limit)) + (atom #{}) (atom #{}) (Object.) (atom false) close-timeout-ms))))) + +(defn task-kind [^Task task] (.-kind task)) +(defn task-state [^Task task] @(.-state task)) +(defn completion-stage [^Task task] (.minimalCompletionStage ^CompletableFuture (.-stage task))) + +(defn- failure [reason certainty] + (ex-info (if (= reason :timeout) "task deadline expired" "task cancelled") + {:tmux/error reason :tmux/operation :async-task :tmux/phase :execution + :tmux/dispatch certainty})) + +(defn- reconcile-failure [cancellation thrown] + (cond + (nil? cancellation) thrown + (or (nil? thrown) (identical? cancellation thrown)) cancellation + :else + ;; Cancellation remains the terminal classification and certainty, while + ;; structured partial-effect evidence and the original cause stay reachable. + (ex-info (.getMessage ^Throwable cancellation) + (merge (ex-data thrown) (ex-data cancellation)) + thrown))) + +(defn- finish! [^Task task throwable value] + (locking task + (let [state (.-state task) + ^AsyncRuntime runtime (.-runtime task) + throwable (reconcile-failure @(.-cancellation task) throwable)] + (when-not (= :terminal @state) + (reset! state :cleanup-pending) + (when-let [^ScheduledFuture timer @(.-deadline task)] (.cancel timer false)) + (let [ready (CountDownLatch. 1)] + (locking (.-lifecycle-lock runtime) + ;; The publisher cannot be shut down while this task remains in the + ;; registry. Acceptance therefore precedes the final registry drop. + (.execute ^ExecutorService (.-completions runtime) + ^Runnable #(do (.await ready) + (try + (if throwable + (.completeExceptionally ^CompletableFuture (.-stage task) throwable) + (.complete ^CompletableFuture (.-stage task) value)) + (finally + (.release ^Semaphore (.-publication-permit task)))))) + (swap! (.-tasks runtime) disj task) + (doseq [^Semaphore permit (.-permits task)] (.release permit)) + (reset! state :terminal) + (.countDown ready))))))) + +(defn- interrupt! [^Task task] + (when-let [^Future future @(.-worker task)] + (let [^ThreadPoolExecutor pool (if (= :wait (.-kind task)) + (.-waits ^AsyncRuntime (.-runtime task)) + (.-ordinary ^AsyncRuntime (.-runtime task)))] + (.cancel future true) + (.remove pool ^Runnable future)))) + +(defn- request-cancel! [^Task task reason] + (let [accepted (locking task + (let [state (.-state task)] + (when (#{:queued :running} @state) + (let [problem (failure reason (if (= :queued @state) + :not-dispatched :unknown))] + (reset! (.-cancellation task) problem) + (reset! state :cancelling) + problem))))] + (if accepted + (do (interrupt! task) true) + false))) + +(defn- reject! [^Task task] + (let [problem (failure :busy :not-dispatched) + cancellable? (locking task + (when (= :queued @(.-state task)) + (reset! (.-cancellation task) problem) + (reset! (.-state task) :cancelling)) + (= :cancelling @(.-state task)))] + (when cancellable? + ;; This FutureTask was never accepted by an executor. Cancellation calls + ;; done synchronously, which is the task's single finalization owner. + (.cancel ^Future @(.-worker task) false)))) + +(defn cancel! + "Requests cancellation. The first accepted cancellation reason wins." + [task] + (when-not (instance? Task task) + (throw (IllegalArgumentException. "cancel! requires a task"))) + (io! (request-cancel! task :cancelled))) + +(defn submit! + "Submits a zero-argument function, returning a Task or a :busy result." + ([runtime thunk] (submit! runtime {} thunk)) + ([^AsyncRuntime runtime options thunk] + (when-not (and (instance? AsyncRuntime runtime) (map? options) (ifn? thunk)) + (throw (IllegalArgumentException. "submit! requires a runtime, option map, and function"))) + (let [allowed #{:kind :timeout-ms} + unknown (seq (remove allowed (keys options))) + {:keys [kind timeout-ms] :or {kind :ordinary}} options] + (io! + (when unknown (throw (IllegalArgumentException. (str "unknown submit options: " unknown)))) + (when @(.-closed runtime) (throw (IllegalStateException. "runtime is closed"))) + (when-not (#{:ordinary :wait} kind) + (throw (IllegalArgumentException. "task kind must be :ordinary or :wait"))) + (when-not (or (nil? timeout-ms) + (and (integer? timeout-ms) (<= 1 timeout-ms Long/MAX_VALUE))) + (throw (IllegalArgumentException. "timeout-ms must be a positive integer"))) + (let [global (.-admission runtime) + wait-permit (when (= :wait kind) (.-wait-admission runtime)) + publication-permit (.-publication-admission runtime) + acquired-global? (.tryAcquire ^Semaphore global) + acquired-wait? (or (nil? wait-permit) (.tryAcquire ^Semaphore wait-permit)) + acquired-publication? (and acquired-global? acquired-wait? + (.tryAcquire ^Semaphore publication-permit))] + (if-not (and acquired-global? acquired-wait? acquired-publication?) + (do (when acquired-global? (.release ^Semaphore global)) + (when (and wait-permit acquired-wait?) (.release ^Semaphore wait-permit)) + (when acquired-publication? (.release ^Semaphore publication-permit)) + {:status :busy :kind kind} + ) + (let [state (atom :queued) started (atom false) worker (atom nil) deadline (atom nil) stage (CompletableFuture.) + cancellation (atom nil) + task (Task. state started worker deadline stage kind runtime cancellation + (cond-> [global] wait-permit (conj wait-permit)) publication-permit) + bindings (get-thread-bindings) + pool (if (= :wait kind) (.-waits runtime) (.-ordinary runtime)) + check (fn [{operation-kind :kind}] + (when-let [problem @cancellation] (throw problem)) + (when (and (= operation-kind :wait) (not= kind :wait)) + (throw (ex-info "wait operation requires a :wait task" + {:tmux/error :invalid-task-kind + :tmux/operation :async-task + :tmux/phase :validation + :tmux/dispatch :not-dispatched})))) + call (reify Callable + (call [_] + (let [run? (locking task + (when (= :queued @state) + (reset! started true) + (reset! state :running) + true))] + (if run? + (try + (let [value (with-bindings (assoc bindings + #'core/*execution-check* check) + (thunk))] + (finish! task @cancellation value)) + (catch Throwable thrown + (finish! task thrown nil))) + (finish! task @cancellation nil))) + nil)) + future (proxy [FutureTask] [call] + (done [] + ;; FutureTask invokes done even when cancellation wins + ;; after dequeue but before Callable.call. + (when (and (not @started) @cancellation) + (finish! task @cancellation nil))))] + (let [rejected? (atom false)] + (locking (.-lifecycle-lock runtime) + (if @(.-closed runtime) + (do + (doseq [^Semaphore permit (.-permits task)] (.release permit)) + (.release ^Semaphore (.-publication-permit task)) + (throw (IllegalStateException. "runtime is closed"))) + (do + (swap! (.-tasks runtime) conj task) + (when timeout-ms + (reset! deadline + (.schedule ^ScheduledExecutorService (.-deadlines runtime) + ^Runnable #(request-cancel! task :timeout) + (long timeout-ms) TimeUnit/MILLISECONDS))) + (reset! worker future) + (try + (execute! pool future) + (catch RejectedExecutionException _ + (reset! rejected? true)))))) + (if @rejected? + (do (reject! task) {:status :busy :kind kind}) + task))))))))) + +(defn then! [^AsyncRuntime runtime ^Task task f] + (when-not (and (instance? AsyncRuntime runtime) (instance? Task task) (ifn? f)) + (throw (IllegalArgumentException. "then! requires a runtime, task, and function"))) + (io! + (locking (.-lifecycle-lock runtime) + (when @(.-closed runtime) (throw (IllegalStateException. "runtime is closed"))) + (when-not (.tryAcquire ^Semaphore (.-callback-admission runtime)) + (throw (ex-info "callback capacity is full" + {:tmux/error :busy :tmux/operation :async/then + :tmux/phase :admission :tmux/dispatch :not-dispatched}))) + (try + (let [bindings (get-thread-bindings) + stage (.thenApplyAsync ^CompletableFuture (.-stage task) + (reify Function + (apply [_ value] (with-bindings bindings (f value)))) + ^Executor (.-callbacks runtime))] + (swap! (.-callback-stages runtime) conj stage) + (.whenComplete ^CompletableFuture stage + (reify BiConsumer + (accept [_ _ _] + (swap! (.-callback-stages runtime) disj stage) + (.release ^Semaphore (.-callback-admission runtime))))) + (.minimalCompletionStage ^CompletableFuture stage)) + (catch Throwable failure + (.release ^Semaphore (.-callback-admission runtime)) + (throw failure)))))) + +(defn- shutdown-completion-if-idle! [^AsyncRuntime runtime] + (locking (.-lifecycle-lock runtime) + (when (and (empty? @(.-tasks runtime)) + (empty? @(.-callback-stages runtime))) + (.shutdown ^ExecutorService (.-completions runtime))))) + +(defn close-runtime! [^AsyncRuntime runtime] + (when-not (instance? AsyncRuntime runtime) + (throw (IllegalArgumentException. "close-runtime! requires a runtime"))) + (io! + (let [deadline (+ (System/nanoTime) + (.toNanos TimeUnit/MILLISECONDS (.-close-timeout-ms runtime))) + workers [(.-ordinary runtime) (.-waits runtime) + (.-callbacks runtime) (.-deadlines runtime)] + ^ExecutorService completion (.-completions runtime) + first-close? (locking (.-lifecycle-lock runtime) + (compare-and-set! (.-closed runtime) false true))] + ;; Never hold the runtime lock while cancellation can finalize a task back + ;; into the registry. + (doseq [task @(.-tasks runtime)] (cancel! task)) + (when first-close? + (let [stages (locking (.-lifecycle-lock runtime) + @(.-callback-stages runtime)) + cancelled (CountDownLatch. (count stages))] + (doseq [^CompletableFuture stage stages] + (.execute completion + ^Runnable #(try + (.completeExceptionally stage (failure :cancelled :unknown)) + (finally (.countDown cancelled))))) + (doseq [^ExecutorService executor workers] (.shutdownNow executor)) + (.await cancelled + (max 0 (.toMillis TimeUnit/NANOSECONDS + (- deadline (System/nanoTime)))) + TimeUnit/MILLISECONDS))) + (doseq [^ExecutorService executor workers] + (.awaitTermination executor + (max 0 (.toMillis TimeUnit/NANOSECONDS + (- deadline (System/nanoTime)))) + TimeUnit/MILLISECONDS)) + (shutdown-completion-if-idle! runtime) + (when (.isShutdown completion) + (.awaitTermination completion + (max 0 (.toMillis TimeUnit/NANOSECONDS + (- deadline (System/nanoTime)))) + TimeUnit/MILLISECONDS)) + (when (or (seq @(.-tasks runtime)) + (seq @(.-callback-stages runtime)) + (some #(not (.isTerminated ^ExecutorService %)) workers) + (not (.isTerminated completion))) + (throw (ex-info "runtime cleanup is still pending" + {:tmux/error :cleanup-pending + :tmux/operation :close-runtime + :tmux/phase :cleanup + :tmux/dispatch :unknown + :tasks (count @(.-tasks runtime)) + :callbacks (count @(.-callback-stages runtime))})))) + nil)) diff --git a/libtmux-clojure/src/main/clojure/libtmux/control.clj b/libtmux-clojure/src/main/clojure/libtmux/control.clj new file mode 100644 index 000000000..e90000dcc --- /dev/null +++ b/libtmux-clojure/src/main/clojure/libtmux/control.clj @@ -0,0 +1,216 @@ +(ns libtmux.control + "Explicit control replies and owned, bounded pane-output subscriptions. + Control needs tmux 3.7 or newer; an older daemon is refused before + any client is attached, with :tmux/error :tmux/unsupported-feature." + (:require [libtmux.core :as tmux]) + (:import [io.github.libtmux Session Server] + [io.github.libtmux.control ControlClient ControlReply Delivery$Event Delivery$Gap + EventSubscription PaneOutput] + [io.github.libtmux.exception DispatchException DispatchException$TimedOut + LibTmuxException UnsupportedFeatureException] + [java.lang AutoCloseable] + [java.nio ByteBuffer] + [java.time Duration] + [java.util Optional] + [java.util.concurrent TimeUnit] + [java.util.concurrent.locks ReentrantLock])) + +(set! *warn-on-reflection* true) + +(defn- invalid! [operation] + (throw (ex-info "Invalid control operation arguments" + {:tmux/error :tmux/validation :tmux/operation operation + :tmux/phase :validation :tmux/dispatch :not-dispatched}))) + +(defn- positive? [value limit] + (and (integer? value) (<= 1 value limit))) + +(defn- duration? [value] + (and (instance? Duration value) (not (.isNegative ^Duration value)))) + +(defn- perform! [operation f] + (io! + (when tmux/*execution-check* + (tmux/*execution-check* {:operation operation :kind (if (= operation :control/next) :wait :mutation)})) + (try (f) + (catch LibTmuxException cause + (throw (ex-info "Control operation failed" + {:tmux/error (cond + (instance? UnsupportedFeatureException cause) + :tmux/unsupported-feature + (instance? DispatchException$TimedOut cause) + :tmux/timeout + :else :tmux/transport) + :tmux/operation operation :tmux/phase :protocol + :tmux/dispatch + (cond + (instance? UnsupportedFeatureException cause) :not-dispatched + (instance? DispatchException cause) + (case (.name (.outcome ^DispatchException cause)) + "NOT_DISPATCHED" :not-dispatched + :unknown) + :else :unknown)} cause)))))) + +(deftype ^:private Connection [^ControlClient client identity] + AutoCloseable + (close [_] (io! (.close client)))) +(alter-meta! #'->Connection assoc :private true) + +(defn attach! + "Attaches an owned control client to an explicit captured Session reference. + Close releases this client, never its server. Use a dedicated connection + when cancellation must not affect other requests and subscriptions." + ([session] (attach! session (Duration/ofSeconds 10))) + ([session timeout] + (when-not (and (instance? Session session) (duration? timeout) + (not (.isZero ^Duration timeout))) + (invalid! :control/attach)) + (perform! :control/attach + #(let [captured (tmux/refresh! session) + ^Session reference (:tmux/ref captured)] + (when-not reference + (throw (ex-info "Control session no longer exists" + {:tmux/error :tmux/not-found + :tmux/operation :control/attach + :tmux/phase :validation :tmux/dispatch :not-dispatched}))) + (Connection. (.control ^Server (.server reference) reference timeout) + (dissoc (:tmux/identity captured) :tmux/kind :tmux/id)))))) + +(defn- reply-data [^ControlReply reply] + {:reply/status (case (.name (.outcome reply)) + "COMPLETE" :complete "FAILED" :failed + "SKIPPED" :skipped :unknown) + :reply/lines (vec (.lines reply)) + :operation/completion :unverified}) + +(defn send! + "Sends argv and returns protocol status, lines, and unverified completion. + A complete reply does not prove deferred work finished. Cancelling an active + send can end this connection and its other requests and subscriptions." + ([connection argv] (send! connection argv (Duration/ofMillis 900))) + ([connection argv timeout] + (when-not (and (instance? Connection connection) + (vector? argv) (seq argv) (every? string? argv) + (duration? timeout) (not (.isZero ^Duration timeout))) + (invalid! :control/send)) + (perform! :control/send + #(reply-data (.send ^ControlClient (.-client ^Connection connection) + ^java.util.List argv timeout))))) + +;; One reader at a time: the Java subscription refuses overlapping reads, so reads queue on +;; read-lock and a timed read counts that wait against its budget. +(deftype ^:private OutputStream [^EventSubscription subscription identity options state read-lock] + AutoCloseable + (close [_] (io! (.close subscription)))) +(alter-meta! #'->OutputStream assoc :private true) + +(defn subscribe! + "Creates a closeable output subscription borrowing its connection. + :encoding is :bytes (caller-owned byte arrays) or :utf-8 (text decoded per + pane by the Java client, so a character cut between two chunks arrives whole + and a byte that is not UTF-8 is written \\xHH). :capacity bounds queued + events; when full the oldest is dropped and the next read is a :gap. + Readers compete; create separate subscriptions for independent fan-out." + ([connection] (subscribe! connection {})) + ([connection options] + (when-not (and (instance? Connection connection) (map? options) + (every? #{:encoding :capacity} (keys options))) + (invalid! :control/subscribe)) + (let [options (merge {:encoding :bytes :capacity 256} options) + {:keys [encoding capacity]} options] + (when-not (and (#{:bytes :utf-8} encoding) (positive? capacity Integer/MAX_VALUE)) + (invalid! :control/subscribe)) + (perform! :control/subscribe + #(OutputStream. + (.subscribeOutput ^ControlClient (.-client ^Connection connection) (int capacity)) + (.-identity ^Connection connection) options + (atom {:sequence 0}) (ReentrantLock.)))))) + +(defn- loss-data [^EventSubscription subscription] + {:upstream/events (.droppedCount subscription)}) + +(defn terminal + "Returns the single recorded stream outcome, or nil until a reader drains it. + :reason is :closed after an intentional close, or :ended with the original + :cause when the control client ended; the cause may contain sensitive + information." + [^OutputStream stream] + (:terminal @(.-state stream))) + +(defn- end! [^OutputStream stream] + (let [^EventSubscription subscription (.-subscription stream) + cause (.orElse (.cause subscription) nil) + outcome (cond-> {:reason (if cause :ended :closed) + :loss (loss-data subscription)} + cause (assoc :cause cause))] + (:terminal (swap! (.-state stream) + #(if (:terminal %) % (assoc % :terminal outcome)))))) + +(defn- bytes-of [^PaneOutput output] + (let [^ByteBuffer buffer (.bytes output) + octets (byte-array (.remaining buffer))] + (.get buffer octets) + octets)) + +(defn- delivery-data [^OutputStream stream delivery] + (let [state (.-state stream) + ^EventSubscription subscription (.-subscription stream)] + (if (instance? Delivery$Gap delivery) + {:stream/status :gap + :gap/events (.missed ^Delivery$Gap delivery) + :loss (loss-data subscription)} + (let [^PaneOutput output (.value ^Delivery$Event delivery) + utf-8? (= :utf-8 (:encoding (.-options stream))) + current (swap! state update :sequence inc)] + {:stream/status :event + :event/sequence (:sequence current) + :tmux/identity (assoc (.-identity stream) :tmux/kind :pane :tmux/id (.value (.pane output))) + :pane/id (.value (.pane output)) + (if utf-8? :output/text :output/bytes) (if utf-8? (.data output) (bytes-of output)) + :loss (loss-data subscription)})))) + +(defn next! + "Reads one event, gap, timeout, or terminal map; never uses nil for termination. + A :gap names the events dropped since the previous read and precedes the + events that remain. The optional nonnegative Duration includes waiting for + another reader. Sequence numbers order the events of this stream. Close wakes + a reader. Termination by the client keeps already delivered events readable; + an intentional close discards them." + ([stream] (next! stream nil)) + ([stream timeout] + (when-not (and (instance? OutputStream stream) (or (nil? timeout) (duration? timeout))) + (invalid! :control/next)) + (let [^OutputStream stream stream] + (perform! :control/next + #(let [^ReentrantLock lock (.-read-lock stream) + started (System/nanoTime) + budget (when timeout + (try (.toNanos ^Duration timeout) + (catch ArithmeticException _ Long/MAX_VALUE))) + acquired (if budget (.tryLock lock budget TimeUnit/NANOSECONDS) + (do (.lockInterruptibly lock) true))] + (if-not acquired + {:stream/status :timeout} + (try + (if-let [outcome (terminal stream)] + {:stream/status :terminal :stream/terminal outcome} + (let [^EventSubscription subscription (.-subscription stream) + ^Optional delivery (if budget + (.next subscription + (Duration/ofNanos + (max 0 (- budget (- (System/nanoTime) started))))) + (.next subscription))] + (cond + (.isPresent delivery) (delivery-data stream (.get delivery)) + (.isClosed subscription) {:stream/status :terminal + :stream/terminal (end! stream)} + :else {:stream/status :timeout}))) + (finally (.unlock lock))))))))) + +(defn close! + "Closes a connection or subscription. Closing a subscription leaves its + borrowed connection open; closing a connection affects all its subscribers." + [resource] + (when-not (or (instance? Connection resource) (instance? OutputStream resource)) + (invalid! :control/close)) + (perform! :control/close #(.close ^AutoCloseable resource))) diff --git a/libtmux-clojure/src/main/clojure/libtmux/core.clj b/libtmux-clojure/src/main/clojure/libtmux/core.clj new file mode 100644 index 000000000..966670682 --- /dev/null +++ b/libtmux-clojure/src/main/clojure/libtmux/core.clj @@ -0,0 +1,517 @@ +(ns libtmux.core + "Explicit tmux effects. Captured maps retain opaque Java references." + (:require [libtmux.data :as data]) + (:import [io.github.libtmux Server ServerEndpoint Session Window Pane Client + SessionSpec SessionSpec$Builder WindowSpec WindowSpec$Builder + SplitSpec SplitSpec$Builder CaptureSpec Dimensions Options Environment Hooks BufferInfo] + [io.github.libtmux.exception LibTmuxException TargetGoneException ServerUnavailableException + UnsupportedFeatureException ServerClosedException] + [io.github.libtmux.exception DispatchException$TimedOut DispatchException] + [io.github.libtmux.batch BatchResult OperationResult] + [java.nio.file Path] + [java.time Duration])) + +(def ^:dynamic ^:no-doc *execution-check* nil) + +(defn- invalid! [operation] + (throw (ex-info "Invalid tmux operation arguments" + {:tmux/error :tmux/validation :tmux/operation operation + :tmux/phase :validation :tmux/dispatch :not-dispatched}))) + +(defn- require! [operation valid?] + (when-not valid? (invalid! operation))) + +(defn- closed! [operation options schema] + (require! operation (and (map? options) + (every? #(contains? schema %) (keys options)) + (every? (fn [[k v]] ((get schema k) v)) options))) + options) + +(set! *warn-on-reflection* true) + +(defmacro ^:private call [object method classes & args] + (let [target (gensym "target")] + `(let [~target ~object] + (cond + ~@(mapcat (fn [class] + [`(instance? ~class ~target) + `(. ~(with-meta target {:tag class}) ~method ~@args)]) classes) + :else (throw (IllegalArgumentException. "Unexpected typed receiver")))))) + +(defn- ref! [operation value classes] + (require! operation (some #(instance? % value) classes)) + value) + +(def ^:private single-effect-operations + #{:raw :batch :chain :select :kill :link-window :unlink-window :move-window :join-pane + :resize :apply-layout :send-literal :send-keys :run-shell :detach-client + :switch-client :set-option :unset-option :set-environment + :unset-environment :remove-environment :set-hook :append-hook :unset-hook + :set-buffer :show-buffer :delete-buffer :paste-buffer :buffers + :signal-channel :capture :expand}) + +(defn- translate [operation mutation? cause & [composed?]] + (let [refused? (and (not composed?) (instance? UnsupportedFeatureException cause)) + dispatch (cond + refused? :not-dispatched + (and (not composed?) (single-effect-operations operation) + (instance? DispatchException cause)) + (case (.name (.outcome ^DispatchException cause)) + "NOT_DISPATCHED" :not-dispatched + "COMPLETE" :complete + :unknown) + :else :unknown) + kind (cond + (instance? TargetGoneException cause) :tmux/not-found + (instance? ServerUnavailableException cause) :tmux/server-unavailable + (instance? UnsupportedFeatureException cause) :tmux/unsupported-feature + (instance? DispatchException$TimedOut cause) :tmux/timeout + (instance? DispatchException cause) :tmux/transport + (instance? ServerClosedException cause) :tmux/closed + :else :tmux/failure)] + (ex-info "tmux operation failed" + {:tmux/error kind :tmux/operation operation + :tmux/phase (if refused? :validation :unknown) + :tmux/dispatch dispatch + :tmux/effects (if (and mutation? (not= :not-dispatched dispatch)) + :possible :none)} cause))) + +(defn- execute! [operation kind f] + (io! + (when *execution-check* (*execution-check* {:operation operation :kind kind})) + (try (f) + (catch LibTmuxException failure + (throw (translate operation (= kind :mutation) failure))) + (catch IllegalArgumentException failure + (if (and (#{:link-window :move-window :join-pane :switch-client} operation) + (#{"handles belong to different tmux servers" + "handles belong to different tmux server incarnations"} + (.getMessage failure))) + (throw (ex-info "References belong to different tmux servers" + {:tmux/error :tmux/validation :tmux/operation operation + :tmux/phase :validation :tmux/dispatch :not-dispatched + :tmux/effects :none} failure)) + (throw failure))) + (catch IllegalStateException failure + (if (= "server is closed" (.getMessage failure)) + (throw (translate operation (= kind :mutation) failure)) + (throw failure)))))) + +(defn error-summary + "Returns fixed error metadata without arguments or cause messages. + Printing the original exception and its cause chain is not sanitized." + [failure] + (select-keys (ex-data failure) + [:tmux/error :tmux/operation :tmux/phase :tmux/dispatch :tmux/effects])) + +(defn- path? [x] (or (string? x) (instance? Path x))) +(defn- path [x] (if (instance? Path x) x (Path/of ^String x (make-array String 0)))) +(defn- positive-int? [x] (and (integer? x) (< 0 x Integer/MAX_VALUE))) +(defn- nonnegative-int? [x] (and (integer? x) (<= 0 x Integer/MAX_VALUE))) +(defn- argv? [x] (and (vector? x) (seq x) (every? string? x))) +(defn- environment? [x] (and (map? x) (every? string? (keys x)) (every? string? (vals x)))) +(defn- duration? [x] (and (instance? Duration x) (not (.isNegative ^Duration x)) + (not (.isZero ^Duration x)))) + +(defn open! + "Opens local resources for an explicit :socket-path or :socket-name endpoint. + The returned Server supports with-open; close does not kill tmux." + [options] + (closed! :open options {:socket-path path? :socket-name string? :binary string? + :config-file path? :timeout duration?}) + (require! :open (= 1 (count (filter #(contains? options %) [:socket-path :socket-name])))) + (let [endpoint (try + (if (:socket-path options) + (ServerEndpoint/socketPath (path (:socket-path options))) + (ServerEndpoint/namedSocket (:socket-name options))) + (catch IllegalArgumentException _ (invalid! :open)))] + (execute! :open :read + #(let [builder (Server/builder)] + (.endpoint builder endpoint) + (when-let [x (:binary options)] (.binary builder x)) + (when-let [x (:config-file options)] (.configFile builder (path x))) + (when-let [x (:timeout options)] (.defaultTimeout builder x)) + (.build builder))))) + +(defn snapshot! "Acquires an eager captured graph from an explicit server." + [server] + (let [^Server server (ref! :snapshot server [Server])] + (execute! :snapshot :read #(data/from-capture (.captureServer server))))) + +(defn sessions! "Acquires captured sessions; acquisition failure throws." [server] + (:tmux/sessions (snapshot! server))) +(defn windows! "Acquires captured window occurrences." [server] + (:tmux/windows (snapshot! server))) +(defn panes! "Acquires captured pane occurrences." [server] + (:tmux/panes (snapshot! server))) +(defn clients! "Acquires captured clients." [server] + (:tmux/clients (snapshot! server))) + +(defn find-session! "Finds a session by exact name; absence returns nil." [server name] + (require! :find-session (string? name)) + (data/one-or-none (filter #(= name (:session/name %)) (sessions! server)))) + +(defn- captured-reference [snapshot reference context] + (let [entries (cond (instance? Server reference) [snapshot] + (instance? Session reference) (:tmux/sessions snapshot) + (instance? Window reference) (:tmux/windows snapshot) + (instance? Pane reference) (:tmux/panes snapshot) + (instance? Client reference) (:tmux/clients snapshot))] + (data/one-or-none + (filter #(and (= reference (:tmux/ref %)) + (if (instance? Pane reference) + (= (or context (.context (.window ^Pane reference))) + (.context (.window ^Pane (:tmux/ref %)))) true)) entries)))) + +(defn- refresh-reference! [reference & [context]] + (let [^Server server (if (instance? Server reference) reference (call reference server [Session Window Pane Client]))] + (captured-reference (data/from-capture (.captureServer server)) reference context))) + +(defn refresh! + "Reacquires an explicit reference, retaining window and pane placement. + Returns nil if that occurrence disappeared; server failure throws." + [entry] + (let [reference (ref! :refresh entry [Server Session Window Pane Client])] + (execute! :refresh :read #(refresh-reference! reference)))) + +(def ^:private creation-schema + {:name string? :command argv? :directory path? :environment environment?}) + +(defn- build-spec [operation options builder schema] + (closed! operation options schema) + (require! operation (not (and (:cells options) (:percent options)))) + (require! operation (not (and (:empty? options) (:command options)))) + (try + (when-let [x (:name options)] (call builder named [SessionSpec$Builder WindowSpec$Builder] x)) + (when-let [x (:window-name options)] (.firstWindowNamed ^SessionSpec$Builder builder x)) + (when-let [x (:command options)] + (call builder running [SessionSpec$Builder WindowSpec$Builder SplitSpec$Builder] + (into-array String (if (= 1 (count x)) + (into ["/bin/sh" "-c" "exec \"$@\"" "libtmux-clojure"] x) + x)))) + (when-let [x (:directory options)] + (call builder in [SessionSpec$Builder WindowSpec$Builder SplitSpec$Builder] (path x))) + (when-let [x (:environment options)] + (call builder environment [SessionSpec$Builder WindowSpec$Builder SplitSpec$Builder] x)) + (when-let [x (:index options)] (.atIndex ^WindowSpec$Builder builder (int x))) + (when (:detached? options) (call builder detached [WindowSpec$Builder SplitSpec$Builder])) + (when (:empty? options) (.empty ^SplitSpec$Builder builder)) + (when (:full-window? options) (.fullWindow ^SplitSpec$Builder builder)) + (when (:zoomed? options) (.zoomed ^SplitSpec$Builder builder)) + (when-let [x (:direction options)] + (case x + :below (.below ^SplitSpec$Builder builder) + :above (.above ^SplitSpec$Builder builder) + :right (.toRight ^SplitSpec$Builder builder) + :left (.toLeft ^SplitSpec$Builder builder))) + (when-let [x (:cells options)] (.cells ^SplitSpec$Builder builder (int x))) + (when-let [x (:percent options)] (.percent ^SplitSpec$Builder builder (int x))) + (call builder build [SessionSpec$Builder WindowSpec$Builder SplitSpec$Builder]) + (catch IllegalArgumentException cause + (throw (ex-info "Invalid tmux specification" + {:tmux/error :tmux/validation :tmux/operation operation + :tmux/phase :validation :tmux/dispatch :not-dispatched} cause))))) + +(defn- capture-after-mutation! [operation reference & [context]] + (try (refresh-reference! reference context) + (catch LibTmuxException failure (throw (translate operation true failure true))) + (catch clojure.lang.ExceptionInfo failure + (if (:tmux/error (ex-data failure)) + (throw (translate operation true failure true)) + (throw failure))))) + +(defn new-session! "Creates a detached session and returns fresh captured data." + [server options] + (let [^Server server (ref! :new-session server [Server]) + ^SessionSpec spec (build-spec :new-session options (SessionSpec/builder) + (assoc creation-schema :window-name string?))] + (execute! :new-session :mutation #(capture-after-mutation! :new-session (.newSession server spec))))) + +(defn new-window! "Creates a window and returns fresh captured data." [session options] + (let [^Session session (ref! :new-window session [Session]) + ^WindowSpec spec (build-spec :new-window options (WindowSpec/builder) + (assoc creation-schema :index nonnegative-int? :detached? boolean?))] + (execute! :new-window :mutation #(capture-after-mutation! :new-window (.newWindow session spec))))) + +(defn split-pane! "Splits a pane using argv, never implicit shell interpolation." [pane options] + (let [^Pane pane (ref! :split-pane pane [Pane]) + ^SplitSpec spec (build-spec :split-pane options (SplitSpec/builder) + (merge (dissoc creation-schema :name) + {:direction #{:below :above :right :left} + :cells positive-int? :percent #(and (integer? %) (<= 1 % 100)) + :detached? boolean? :empty? boolean? + :full-window? boolean? :zoomed? boolean?}))] + (execute! :split-pane :mutation #(capture-after-mutation! :split-pane (.split pane spec) (.context (.window pane)))))) + +(defn rename! "Renames a session or window and captures its current data." [value name] + (require! :rename (string? name)) + (let [reference (ref! :rename value [Session Window])] + (execute! :rename :mutation #(capture-after-mutation! :rename (call reference rename [Session Window] name))))) + +(defn select! "Selects the preserved window or pane occurrence." [value] + (let [reference (ref! :select value [Window Pane])] + (execute! :select :mutation #(call reference select [Window Pane])))) + +(defn kill! "Kills the explicitly supplied session, window, or pane." [value] + (let [reference (ref! :kill value [Session Window Pane])] + (execute! :kill :mutation #(call reference kill [Session Window Pane])))) + +(defn kill-server! "Kills the explicit tmux server; close only releases local resources." [server] + (let [^Server server (ref! :kill-server server [Server])] + (execute! :kill-server :mutation #(.killServer server)))) + +(defn close! "Releases the supplied Server's local resources without killing tmux." [server] + (let [^Server server (ref! :close server [Server])] + (execute! :close :read #(.close server)))) + +(defn link-window! "Links a window into a session." [window session] + (let [^Window window (ref! :link-window window [Window]) + ^Session session (ref! :link-window session [Session])] + (execute! :link-window :mutation #(.linkTo window session)))) +(defn unlink-window! "Unlinks the preserved window occurrence." [window] + (let [^Window window (ref! :unlink-window window [Window])] + (execute! :unlink-window :mutation #(.unlink window)))) +(defn move-window! "Moves a window to a session and explicit index." [window session index] + (require! :move-window (nonnegative-int? index)) + (let [^Window window (ref! :move-window window [Window]) + ^Session session (ref! :move-window session [Session])] + (execute! :move-window :mutation #(.moveTo window session (int index))))) +(defn join-pane! "Joins a pane to a window." [pane window] + (let [^Pane pane (ref! :join-pane pane [Pane]) ^Window window (ref! :join-pane window [Window])] + (execute! :join-pane :mutation #(.joinTo pane window)))) +(defn resize! "Resizes a window or pane to positive cell dimensions." [value width height] + (require! :resize (and (positive-int? width) (positive-int? height))) + (let [reference (ref! :resize value [Window Pane])] + (execute! :resize :mutation #(call reference resizeTo [Window Pane] (Dimensions. (int width) (int height)))))) +(defn apply-layout! "Applies an explicit tmux layout string." [window layout] + (require! :apply-layout (string? layout)) + (let [^Window window (ref! :apply-layout window [Window])] + (execute! :apply-layout :mutation #(.applyLayout window ^String layout)))) + +(defn send-literal! "Sends literal text; completion does not mean application readiness." [pane text] + (require! :send-literal (string? text)) + (let [^Pane pane (ref! :send-literal pane [Pane])] + (execute! :send-literal :mutation #(.sendLiteral pane [text])))) +(defn send-keys! "Sends an explicit vector of tmux key names." [pane keys] + (require! :send-keys (argv? keys)) + (let [^Pane pane (ref! :send-keys pane [Pane])] + (execute! :send-keys :mutation #(.sendKeys pane keys)))) + +(defn capture! + "Captures pane lines; :from :history includes scrollback." + ([pane] (capture! pane {})) + ([pane options] + (closed! :capture options {:from #(or (= :history %) (and (integer? %) (<= Integer/MIN_VALUE % Integer/MAX_VALUE))) + :to #(and (integer? %) (<= Integer/MIN_VALUE % Integer/MAX_VALUE)) + :join-wrapped? boolean? :escape-sequences? boolean?}) + (let [^Pane pane (ref! :capture pane [Pane]) builder (CaptureSpec/builder)] + (when-let [x (:from options)] (if (= :history x) (.fromStartOfHistory builder) (.from builder (int x)))) + (when-let [x (:to options)] (.to builder (int x))) + (when (:join-wrapped? options) (.joiningWrappedLines builder)) + (when (:escape-sequences? options) (.withEscapeSequences builder)) + (let [spec (.build builder)] (execute! :capture :read #(vec (.capture pane spec))))))) + +(defn expand! "Evaluates an explicit tmux format at the supplied scope." [value format] + (require! :expand (string? format)) + (let [reference (ref! :expand value [Server Session Window Pane])] + (execute! :expand :read #(call reference expand [Server Session Window Pane] format)))) + +(defn raw! "Runs explicit tmux argv and returns exit status and streams; no shell." [server argv] + (require! :raw (argv? argv)) + (let [^Server server (ref! :raw server [Server])] + (execute! :raw :mutation #(let [result (.cmd server ^java.util.List argv)] + {:exit-code (.exitCode result) :stdout (vec (.stdout result)) :stderr (vec (.stderr result))})))) + +(defn run-shell! "Runs an explicitly requested shell command on the server." [server command] + (require! :run-shell (string? command)) + (let [^Server server (ref! :run-shell server [Server])] + (execute! :run-shell :mutation #(.run (.shell server) command)))) + +(defn await-text! "Waits within a positive Duration; returns a keyword describing the outcome." [pane text timeout] + (require! :await-text (and (string? text) (duration? timeout))) + (let [^Pane pane (ref! :await-text pane [Pane])] + (execute! :await-text :wait + #(case (.name (.awaitText pane text timeout)) + "APPEARED" :appeared + "PRESENT_AT_ENTRY" :present-at-entry + "TIMED_OUT" :timed-out + "SERVER_GONE" :server-gone)))) + +(defn detach-client! "Detaches the explicit client." [client] + (let [^Client client (ref! :detach-client client [Client])] + (execute! :detach-client :mutation #(.detach client)))) +(defn switch-client! "Switches an explicit client to a session." [client session] + (let [^Client client (ref! :switch-client client [Client]) ^Session session (ref! :switch-client session [Session])] + (execute! :switch-client :mutation #(.switchTo client session)))) + +(defn- ^Options options-reference [reference] + (call reference options [Server Session Window Pane])) +(defn- ^Environment environment-reference [reference] + (call reference environment [Server Session])) +(defn- ^Hooks hooks-reference [reference] + (call reference hooks [Server Session Window Pane])) + +(defn options! + "Reads :local options by default, or :effective inherited options." + ([value] (options! value :local)) + ([value scope] + (require! :options (#{:local :effective} scope)) + (let [reference (ref! :options value [Server Session Window Pane])] + (execute! :options :read + #(into {} (let [^Options options (call reference options [Server Session Window Pane])] + (if (= scope :local) (.all options) (.effective options)))))))) +(defn set-option! "Sets a literal option value at the explicit scope." [value name text] + (require! :set-option (and (string? name) (string? text))) + (let [reference (ref! :set-option value [Server Session Window Pane])] + (execute! :set-option :mutation #(.set (options-reference reference) ^String name ^String text)))) +(defn unset-option! "Removes a local option override." [value name] + (require! :unset-option (string? name)) + (let [reference (ref! :unset-option value [Server Session Window Pane])] + (execute! :unset-option :mutation #(.unset (options-reference reference) name)))) + +(defn environment! + "Returns values and explicitly removed names, preserving removal versus absence." + ([value] (environment! value :local)) + ([value scope] + (require! :environment (#{:local :effective} scope)) + (let [reference (ref! :environment value [Server Session])] + (execute! :environment :read + #(let [^Environment environment (call reference environment [Server Session])] + {:values (into {} (if (= scope :local) (.all environment) (.effective environment))) + :removed (set (.removed environment))}))))) +(defn set-environment! "Sets a literal environment value." [value name text] + (require! :set-environment (and (string? name) (string? text))) + (let [reference (ref! :set-environment value [Server Session])] + (execute! :set-environment :mutation #(.set (environment-reference reference) name text)))) +(defn unset-environment! "Removes the local environment entry, allowing inheritance." [value name] + (require! :unset-environment (string? name)) + (let [reference (ref! :unset-environment value [Server Session])] + (execute! :unset-environment :mutation #(.unset (environment-reference reference) name)))) +(defn remove-environment! "Marks an environment name removed for new child processes." [value name] + (require! :remove-environment (string? name)) + (let [reference (ref! :remove-environment value [Server Session])] + (execute! :remove-environment :mutation #(.remove (environment-reference reference) name)))) + +(defn hooks! "Returns hook names and their ordered command vectors." [value] + (let [reference (ref! :hooks value [Server Session Window Pane])] + (execute! :hooks :read #(into {} (map (fn [[k v]] [k (vec v)])) + (.all (hooks-reference reference)))))) +(defn set-hook! "Replaces a hook with one explicit tmux argv command." [value event argv] + (require! :set-hook (and (string? event) (argv? argv))) + (let [reference (ref! :set-hook value [Server Session Window Pane])] + (execute! :set-hook :mutation #(.set (hooks-reference reference) ^String event ^java.util.List argv)))) +(defn append-hook! "Appends one explicit tmux argv command to a hook." [value event argv] + (require! :append-hook (and (string? event) (argv? argv))) + (let [reference (ref! :append-hook value [Server Session Window Pane])] + (execute! :append-hook :mutation #(.append (hooks-reference reference) ^String event ^java.util.List argv)))) +(defn unset-hook! "Removes an explicit hook." [value event] + (require! :unset-hook (string? event)) + (let [reference (ref! :unset-hook value [Server Session Window Pane])] + (execute! :unset-hook :mutation #(.unset (hooks-reference reference) event)))) + +(defn set-buffer! "Stores literal buffer contents." [server name text] + (require! :set-buffer (and (string? name) (string? text))) + (let [^Server server (ref! :set-buffer server [Server])] + (execute! :set-buffer :mutation #(.set (.buffers server) name text)))) +(defn show-buffer! "Reads literal buffer contents." [server name] + (require! :show-buffer (string? name)) + (let [^Server server (ref! :show-buffer server [Server])] + (execute! :show-buffer :read #(.show (.buffers server) name)))) +(defn delete-buffer! "Deletes an explicit buffer." [server name] + (require! :delete-buffer (string? name)) + (let [^Server server (ref! :delete-buffer server [Server])] + (execute! :delete-buffer :mutation #(.delete (.buffers server) name)))) +(defn paste-buffer! "Pastes an explicit buffer into a pane." [pane name] + (require! :paste-buffer (string? name)) + (let [^Pane pane (ref! :paste-buffer pane [Pane])] + (execute! :paste-buffer :mutation #(.pasteBuffer pane name)))) + + +(defn buffers! "Lists captured buffer names and byte sizes." [server] + (let [^Server server (ref! :buffers server [Server])] + (execute! :buffers :read + #(mapv (fn [^BufferInfo buffer] + {:buffer/name (.name buffer) :buffer/size (.size buffer)}) + (.list (.buffers server)))))) + + +(defn await-channel! + "Waits on an explicit tmux channel while reserving capacity for its release. + Returns :signalled, :timed-out, or :server-gone; interruption propagates." + [server name timeout] + (require! :await-channel (and (string? name) (duration? timeout))) + (let [^Server server (ref! :await-channel server [Server])] + (execute! :await-channel :wait + #(case (.name (.awaitReservingCapacity (.channel server name) timeout)) + "SIGNALLED" :signalled + "TIMED_OUT" :timed-out + "SERVER_GONE" :server-gone)))) + +(defn signal-channel! "Signals an explicit tmux channel." [server name] + (require! :signal-channel (string? name)) + (let [^Server server (ref! :signal-channel server [Server])] + (execute! :signal-channel :mutation #(.signal (.channel server name))))) + + +(defn- operation-result [index ^OperationResult result] + {:operation/index index + :operation/argv (vec (.argv result)) + :operation/outcome (case (.name (.outcome result)) + "COMPLETE" :complete + "FAILED" :failed + "SKIPPED" :skipped + "UNKNOWN" :unknown) + :operation/stdout (vec (.stdout result)) + :operation/stderr (vec (.stderr result))}) + +(defn- group! [operation server operations options] + (closed! operation options {:kind #{:mutation :wait}}) + (require! operation (and (vector? operations) (every? argv? operations))) + (require! operation + (or (= :wait (:kind options)) + (not-any? (fn [argv] + (and (#{"wait-for" "wait"} (first argv)) + (not-any? #{"-S" "-U"} (rest argv)))) + operations))) + (let [^Server server (ref! operation server [Server]) + waiting? (= :wait (:kind options))] + (execute! + operation (if waiting? :wait :mutation) + #(try + (let [^BatchResult result + (if (= operation :batch) + (let [builder (if waiting? (.batchReservingCapacity server) (.batch server))] + (doseq [argv operations] (.add builder ^java.util.List argv)) + (.run builder)) + (let [builder (if waiting? (.chainReservingCapacity server) (.chain server))] + (doseq [argv operations] (.then builder ^java.util.List argv)) + (.run builder)))] + (mapv operation-result (range) (.operations result))) + (catch DispatchException cause + (let [error (translate operation true cause) + outcome (if (= :not-dispatched (:tmux/dispatch (ex-data error))) + :skipped :unknown)] + (throw (ex-info "tmux command group failed" + (assoc (ex-data error) :tmux/results + (mapv (fn [index argv] + {:operation/index index :operation/argv argv + :operation/outcome outcome + :operation/stdout [] :operation/stderr []}) + (range) operations)) + cause)))))))) + +(defn batch! + "Runs independent argv commands using a fresh Java Batch and returns positions. + Supply explicit targets. Failed/skipped outcomes are data; transport failure + throws with :tmux/results. Use {:kind :wait} for groups needing a release, + including blocking shell commands. No rollback or retry safety is promised." + ([server operations] (batch! server operations {})) + ([server operations options] (group! :batch server operations options))) + +(defn chain! + "Runs dependent argv commands with tmux's implicit target between positions. + Uses a fresh Java CommandChain. Failed/skipped outcomes are data; transport + failure throws with :tmux/results. Use {:kind :wait} for groups needing a release, + including blocking shell commands. No rollback or retry safety is promised." + ([server operations] (chain! server operations {})) + ([server operations options] (group! :chain server operations options))) diff --git a/libtmux-clojure/src/main/clojure/libtmux/data.clj b/libtmux-clojure/src/main/clojure/libtmux/data.clj new file mode 100644 index 000000000..4419d7237 --- /dev/null +++ b/libtmux-clojure/src/main/clojure/libtmux/data.clj @@ -0,0 +1,232 @@ +(ns libtmux.data + "Pure captured values, identity, EDN projection, and local selection." + (:import [io.github.libtmux CapturedServer Client Pane ServerIdentity Session Window] + [io.github.libtmux.query FilterExpr] + [io.github.libtmux.snapshot WindowContext] + [java.time Instant] + [java.util List UUID])) + +(set! *warn-on-reflection* true) + +(defn- cardinality-error [operation reason] + (ex-info "Selection cardinality does not match the requested result" + {:tmux/error :tmux/cardinality + :tmux/operation operation + :tmux/phase :selection + :tmux/dispatch :not-dispatched + :cardinality/reason reason})) + +(defn one-or-none + "Returns the only value, nil for none, or throws for multiple values. + Stops requesting matches once ambiguity is known. Finding zero or one may + scan all input; chunked producers may realize more. Nil and false are values." + [values] + (when-let [items (seq values)] + (when (next items) + (throw (cardinality-error :one-or-none :multiple))) + (first items))) + +(defn exactly-one + "Returns the only value or throws ex-info for zero or multiple values. + Stops requesting matches once ambiguity is known. Finding zero or one may + scan all input; chunked producers may realize more. Nil and false are values." + [values] + (let [items (seq values)] + (when-not items + (throw (cardinality-error :exactly-one :none))) + (when (next items) + (throw (cardinality-error :exactly-one :multiple))) + (first items))) + +(defn data + "Projects captured data to EDN, removing :tmux/ref at every map level. + Instants become ISO-8601 strings; sequences and Java lists become vectors. + Unsupported JVM objects throw rather than producing unreadable EDN." + [value] + (cond + (map? value) (into {} (comp (remove #(= :tmux/ref (key %))) + (map (fn [[k v]] [(data k) (data v)]))) value) + (set? value) (into #{} (map data) value) + (or (sequential? value) (instance? List value)) (mapv data value) + (instance? Instant value) (str value) + (or (nil? value) (string? value) (number? value) (boolean? value) + (keyword? value) (symbol? value) (char? value) (instance? UUID value)) value + :else (throw (ex-info "Value has no supported EDN projection" + {:tmux/error :tmux/invalid-data + :tmux/operation :data + :tmux/phase :projection + :tmux/dispatch :not-dispatched})))) + +(defn entity-key + "Returns a captured physical identity, qualified by endpoint and server PID. + PID reuse is not a stronger daemon-incarnation guarantee." + [entry] + (or (:tmux/identity entry) + (throw (ex-info "Value has no captured entity identity" + {:tmux/error :tmux/invalid-data + :tmux/operation :entity-key + :tmux/phase :selection + :tmux/dispatch :not-dispatched})))) + +(defn link-key + "Returns the physical identity and contextual window placement. + Only window and pane occurrences carry a link; missing context throws." + [entry] + (let [identity (entity-key entry)] + (if-let [link (:tmux/link entry)] + [identity link] + (throw (ex-info "Value has no captured window placement" + {:tmux/error :tmux/invalid-data + :tmux/operation :link-key + :tmux/phase :selection + :tmux/dispatch :not-dispatched}))))) + +(defn same-entity? + "Compares physical entity identities without acquiring current state." + [left right] + (= (entity-key left) (entity-key right))) + +(defn same-link? + "Compares occurrences, including the session, window ID, and index." + [left right] + (= (link-key left) (link-key right))) + +(defn predicate + "Adapts a Java FilterExpr to a predicate over captured entries. + Evaluates the preserved :tmux/ref, including Java relation semantics; + editing fields in an entry does not change the reference being queried." + [^FilterExpr expression] + (when-not (instance? FilterExpr expression) + (throw (IllegalArgumentException. "Expected a Java FilterExpr"))) + (fn [entry] + (if-let [reference (:tmux/ref entry)] + (.test expression reference) + (throw (ex-info "Detached data has no query reference" + {:tmux/error :tmux/invalid-data + :tmux/operation :predicate + :tmux/phase :selection + :tmux/dispatch :not-dispatched}))))) + +(defn matching + "Filters captured entries using a Java FilterExpr over their preserved refs. + One argument returns a transducer; two return a lazy sequence. Both are local + and preserve order and multiplicity. Detached maps have no query reference." + ([expression] (filter (predicate expression))) + ([expression entries] (filter (predicate expression) entries))) + +(defn windows + "Returns the captured window occurrences in a snapshot or session." + [value] + (or (:tmux/windows value) (:session/windows value) + (throw (IllegalArgumentException. "Expected a snapshot or session")))) + +(defn panes + "Returns captured pane occurrences in a snapshot, session, or window." + [value] + (or (:tmux/panes value) (:window/panes value) + (when-let [ws (:session/windows value)] + (into [] (mapcat :window/panes) ws)) + (throw (IllegalArgumentException. "Expected a snapshot, session, or window")))) + +(defn active-window + "Returns the captured active window of a session, or nil." + [session] + (one-or-none (filter :window/active? (windows session)))) + +(defn active-pane + "Returns the captured active pane of a window or session, or nil." + [value] + (if (contains? value :session/windows) + (some-> (active-window value) active-pane) + (one-or-none (filter :pane/active? (panes value))))) + +(defn- placement [^WindowContext context] + {:session/id (.value (.session context)) + :window/id (.value (.window context)) + :window/index (.value (.index context))}) + +(defn- physical [server kind id] + (assoc server :tmux/kind kind :tmux/id id)) + +(defn- pane-entry [server ^Pane pane] + (let [size (.size pane) + position (.position pane) + edges (.edges pane) + pid (.pid pane)] + {:tmux/ref pane + :tmux/identity (physical server :pane (.value (.id pane))) + :tmux/link (placement (.context (.window pane))) + :pane/id (.value (.id pane)) + :pane/index (.index pane) + :pane/active? (.active pane) + :pane/current-command (.currentCommand pane) + :pane/floating? (.orElse (.floating pane) nil) + :pane/width (.width size) + :pane/height (.height size) + :pane/left (.left position) + :pane/top (.top position) + :pane/title (.title pane) + :pane/current-path (.currentPathText pane) + :pane/pid (when (.isPresent pid) (.getAsLong pid)) + :pane/edges {:top? (.top edges) :bottom? (.bottom edges) + :left? (.left edges) :right? (.right edges)}})) + +(defn- window-entry [server by-link ^Window window] + (let [link (placement (.context window)) + size (.size window)] + {:tmux/ref window + :tmux/identity (physical server :window (.value (.id window))) + :tmux/link link + :window/id (.value (.id window)) + :window/index (.value (.index window)) + :window/name (.name window) + :window/active? (.active window) + :window/linked? (.linked window) + :window/width (.width size) + :window/height (.height size) + :window/layout (.value (.layout window)) + :window/panes (get by-link link [])})) + +(defn- session-entry [server by-session ^Session session] + {:tmux/ref session + :tmux/identity (physical server :session (.value (.id session))) + :session/id (.value (.id session)) + :session/name (.name session) + :session/attached? (.attached session) + :session/windows (get by-session (.value (.id session)) [])}) + +(defn- client-entry [server by-id ^Client client] + (let [^Session session (.orElse (.session client) nil) + entry (when session (get by-id (.value (.id session))))] + {:tmux/ref client + :tmux/identity (physical server :client (.name client)) + :client/name (.name client) + :client/session entry + :client/active-window (when entry (active-window entry)) + :client/active-pane (when entry (active-pane entry))})) + +(defn from-capture + "Converts an authenticated Java CapturedServer to eager persistent values. + Reads no live getters and issues no commands. Relations share these values; + window links and pane occurrences retain their captured order and placement." + [^CapturedServer capture] + (let [^ServerIdentity server-identity (.identity capture) + pid (.processId server-identity) + server {:tmux/realm (.realm server-identity) + :tmux/server (.server server-identity) + :tmux/pid (when (.isPresent pid) (.getAsLong pid))} + snapshot (.snapshot capture) + ps (mapv #(pane-entry server %) (.panes capture)) + by-link (group-by :tmux/link ps) + ws (mapv #(window-entry server by-link %) (.windows capture)) + by-session (group-by (comp :session/id :tmux/link) ws) + ss (mapv #(session-entry server by-session %) (.sessions capture)) + by-id (into {} (map (juxt :session/id identity)) ss)] + {:tmux/ref (.server capture) + :tmux/identity (physical server :server (.server server-identity)) + :tmux/captured-at (.capturedAt snapshot) + :tmux/version (some-> (.orElse (.serverVersion snapshot) nil) str) + :tmux/sessions ss + :tmux/windows ws + :tmux/panes ps + :tmux/clients (mapv #(client-entry server by-id %) (.clients capture))})) diff --git a/libtmux-clojure/src/test/clojure/libtmux/async_test.clj b/libtmux-clojure/src/test/clojure/libtmux/async_test.clj new file mode 100644 index 000000000..8df2ca1d5 --- /dev/null +++ b/libtmux-clojure/src/test/clojure/libtmux/async_test.clj @@ -0,0 +1,394 @@ +(ns libtmux.async-test + (:require [clojure.test :refer [deftest is testing]] + [libtmux.async :as async] + [libtmux.core :as core]) + (:import [java.util.concurrent CompletableFuture CompletionStage CountDownLatch Future FutureTask + ThreadPoolExecutor TimeUnit])) + +(def ^:dynamic *submitted-value* nil) + +(defn- await-uninterruptibly [^CountDownLatch latch] + (loop [] + (when-not (try (.await latch) true + (catch InterruptedException _ false)) + (recur)))) + +(deftest runtime-requires-a-release-slot + (is (thrown? IllegalArgumentException (async/runtime! {:max-running 1})))) + +(deftest submission-conveys-bindings-and-stage-is-read-only + (with-open [^libtmux.async.AsyncRuntime runtime (async/runtime!)] + (let [task (binding [*submitted-value* :captured] + (async/submit! runtime (fn [] *submitted-value*))) + stage (async/completion-stage task)] + (is (= :captured @task)) + (is (thrown? UnsupportedOperationException + (.complete ^java.util.concurrent.CompletableFuture stage :changed)))))) + +(deftest timed-deref-does-not-cancel-work + (with-open [^libtmux.async.AsyncRuntime runtime (async/runtime!)] + (let [release (CountDownLatch. 1) + task (async/submit! runtime #(.await release))] + (is (= ::waiting (deref task 20 ::waiting))) + (is (false? (realized? task))) + (.countDown release) + (is (nil? (deref task 500 ::timeout)))))) + +(deftest queued-cancellation-is-not-dispatched + (with-open [^libtmux.async.AsyncRuntime runtime + (async/runtime! {:max-running 2 :queue-limit 2})] + (let [release (CountDownLatch. 1) + running (async/submit! runtime #(.await release)) + invoked (atom false) + queued (async/submit! runtime #(reset! invoked true))] + (async/cancel! queued) + (is (thrown-with-msg? clojure.lang.ExceptionInfo #"cancelled" @queued)) + (is (false? @invoked)) + (.countDown release) + (deref running 500 ::timeout)))) + +(deftest wait-tasks-do-not-block-an-ordinary-release + (with-open [^libtmux.async.AsyncRuntime runtime (async/runtime! {:max-running 2})] + (let [release (CountDownLatch. 1) + waiting (async/submit! runtime {:kind :wait} #(.await release)) + ordinary (async/submit! runtime #(.countDown release))] + (is (nil? (deref ordinary 500 ::timeout))) + (is (nil? (deref waiting 500 ::timeout)))))) + +(deftest deadline-interrupts-worker + (with-open [^libtmux.async.AsyncRuntime runtime (async/runtime!)] + (let [interrupted (promise) + block (CountDownLatch. 1) + task (async/submit! runtime {:timeout-ms 30} + #(try + (.await block) + (catch InterruptedException _ + (deliver interrupted true))))] + (is (thrown? clojure.lang.ExceptionInfo @task)) + (is (= true (deref interrupted 500 ::timeout)))))) + +(deftest close-rejects-new-work + (let [^libtmux.async.AsyncRuntime runtime (async/runtime!)] + (.close runtime) + (is (thrown? IllegalStateException (async/submit! runtime identity))))) + +(deftest running-cancellation-waits-for-worker-cleanup + (with-open [^libtmux.async.AsyncRuntime runtime (async/runtime!)] + (let [block (CountDownLatch. 1) + started (CountDownLatch. 1) + cleaning (CountDownLatch. 1) + cleaned (CountDownLatch. 1) + task (async/submit! runtime + #(try + (.countDown started) + (.await block) + (catch InterruptedException _ + (.countDown cleaning) + (.await cleaned))))] + (is (.await started 500 TimeUnit/MILLISECONDS)) + (async/cancel! task) + (is (.await cleaning 500 TimeUnit/MILLISECONDS)) + (is (= :cancelling (async/task-state task))) + (is (false? (realized? task))) + (.countDown cleaned) + (is (thrown? clojure.lang.ExceptionInfo @task))))) + +(deftest wait-operation-requires-wait-kind + (with-open [^libtmux.async.AsyncRuntime runtime (async/runtime!)] + (let [task (async/submit! runtime + #(core/*execution-check* {:kind :wait}))] + (is (thrown-with-msg? clojure.lang.ExceptionInfo + #"requires a :wait task" @task))))) + +(deftest admission-is-globally-bounded + (with-open [^libtmux.async.AsyncRuntime runtime + (async/runtime! {:max-running 2 :queue-limit 1})] + (let [release (CountDownLatch. 1) + tasks [(async/submit! runtime #(.await release)) + (async/submit! runtime {:kind :wait} #(.await release)) + (async/submit! runtime #(.await release))] + rejected (async/submit! runtime #(.await release))] + (is (= {:status :busy :kind :ordinary} rejected)) + (.countDown release) + (doseq [task tasks] (deref task 500 ::timeout))))) + +(deftest submission-is-rejected-inside-a-transaction + (with-open [^libtmux.async.AsyncRuntime runtime (async/runtime!)] + (is (thrown? IllegalStateException + (dosync (async/submit! runtime identity)))))) + +(deftest managed-continuation-uses-owned-callback-executor + (with-open [^libtmux.async.AsyncRuntime runtime (async/runtime!)] + (let [task (async/submit! runtime (constantly :ok)) + ^CompletionStage stage (async/then! runtime task + (fn [_] (.getName (Thread/currentThread))))] + (is (thrown? UnsupportedOperationException + (.complete ^CompletableFuture stage :changed))) + (is (.startsWith ^String (.get (.toCompletableFuture stage) 500 TimeUnit/MILLISECONDS) + "libtmux-callback-"))))) + +(deftest timeout-validation-precedes-dispatch + (with-open [^libtmux.async.AsyncRuntime runtime (async/runtime!)] + (doseq [timeout [0 -1 1.5 "1"]] + (is (thrown? IllegalArgumentException + (async/submit! runtime {:timeout-ms timeout} identity)))))) + +(deftest cancelled-queued-task-releases-admission + (with-open [^libtmux.async.AsyncRuntime runtime + (async/runtime! {:max-running 2 :queue-limit 1})] + (let [release (CountDownLatch. 1) + running (async/submit! runtime #(.await release)) + waiting (async/submit! runtime {:kind :wait} #(.await release)) + queued (async/submit! runtime identity)] + (is (true? (async/cancel! queued))) + (let [outcome (try (deref queued 500 ::timeout) + (catch clojure.lang.ExceptionInfo error error))] + (is (instance? clojure.lang.ExceptionInfo outcome)) + (is (= :cancelled (:tmux/error (ex-data outcome))))) + (is (instance? libtmux.async.Task (async/submit! runtime identity))) + (.countDown release) + (deref running 500 ::timeout) + (deref waiting 500 ::timeout)))) + +(deftest wait-admission-preserves-an-ordinary-slot + (with-open [^libtmux.async.AsyncRuntime runtime + (async/runtime! {:max-running 2 :queue-limit 1})] + (let [release (CountDownLatch. 1) + waits [(async/submit! runtime {:kind :wait} #(.await release)) + (async/submit! runtime {:kind :wait} #(.await release))] + rejected-wait (async/submit! runtime {:kind :wait} identity) + ordinary (async/submit! runtime #(.countDown release))] + (is (= {:status :busy :kind :wait} rejected-wait)) + (is (nil? (deref ordinary 500 ::timeout))) + (doseq [task waits] (deref task 500 ::timeout))))) + +(deftest dequeued-cancellation-still-finalizes + (with-open [^libtmux.async.AsyncRuntime runtime + (async/runtime! {:max-running 2 :queue-limit 1})] + (let [release (CountDownLatch. 1) + running (async/submit! runtime #(.await release)) + queued (async/submit! runtime identity) + ^ThreadPoolExecutor pool (.-ordinary runtime) + ^Future future @(.-worker ^libtmux.async.Task queued)] + ;; Model the executor boundary after dequeue and before Callable.call. + (is (.remove pool ^Runnable future)) + (is (true? (async/cancel! queued))) + (let [outcome (try (deref queued 300 ::stuck) + (catch clojure.lang.ExceptionInfo error error))] + (is (instance? clojure.lang.ExceptionInfo outcome)) + (is (re-find #"cancelled" (.getMessage ^Throwable outcome)))) + (is (instance? libtmux.async.Task (async/submit! runtime identity))) + (.countDown release) + (deref running 500 ::timeout)))) + +(deftest first-cancellation-wins + (with-open [^libtmux.async.AsyncRuntime runtime (async/runtime!)] + (let [started (CountDownLatch. 1) + release (CountDownLatch. 1) + task (async/submit! runtime #(do (.countDown started) (.await release)))] + (is (.await started 500 TimeUnit/MILLISECONDS)) + (is (true? (async/cancel! task))) + (is (false? (#'async/request-cancel! task :timeout))) + (.countDown release) + (let [failure (try @task (catch clojure.lang.ExceptionInfo error error))] + (is (= :cancelled (:tmux/error (ex-data failure)))))))) + +(deftest pending-continuations-are-bounded-before-parent-completes + (with-open [^libtmux.async.AsyncRuntime runtime + (async/runtime! {:queue-limit 1})] + (let [release (CountDownLatch. 1) + task (async/submit! runtime #(.await release))] + (async/then! runtime task identity) + (async/then! runtime task identity) + (let [failure (try (async/then! runtime task identity) + (catch clojure.lang.ExceptionInfo error error))] + (is (= :busy (:tmux/error (ex-data failure)))) + (is (= :not-dispatched (:tmux/dispatch (ex-data failure))))) + (.countDown release) + (deref task 500 ::timeout)))) + +(deftest close-budget-includes-blocking-inline-stage-callback + (let [runtime (async/runtime! {:close-timeout-ms 40}) + release-task (CountDownLatch. 1) + release-callback (CountDownLatch. 1) + task (async/submit! runtime #(.await release-task)) + stage (.toCompletableFuture ^CompletionStage (async/completion-stage task))] + (.whenComplete stage + (reify java.util.function.BiConsumer + (accept [_ _ _] (.await release-callback)))) + (let [started (System/nanoTime) + failure (try (.close ^libtmux.async.AsyncRuntime runtime) + (catch clojure.lang.ExceptionInfo error error)) + elapsed-ms (.toMillis TimeUnit/NANOSECONDS (- (System/nanoTime) started))] + (is (= :cleanup-pending (:tmux/error (ex-data failure)))) + (is (< elapsed-ms 500))) + (.countDown release-task) + (.countDown release-callback))) + +(deftest late-worker-cleanup-remains-publishable-after-failed-close + (let [runtime (async/runtime! {:close-timeout-ms 40}) + started (CountDownLatch. 1) + release (CountDownLatch. 1) + task (async/submit! runtime + #(do + (.countDown started) + (await-uninterruptibly release)))] + (is (.await started 500 TimeUnit/MILLISECONDS)) + (let [failure (try (.close ^libtmux.async.AsyncRuntime runtime) + (catch clojure.lang.ExceptionInfo error error))] + (is (= :cleanup-pending (:tmux/error (ex-data failure)))) + (is (= :cancelling (async/task-state task))) + (is (false? (realized? task)))) + (.countDown release) + (let [outcome (try (deref task 500 ::stuck) + (catch clojure.lang.ExceptionInfo error error))] + (is (instance? clojure.lang.ExceptionInfo outcome)) + (is (= :cancelled (:tmux/error (ex-data outcome))))) + (is (nil? (.close ^libtmux.async.AsyncRuntime runtime))))) + +(deftest lifecycle-effects-are-rejected-inside-transactions + (let [runtime (async/runtime!) + started (CountDownLatch. 1) + release (CountDownLatch. 1) + task (async/submit! runtime #(do (.countDown started) (.await release)))] + (is (.await started 500 TimeUnit/MILLISECONDS)) + (is (thrown? IllegalStateException (dosync (async/cancel! task)))) + (is (= :running (async/task-state task))) + (is (thrown? IllegalStateException (dosync (async/then! runtime task identity)))) + (is (thrown? IllegalStateException (dosync (async/close-runtime! runtime)))) + (.countDown release) + (deref task 500 ::timeout) + (.close ^libtmux.async.AsyncRuntime runtime))) + +(deftest blocked-inline-completions-retain-bounded-publication-admission + (let [runtime (async/runtime! {:max-running 2 :queue-limit 1}) + release-callbacks (CountDownLatch. 1) + tasks (atom [])] + (try + (dotimes [_ 3] + (let [entered (CountDownLatch. 1) + release-task (CountDownLatch. 1) + task (async/submit! runtime #(.await release-task)) + stage (.toCompletableFuture ^CompletionStage (async/completion-stage task))] + (.whenComplete stage + (reify java.util.function.BiConsumer + (accept [_ _ _] + (.countDown entered) + (.await release-callbacks)))) + (swap! tasks conj task) + (.countDown release-task) + (is (.await entered 500 TimeUnit/MILLISECONDS)))) + (is (= {:status :busy :kind :ordinary} + (async/submit! runtime identity))) + (finally + (.countDown release-callbacks) + (doseq [task @tasks] (deref task 500 ::timeout)) + (.close ^libtmux.async.AsyncRuntime runtime))))) + +(deftest accepted-cancellation-wins-after-thunk-return + (with-open [^libtmux.async.AsyncRuntime runtime (async/runtime!)] + (let [entered-finalization (CountDownLatch. 1) + release-finalization (CountDownLatch. 1) + real-finish @#'async/finish!] + (with-redefs [async/finish! + (fn [task throwable value] + (.countDown entered-finalization) + (await-uninterruptibly release-finalization) + (real-finish task throwable value))] + (let [task (async/submit! runtime (constantly :success))] + (is (.await entered-finalization 500 TimeUnit/MILLISECONDS)) + (is (true? (async/cancel! task))) + (.countDown release-finalization) + (let [outcome (try (deref task 500 ::stuck) + (catch clojure.lang.ExceptionInfo error error))] + (is (= :cancelled (:tmux/error (ex-data outcome)))))))))) + +(deftest cancellation-retains-structured-partial-effect-failure + (let [transport (ex-info "group interrupted" + {:tmux/error :tmux/transport + :tmux/dispatch :not-dispatched + :tmux/results [{:index 0 :status :unknown}]}) + cancellation (#'async/failure :cancelled :unknown) + combined (#'async/reconcile-failure cancellation transport)] + (is (= :cancelled (:tmux/error (ex-data combined)))) + (is (= :unknown (:tmux/dispatch (ex-data combined)))) + (is (= [{:index 0 :status :unknown}] (:tmux/results (ex-data combined)))) + (is (identical? transport (.getCause ^Throwable combined))))) + +(deftest deadline-and-executor-rejection-share-one-finalizer + (with-open [^libtmux.async.AsyncRuntime runtime + (async/runtime! {:max-running 4 :queue-limit 1})] + (let [release (CountDownLatch. 1) + blockers (vec (repeatedly 4 #(async/submit! runtime + (fn [] (await-uninterruptibly release))))) + publishing (CountDownLatch. 1) + deadline-fired (CountDownLatch. 1) + allow-interrupt (CountDownLatch. 1) + result (atom nil) + real-reset reset! + real-interrupt @#'async/interrupt! + real-execute @#'async/execute!] + (with-redefs [async/execute! + (fn [executor task] + (if (instance? ThreadPoolExecutor executor) + (do + (.countDown publishing) + (await-uninterruptibly deadline-fired) + (.countDown allow-interrupt) + (throw (java.util.concurrent.RejectedExecutionException. "forced full pool"))) + (real-execute executor task))) + async/interrupt! + (fn [task] + (.countDown deadline-fired) + (await-uninterruptibly allow-interrupt) + (real-interrupt task))] + (let [submitter (.start (Thread/ofVirtual) + ^Runnable #(real-reset result + (async/submit! runtime {:timeout-ms 1} + identity)))] + (is (.await publishing 500 TimeUnit/MILLISECONDS)) + (is (.await deadline-fired 500 TimeUnit/MILLISECONDS)) + (.join submitter 500) + (is (not (.isAlive submitter))) + (is (= {:status :busy :kind :ordinary} @result)))) + (.countDown release) + (doseq [task blockers] (deref task 500 ::stuck)) + (is (= 5 (.availablePermits ^java.util.concurrent.Semaphore + (.-admission runtime))))))) + +(deftest close-never-succeeds-with-an-open-completion-executor + (let [^libtmux.async.AsyncRuntime runtime (async/runtime! {:close-timeout-ms 100}) + fake (CompletableFuture.) + callback-entered (CountDownLatch. 1) + release-callback (CountDownLatch. 1) + callback-removed (CountDownLatch. 1) + eligibility-checked (CountDownLatch. 1) + allow-close (CountDownLatch. 1) + close-result (atom nil) + real-check @#'async/shutdown-completion-if-idle!] + (swap! (.-callback-stages runtime) conj fake) + (.whenComplete fake + (reify java.util.function.BiConsumer + (accept [_ _ _] + (.countDown callback-entered) + (await-uninterruptibly release-callback) + (swap! (.-callback-stages runtime) disj fake) + (.countDown callback-removed)))) + (with-redefs [async/shutdown-completion-if-idle! + (fn [owner] + (real-check owner) + (.countDown eligibility-checked) + (await-uninterruptibly allow-close))] + (let [closer (.start (Thread/ofVirtual) + ^Runnable #(reset! close-result + (try (.close ^libtmux.async.AsyncRuntime runtime) + :closed + (catch clojure.lang.ExceptionInfo error error))))] + (is (.await callback-entered 500 TimeUnit/MILLISECONDS)) + (is (.await eligibility-checked 500 TimeUnit/MILLISECONDS)) + (.countDown release-callback) + (is (.await callback-removed 500 TimeUnit/MILLISECONDS)) + (.countDown allow-close) + (.join closer 500) + (is (= :cleanup-pending (:tmux/error (ex-data @close-result)))))) + (is (nil? (.close ^libtmux.async.AsyncRuntime runtime))))) diff --git a/libtmux-clojure/src/test/clojure/libtmux/control_test.clj b/libtmux-clojure/src/test/clojure/libtmux/control_test.clj new file mode 100644 index 000000000..17b8d0265 --- /dev/null +++ b/libtmux-clojure/src/test/clojure/libtmux/control_test.clj @@ -0,0 +1,151 @@ +(ns libtmux.control-test + (:require [clojure.test :refer [deftest is]] + [libtmux.control :as control]) + (:import [io.github.libtmux PaneId] + [io.github.libtmux.batch OperationOutcome] + [io.github.libtmux.control ControlReply EventSubscription PaneOutput] + [java.lang AutoCloseable] + [java.nio ByteBuffer] + [java.time Duration] + [java.util.concurrent CountDownLatch TimeUnit] + [java.util.concurrent.locks ReentrantLock] + [java.util.function Consumer])) + +(defn- test-stream [encoding capacity] + (let [constructor (.getDeclaredConstructor EventSubscription + (into-array Class [Integer/TYPE Consumer])) + _ (.setAccessible constructor true) + subscription (.newInstance constructor + (object-array [(int capacity) (reify Consumer (accept [_ _]))]))] + (libtmux.control.OutputStream. + subscription {:tmux/endpoint :test} {:encoding encoding :capacity capacity} + (atom {:sequence 0}) (ReentrantLock.)))) + +(defn- offer! [^libtmux.control.OutputStream stream pane text] + (let [method (.getDeclaredMethod EventSubscription "offer" (into-array Class [Object]))] + (.setAccessible method true) + (.invoke method (.-subscription stream) + (object-array [(PaneOutput. (PaneId. pane) text + (ByteBuffer/wrap (.getBytes ^String text "UTF-8")))])))) + +(defn- end! [^libtmux.control.OutputStream stream cause] + (let [method (.getDeclaredMethod EventSubscription "end" (into-array Class [Throwable]))] + (.setAccessible method true) + (.invoke method (.-subscription stream) (object-array [cause])))) + +(deftest protocol-complete-does-not-claim-command-completion + (doseq [[status expected] [[OperationOutcome/COMPLETE :complete] + [OperationOutcome/FAILED :failed] + [OperationOutcome/UNKNOWN :unknown]]] + (is (= {:reply/status expected :reply/lines ["reply"] + :operation/completion :unverified} + (#'control/reply-data (ControlReply. status ["reply"])))))) + +(deftest invalid-input-does-not-contact-tmux + (doseq [action [#(control/attach! {}) #(control/send! nil ["list-panes"]) + #(control/subscribe! nil) #(control/next! nil) #(control/close! nil) + #(control/subscribe! nil {:max-bytes 1})]] + (let [failure (try (action) (catch clojure.lang.ExceptionInfo error error))] + (is (= :tmux/validation (:tmux/error (ex-data failure)))) + (is (= :not-dispatched (:tmux/dispatch (ex-data failure))))))) + +(deftest events-carry-bytes-or-text-by-encoding + (with-open [^AutoCloseable raw (test-stream :bytes 4) + ^AutoCloseable text (test-stream :utf-8 4)] + (offer! raw "%1" "é") + (offer! text "%1" "é") + (let [event (control/next! raw Duration/ZERO)] + (is (= :event (:stream/status event))) + (is (= [-61 -87] (vec (:output/bytes event)))) + (is (= "%1" (:pane/id event))) + (is (= 1 (:event/sequence event)))) + (is (= "é" (:output/text (control/next! text Duration/ZERO)))))) + +(deftest overflow-is-a-gap-ahead-of-the-events-that-remain + (with-open [^AutoCloseable stream (test-stream :utf-8 1)] + (offer! stream "%1" "a") + (offer! stream "%1" "b") + (offer! stream "%1" "c") + (let [gap (control/next! stream Duration/ZERO) + event (control/next! stream Duration/ZERO)] + (is (= :gap (:stream/status gap))) + (is (= 2 (:gap/events gap))) + (is (= {:upstream/events 2} (:loss gap))) + (is (= "c" (:output/text event))) + (is (= {:upstream/events 2} (:loss event))) + (is (= {:stream/status :timeout} (control/next! stream Duration/ZERO)))))) + +(deftest timed-read-includes-waiting-for-another-reader + (with-open [^libtmux.control.OutputStream stream (test-stream :bytes 2)] + (let [^ReentrantLock lock (.-read-lock stream) + finished (CountDownLatch. 1) + outcome (atom nil)] + (.lock lock) + (let [worker (.start (Thread/ofVirtual) + ^Runnable #(try (reset! outcome (control/next! stream Duration/ZERO)) + (finally (.countDown finished))))] + (try + (is (.await finished 900 TimeUnit/MILLISECONDS)) + (finally (.unlock lock) (.join worker 900))) + (is (= {:stream/status :timeout} @outcome)))))) + +(deftest closeable-stream-rejects-retriable-transaction-effects + (with-open [^AutoCloseable stream (test-stream :bytes 2)] + (is (= :rejected (try (dosync (.close stream)) :closed + (catch IllegalStateException _ :rejected)))) + (is (= {:stream/status :timeout} (control/next! stream Duration/ZERO))))) + +(deftest client-termination-drains-buffer-and-preserves-original-cause + (with-open [^AutoCloseable stream (test-stream :bytes 2)] + (let [cause (java.io.IOException. "control lost")] + (offer! stream "%1" "A") + (offer! stream "%1" "B") + (end! stream cause) + (is (nil? (control/terminal stream))) + (is (= [65 66] (mapv (fn [_] (first (:output/bytes (control/next! stream Duration/ZERO)))) + (range 2)))) + (let [outcome (:stream/terminal (control/next! stream Duration/ZERO))] + (is (= :ended (:reason outcome))) + (is (identical? cause (:cause outcome))) + (is (identical? outcome (:stream/terminal (control/next! stream Duration/ZERO)))))))) + +(deftest intentional-close-discards-the-buffer-and-reports-closed + (with-open [^AutoCloseable stream (test-stream :bytes 2)] + (offer! stream "%1" "A") + (control/close! stream) + (let [outcome (:stream/terminal (control/next! stream Duration/ZERO))] + (is (= :closed (:reason outcome))) + (is (not (contains? outcome :cause)))))) + +(deftest competing-readers-deliver-each-event-once + (with-open [^AutoCloseable stream (test-stream :bytes 16)] + (doseq [octet (range 16)] (offer! stream "%1" (str (char (+ 65 octet))))) + (end! stream (java.io.IOException. "done")) + (let [ready (CountDownLatch. 2) + release (CountDownLatch. 1) + finished (CountDownLatch. 2) + results (atom []) + read-events (fn [] + (.countDown ready) + (.await release) + (try + (loop [] + (let [result (control/next! stream)] + (swap! results conj result) + (when (= :event (:stream/status result)) (recur)))) + (finally (.countDown finished)))) + workers (mapv (fn [_] (.start (Thread/ofVirtual) ^Runnable read-events)) (range 2))] + (try + (is (.await ready 900 TimeUnit/MILLISECONDS)) + (.countDown release) + (is (.await finished 900 TimeUnit/MILLISECONDS)) + (let [events (sort-by :event/sequence (filter #(= :event (:stream/status %)) @results)) + outcomes (map :stream/terminal (filter #(= :terminal (:stream/status %)) @results))] + (is (= (range 1 17) (map :event/sequence events))) + (is (= (range 65 81) (map #(first (:output/bytes %)) events))) + (is (= 2 (count outcomes))) + (is (every? #(identical? (control/terminal stream) %) outcomes))) + (finally + (.countDown release) + (control/close! stream) + (doseq [^Thread worker workers] (.join worker 900))))))) diff --git a/libtmux-clojure/src/test/clojure/libtmux/core_test.clj b/libtmux-clojure/src/test/clojure/libtmux/core_test.clj new file mode 100644 index 000000000..9209c41c0 --- /dev/null +++ b/libtmux-clojure/src/test/clojure/libtmux/core_test.clj @@ -0,0 +1,156 @@ +(ns libtmux.core-test + (:require [clojure.test :refer [deftest is]] + [libtmux.core :as tmux]) + (:import [io.github.libtmux Server ServerConfig] + [io.github.libtmux.transport TmuxTransport CommandRequest CommandResult DispatchOutcome] + [io.github.libtmux.exception DispatchException DispatchException$Failed])) + +(defn- version-probe? + "The hook-free admission probe: run-shell -C '#{version}'." + [^CommandRequest request] + (let [commands (.commands request)] + (and (= 1 (count commands)) + (let [argv (vec (first commands))] + (and (= 3 (count argv)) (= ["run-shell" "-C"] (subvec argv 0 2)) + (.startsWith ^String (argv 2) "libtmux-version-")))))) + +(defn- probe-reply [^CommandRequest request] + (let [^String format (nth (first (.commands request)) 2)] + (CommandResult. 1 [] [(str "unknown command: " + (subs format 0 (- (count format) (count "#{version}"))) + "3.7c")]))) + +(defn- ^Server fake-server + "A server over a fake transport. Admission is answered, so `dispatch` sees only the + requests a call itself makes." + [dispatch] + (Server/using (.build (ServerConfig/builder)) + (reify TmuxTransport + (execute [_ request] + (if (version-probe? request) (probe-reply request) (dispatch request))) + (close [_])))) + +(defn- failure [f] + (try (f) nil (catch clojure.lang.ExceptionInfo e e))) + +(deftest validation-rejects-before-dispatch + (let [calls (atom 0)] + (with-open [server (fake-server (fn [_] (swap! calls inc) (CommandResult. 0 [] [])))] + (doseq [options [{:unknown true} {:command "echo nope"} {:name 1} {:environment {:a "b"}}]] + (let [error (failure #(tmux/new-session! server options))] + (is (= :tmux/validation (:tmux/error (ex-data error)))) + (is (= :not-dispatched (:tmux/dispatch (ex-data error)))))) + (is (= :tmux/validation + (:tmux/error (ex-data (failure #(tmux/snapshot! {:tmux/ref server})))))) + (is (zero? @calls))) + (is (= :tmux/validation (:tmux/error (ex-data (failure #(tmux/open! {})))))) + (is (= :tmux/validation (:tmux/error (ex-data (failure #(tmux/kill! {:pane/id "%0"})))))))) + +(deftest stm-and-execution-hook-precede-dispatch + (let [calls (atom 0)] + (with-open [server (fake-server (fn [_] (swap! calls inc) (CommandResult. 0 [] [])))] + (is (thrown? IllegalStateException (dosync (tmux/raw! server ["list-sessions"])))) + (binding [tmux/*execution-check* (fn [metadata] + (is (= {:operation :raw :kind :mutation} metadata)) + (throw (ex-info "expired" {:expired true})))] + (is (:expired (ex-data (failure #(tmux/raw! server ["list-sessions"])))))) + (is (zero? @calls))))) + +(deftest original-cause-and-conservative-dispatch-survive + (let [cause (DispatchException$Failed. "sensitive payload" DispatchOutcome/NOT_DISPATCHED nil)] + (with-open [server (fake-server (fn [_] (throw cause)))] + (let [error (failure #(tmux/raw! server ["set-buffer" "private"]))] + (is (identical? cause (.getCause ^Throwable error))) + (is (= {:tmux/error :tmux/transport :tmux/operation :raw + :tmux/phase :unknown :tmux/dispatch :not-dispatched :tmux/effects :none} + (tmux/error-summary error))) + (is (not (.contains (pr-str (tmux/error-summary error)) "sensitive"))))))) + +(deftest programmer-errors-and-interruption-are-not-reclassified + (doseq [cause [(IllegalArgumentException. "caller bug") + (InterruptedException. "cancelled")]] + (with-open [server (fake-server (fn [_] (throw cause)))] + (let [caught (try (tmux/raw! server ["list-sessions"]) + (catch Exception e e))] + (is (identical? cause caught)))))) + + +(deftest typed-single-effect-preserves-transport-certainty + (doseq [[outcome dispatch effects] [[DispatchOutcome/NOT_DISPATCHED :not-dispatched :none] + [DispatchOutcome/UNKNOWN :unknown :possible] + [DispatchOutcome/COMPLETE :complete :possible]]] + (let [cause (DispatchException$Failed. "transport detail" outcome nil)] + (with-open [server (fake-server (fn [_] (throw cause)))] + (let [error (failure #(tmux/set-buffer! server "buffer" "contents"))] + (is (identical? cause (.getCause ^Throwable error))) + (is (= dispatch (:tmux/dispatch (ex-data error)))) + (is (= effects (:tmux/effects (ex-data error))))))))) + +(defn- run-group [kind server operations] + ((ns-resolve 'libtmux.core kind) server operations)) + +(defn- marked-reply + "Answers a group the way tmux does. Admission puts a version fence ahead of the caller's + commands, which prints nothing; each caller command is then followed by its marker." + [request exit] + (let [commands (rest (.commands ^io.github.libtmux.transport.CommandRequest request))] + (CommandResult. exit + (vec (mapcat (fn [[index marker]] [(str "value-" index) (last marker)]) + (map-indexed vector (take-nth 2 (rest commands))))) + (if (zero? exit) [] ["late failure"])))) + +(deftest groups-use-fresh-builders-and-positioned-output + (doseq [kind ['batch! 'chain!]] + (let [sizes (atom [])] + (with-open [server (fake-server (fn [request] + (swap! sizes conj (count (.commands ^io.github.libtmux.transport.CommandRequest request))) + (marked-reply request 0)))] + (dotimes [_ 2] + (let [results (run-group kind server [["display-message" "-p" "one"] + ["display-message" "-p" "two"]])] + (is (= [0 1] (mapv :operation/index results))) + (is (= [:complete :complete] (mapv :operation/outcome results))) + (is (= [["value-0"] ["value-1"]] (mapv :operation/stdout results))))) + (is (= [5 5] @sizes) "one fence, then each command and its marker"))))) + +(deftest groups-preserve-unknown-and-reject-invalid-input + (doseq [kind ['batch! 'chain!]] + (let [calls (atom 0)] + (with-open [server (fake-server (fn [request] (swap! calls inc) (marked-reply request 1)))] + (let [results (run-group kind server [["display-message" "-p" "one"] + ["display-message" "-p" "two"]])] + (is (= [:complete :unknown] (mapv :operation/outcome results)))) + (doseq [operations ['(["list-sessions"]) [["list-sessions"] []] [[1]]]] + (is (= :tmux/validation (:tmux/error (ex-data (failure #(run-group kind server operations))))))) + (is (= 1 @calls)) + (is (thrown? IllegalStateException (dosync (run-group kind server [["list-sessions"]])))) + (is (= 1 @calls)))))) + +(deftest group-transport-errors-preserve-all-unresolved-positions + (doseq [kind ['batch! 'chain!] + [outcome dispatch result-outcome] [[DispatchOutcome/NOT_DISPATCHED :not-dispatched :skipped] + [DispatchOutcome/UNKNOWN :unknown :unknown]]] + (let [cause (DispatchException$Failed. "transport failed" outcome nil)] + (with-open [server (fake-server (fn [_] (throw cause)))] + (let [error (failure #(run-group kind server [["set-buffer" "a"] ["set-buffer" "b"]]))] + (is (identical? cause (.getCause ^Throwable error))) + (is (= dispatch (:tmux/dispatch (ex-data error)))) + (is (= [result-outcome result-outcome] + (mapv :operation/outcome (:tmux/results (ex-data error)))))))))) + +(deftest blocking-groups-require-explicit-wait-classification + (let [calls (atom 0)] + (with-open [server (fake-server (fn [request] (swap! calls inc) (marked-reply request 0)))] + (doseq [kind ['batch! 'chain!]] + (is (= :tmux/validation + (:tmux/error (ex-data (failure #(run-group kind server [["wait-for" "blocked"]])))))) + (is (= :tmux/validation + (:tmux/error (ex-data (failure #(run-group kind server [["wait-for" "-L" "lock"]]))))))) + (binding [tmux/*execution-check* (fn [metadata] + (is (= :wait (:kind metadata))) + (throw (ex-info "classified wait" {:classified true})))] + (doseq [kind ['batch! 'chain!]] + (is (:classified + (ex-data (failure #((ns-resolve 'libtmux.core kind) + server [["wait-for" "blocked"]] {:kind :wait}))))))) + (is (zero? @calls))))) diff --git a/libtmux-clojure/src/test/clojure/libtmux/data_test.clj b/libtmux-clojure/src/test/clojure/libtmux/data_test.clj new file mode 100644 index 000000000..7c1fdf6ff --- /dev/null +++ b/libtmux-clojure/src/test/clojure/libtmux/data_test.clj @@ -0,0 +1,73 @@ +(ns libtmux.data-test + (:require [clojure.edn :as edn] + [clojure.test :refer [deftest is testing]] + [libtmux.data :as data]) + (:import [io.github.libtmux.query FilterExpr] + [java.time Instant])) + +(deftest cardinality-is-strict-and-bounded + (is (nil? (data/one-or-none []))) + (is (= false (data/exactly-one [false]))) + (is (nil? (data/exactly-one [nil]))) + (is (= :item (data/one-or-none [:item]))) + (doseq [[f values reason] [[data/exactly-one [] :none] + [data/exactly-one [1 2] :multiple] + [data/one-or-none [1 2] :multiple]]] + (let [error (try (f values) (catch clojure.lang.ExceptionInfo e e))] + (is (= :tmux/cardinality (:tmux/error (ex-data error)))) + (is (= reason (:cardinality/reason (ex-data error)))))) + (let [observed (atom 0) + items ((fn step [n] + (lazy-seq + (swap! observed inc) + (cons n (step (inc n))))) 0)] + (is (thrown? clojure.lang.ExceptionInfo (data/one-or-none items))) + (is (= 2 @observed)))) + +(deftest projection-is-edn-and-drops-references-recursively + (let [at (Instant/parse "2026-09-20T00:00:00Z") + value {:tmux/ref (Object.) + :tmux/captured-at at + :pane/floating? nil + :pane/active? false + :pane/current-path "raw\\xFF" + :children [{:tmux/ref (Object.) :pane/id "%0"}] + :tags #{:a :b}} + projected (data/data value)] + (is (= projected (edn/read-string (pr-str projected)))) + (is (= "2026-09-20T00:00:00Z" (:tmux/captured-at projected))) + (is (false? (:pane/active? projected))) + (is (contains? projected :pane/floating?)) + (is (not (contains? projected :pane/dead?))) + (is (= "raw\\xFF" (:pane/current-path projected))) + (is (= [{:pane/id "%0"}] (:children projected))) + (is (not (contains? projected :tmux/ref)))) + (is (thrown? clojure.lang.ExceptionInfo (data/data (Object.))))) + +(deftest physical-and-occurrence-identity-differ + (let [entity {:tmux/realm "local" :tmux/server "opaque" :tmux/pid 12 + :tmux/kind :pane :tmux/id "%0"} + first-link {:session/id "$0" :window/id "@0" :window/index 0} + second-link (assoc first-link :window/index 1) + a {:tmux/identity entity :tmux/link first-link} + b {:tmux/identity entity :tmux/link second-link}] + (is (data/same-entity? a b)) + (is (not (data/same-link? a b))) + (is (data/same-link? a (assoc a :pane/title "renamed"))) + (is (not (data/same-entity? a (assoc-in b [:tmux/identity :tmux/pid] 13)))) + (is (not (data/same-entity? a (assoc-in b [:tmux/identity :tmux/server] "other")))) + (is (thrown? clojure.lang.ExceptionInfo (data/entity-key {}))) + (is (thrown? clojure.lang.ExceptionInfo (data/link-key {:tmux/identity entity}))))) + +(deftest java-expression-adapters-follow-native-filter-arities + (let [expression (FilterExpr/and []) + a {:tmux/ref (Object.) :label :first} + b {:tmux/ref (Object.) :label :second} + entries [a b a] + predicate (data/predicate expression)] + (is (ifn? predicate)) + (is (true? (predicate a))) + (is (thrown? clojure.lang.ExceptionInfo (predicate (data/data a)))) + (is (= entries (into [] (data/matching expression) entries))) + (is (= entries (vec (data/matching expression entries)))) + (is (= [] (into [] (data/matching (FilterExpr/or [])) entries))))) diff --git a/libtmux-clojure/src/test/clojure/libtmux/internal/package_test.clj b/libtmux-clojure/src/test/clojure/libtmux/internal/package_test.clj new file mode 100644 index 000000000..471ba4224 --- /dev/null +++ b/libtmux-clojure/src/test/clojure/libtmux/internal/package_test.clj @@ -0,0 +1,55 @@ +(ns libtmux.internal.package-test + (:require [clojure.java.io :as io] + [clojure.string :as str] + [clojure.test :as test :refer [deftest is]] + [libtmux.internal.test-runner :as runner]) + (:import [clojure.lang Compiler DynamicClassLoader] + [java.io File StringWriter] + [java.nio.file Files])) + +(deftest core-does-not-pull-the-optional-adapter + (is (nil? (io/resource "clojure/core/async.clj")))) + +(defn- with-test-sources [body] + (let [directory (.toFile (Files/createTempDirectory "clj-runner-" + (make-array java.nio.file.attribute.FileAttribute 0))) + property "libtmux.clojure.test-namespaces" + previous (System/getProperty property)] + (with-open [loader (DynamicClassLoader. (clojure.lang.RT/baseLoader))] + (.addURL loader (.toURL (.toURI directory))) + (try + (doseq [[namespace test-form] + {"runner-probe.visible-test" "(deftest pass (is true))" + "runner-probe.omitted-test" "(deftest fail (is false))"}] + (let [^File source (io/file directory + (str (str/replace (str/replace namespace "." "/") "-" "_") ".clj"))] + (.mkdirs (.getParentFile source)) + (spit source (str "(ns " namespace " (:require [clojure.test :refer [deftest is]]))\n" + test-form)))) + (System/setProperty property "runner-probe.visible-test") + (with-bindings {Compiler/LOADER loader} + (body directory)) + (finally + (if previous (System/setProperty property previous) (System/clearProperty property)) + (doseq [^File file (reverse (file-seq directory))] + (Files/deleteIfExists (.toPath file)))))))) + +(defn- runner-failure [& arguments] + (binding [test/*test-out* (StringWriter.)] + (try + (apply runner/-main arguments) + nil + (catch clojure.lang.ExceptionInfo failure failure)))) + +(deftest source-discovery-does-not-skip-an-omitted-failing-namespace + (with-test-sources + (fn [directory] + (is (= {:fail 1 :error 0} + (select-keys (ex-data (runner-failure "test" (str directory))) [:fail :error])))))) + +(deftest source-discovery-rejects-an-empty-root + (with-test-sources + (fn [directory] + (let [^File empty-root (io/file directory "empty")] + (.mkdir empty-root) + (is (some? (runner-failure "test" (str empty-root)))))))) diff --git a/libtmux-junit5/src/main/java/io/github/libtmux/junit5/FakeTmux.java b/libtmux-junit5/src/main/java/io/github/libtmux/junit5/FakeTmux.java index e782da0ea..d6cb8b433 100644 --- a/libtmux-junit5/src/main/java/io/github/libtmux/junit5/FakeTmux.java +++ b/libtmux-junit5/src/main/java/io/github/libtmux/junit5/FakeTmux.java @@ -9,14 +9,21 @@ import io.github.libtmux.transport.CommandResult; import io.github.libtmux.transport.ControlCarrier; import io.github.libtmux.transport.TmuxTransport; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.UUID; import java.util.function.Function; import org.jspecify.annotations.Nullable; @@ -53,6 +60,9 @@ public final class FakeTmux implements TmuxTransport { /** The release it claims to be, one every supported feature is available on. */ private static final String VERSION = "3.7c"; + // Pins retain model identities; this fake has no host socket. + private final Map controlPins = new HashMap<>(); + private static final String LAYOUT = "c3a4,80x24,0,0,1"; /** @@ -247,7 +257,116 @@ public synchronized CommandResult execute(CommandRequest request) { */ @Override public Optional controlCarrier() { - return Optional.of(command -> attach()); + return Optional.of(new ControlCarrier() { + @Override + public Process start(List command) throws IOException { + synchronized (FakeTmux.this) { + int selector = command.indexOf("-S"); + if (selector >= 0 + && !Objects.equals(controlPins.get(command.get(selector + 1)), pid + ":" + started)) { + throw new IOException("the fake pinned incarnation has ended"); + } + if (command.size() >= 3 && isVersionProbe(command.subList(command.size() - 3, command.size()))) { + return VersionProbe.refusing(refusal(command.getLast())); + } + if (command.size() >= 3 + && command.get(command.size() - 3).equals("run-shell") + && command.get(command.size() - 2).equals("-C") + && command.getLast().startsWith("libtmux-identity-")) { + String format = command.getLast(); + String marker = format.substring(0, format.indexOf("#{version}")); + return VersionProbe.refusing("unknown command: " + marker + VERSION + "," + pid + "," + started + + ",/tmp/libtmux-java-fake/" + pid); + } + if (command.size() >= 4 + && command.subList(command.size() - 4, command.size() - 1) + .equals(List.of("-C", "attach-session", "-t"))) return attach(); + throw new IOException("the fake carrier supports identity probes and control attachment"); + } + } + + @Override + public PinnedSocket pinSocket(String socketPath, Duration timeout) throws IOException { + synchronized (FakeTmux.this) { + if (!socketPath.equals("/tmp/libtmux-java-fake/" + pid)) + throw new IOException("the fake socket incarnation has ended"); + String alias = socketPath + "/control-" + UUID.randomUUID(); + controlPins.put(alias, pid + ":" + started); + return new PinnedSocket() { + @Override + public String socketPath() { + return alias; + } + + @Override + public void close() { + synchronized (FakeTmux.this) { + controlPins.remove(alias); + } + } + }; + } + } + }); + } + + /** + * The version probe: {@code run-shell -C '#{version}'}, which a daemon answers by + * failing to parse the expansion and spelling the version in the error. + */ + private static boolean isVersionProbe(List command) { + return command.size() == 3 + && command.get(0).equals("run-shell") + && command.get(1).equals("-C") + && command.get(2).endsWith("#{version}"); + } + + private static String refusal(String format) { + return "unknown command: " + format.substring(0, format.length() - "#{version}".length()) + VERSION; + } + + private static final class VersionProbe extends Process { + private final InputStream output; + private final InputStream error; + private final int exit; + + private VersionProbe(int exit, String stdout, String stderr) { + this.exit = exit; + output = new ByteArrayInputStream((stdout.isEmpty() ? "" : stdout + "\n").getBytes(StandardCharsets.UTF_8)); + error = new ByteArrayInputStream((stderr.isEmpty() ? "" : stderr + "\n").getBytes(StandardCharsets.UTF_8)); + } + + static VersionProbe refusing(String stderr) { + return new VersionProbe(1, "", stderr); + } + + @Override + public OutputStream getOutputStream() { + return OutputStream.nullOutputStream(); + } + + @Override + public InputStream getInputStream() { + return output; + } + + @Override + public InputStream getErrorStream() { + return error; + } + + @Override + public int waitFor() { + return exit; + } + + @Override + public int exitValue() { + return exit; + } + + @Override + public void destroy() {} } private synchronized Process attach() { @@ -317,6 +436,10 @@ private CommandResult one(List argv) { if (verb.equals("-V")) { return ok("tmux " + VERSION); } + if (isVersionProbe(argv)) { + sent.add(List.copyOf(argv)); + return new CommandResult(1, List.of(), List.of(refusal(argv.getLast()))); + } if (!COMMANDS.contains(verb)) { return new CommandResult(1, List.of(), List.of("unknown command: " + verb)); } diff --git a/libtmux-junit5/src/test/java/io/github/libtmux/junit5/NamedServerFixtureTest.java b/libtmux-junit5/src/test/java/io/github/libtmux/junit5/NamedServerFixtureTest.java index 681193c88..9d36fe40f 100644 --- a/libtmux-junit5/src/test/java/io/github/libtmux/junit5/NamedServerFixtureTest.java +++ b/libtmux-junit5/src/test/java/io/github/libtmux/junit5/NamedServerFixtureTest.java @@ -10,6 +10,7 @@ import io.github.libtmux.ServerEndpoint; import io.github.libtmux.transport.CommandRequest; import io.github.libtmux.transport.CommandResult; +import io.github.libtmux.transport.ProcessTransport; import io.github.libtmux.transport.TmuxTransport; import java.io.IOException; import java.nio.file.Files; @@ -195,11 +196,17 @@ void replacementBetweenIdentityReadAndKillSurvives(@TempDir Path directory) thro @Override public CommandResult execute(CommandRequest request) { requests.add(request); - List command = request.commands().getFirst(); + List command = request.commands().getLast(); Server destination = command.getFirst().equals("display-message") && identityReads[0]++ < 2 ? original : replacement; - return destination.cmd(command); + try (ProcessTransport processes = new ProcessTransport(1)) { + List endpoint = + new ArrayList<>(destination.config().endpointCommand()); + endpoint.add(1, "-N"); + return processes.execute(new CommandRequest( + endpoint, request.commands(), request.timeout(), request.input())); + } } @Override @@ -213,7 +220,7 @@ public void close() {} assertTrue(String.valueOf(refused.getMessage()).contains("replacement")); assertTrue(replacement.hasSession("race-replacement")); assertEquals( - List.of("display-message", "if-shell"), + List.of("run-shell", "if-shell", "if-shell"), requests.stream() .map(request -> request.commands().getFirst().getFirst()) @@ -237,7 +244,10 @@ void failedAuthenticationNeverSendsAnEndpointCommand(@TempDir Path directory) th @Override public CommandResult execute(CommandRequest request) { requests.add(request); - if (request.commands().getFirst().getFirst().equals("display-message")) { + if (VersionProbes.is(request)) { + return VersionProbes.reply(request, "3.7c"); + } + if (request.commands().getLast().getFirst().equals("display-message")) { return new CommandResult(0, List.of("malformed"), List.of()); } return new CommandResult(0, List.of(), List.of()); @@ -258,10 +268,11 @@ public void close() {} assertTrue(String.valueOf(refused.getMessage()).contains("malformed identity row")); } assertEquals("keep", Files.readString(sentinel)); - assertEquals(1, requests.size()); + assertEquals(2, requests.size()); + assertTrue(VersionProbes.is(requests.getFirst())); assertEquals( List.of("display-message", "-p", "#{pid}\t#{socket_path}"), - requests.get(0).commands().getFirst()); + requests.getLast().commands().getLast()); assertTrue(requests.stream().allMatch(request -> request.endpoint().contains(sentinel.toString()))); } diff --git a/libtmux-junit5/src/test/java/io/github/libtmux/junit5/VersionProbes.java b/libtmux-junit5/src/test/java/io/github/libtmux/junit5/VersionProbes.java new file mode 100644 index 000000000..7d91ae9f3 --- /dev/null +++ b/libtmux-junit5/src/test/java/io/github/libtmux/junit5/VersionProbes.java @@ -0,0 +1,39 @@ +package io.github.libtmux.junit5; + +import io.github.libtmux.transport.CommandRequest; +import io.github.libtmux.transport.CommandResult; +import java.util.List; + +/** + * The version probe as a test double sees it, and tmux's answer to it. + * + *

The probe is {@code run-shell -C '#{version}'}: the daemon expands the format, fails to + * parse the result as a command, and spells the version in {@code unknown command: }. + */ +final class VersionProbes { + + private static final String MARKER = "libtmux-version-"; + private static final String FORMAT = "#{version}"; + + private VersionProbes() {} + + /** Whether this request is the version probe and nothing else. */ + static boolean is(CommandRequest request) { + return request.commands().size() == 1 && is(request.commands().getFirst()); + } + + static boolean is(List command) { + return command.size() == 3 + && command.get(0).equals("run-shell") + && command.get(1).equals("-C") + && command.get(2).startsWith(MARKER) + && command.get(2).endsWith(FORMAT); + } + + /** The reply of a daemon that reports this version. */ + static CommandResult reply(CommandRequest request, String version) { + String argument = request.commands().getFirst().get(2); + String marker = argument.substring(0, argument.length() - FORMAT.length()); + return new CommandResult(1, List.of(), List.of("unknown command: " + marker + version)); + } +} diff --git a/libtmux-kotlin/README.md b/libtmux-kotlin/README.md index 6c53853f3..9f882961e 100644 --- a/libtmux-kotlin/README.md +++ b/libtmux-kotlin/README.md @@ -170,6 +170,10 @@ compiler rejects it. ## A subscription as a cold `Flow` +Managed control requires tmux 3.7+ and a carrier-owned pinned socket. +Ordinary suspend operations retain the core 3.3a minimum. + + ```kotlin // Given: config: ServerConfig import io.github.libtmux.control.Delivery @@ -200,6 +204,7 @@ ahead of the events that survived a full buffer; nothing is silently dropped. ## The live server as a `StateFlow` + ```kotlin // Given: config: ServerConfig import kotlinx.coroutines.flow.first diff --git a/libtmux-kotlin/api/libtmux-kotlin.api b/libtmux-kotlin/api/libtmux-kotlin.api index 3816ce482..1096cba1e 100644 --- a/libtmux-kotlin/api/libtmux-kotlin.api +++ b/libtmux-kotlin/api/libtmux-kotlin.api @@ -392,6 +392,7 @@ public final class io/github/libtmux/kotlin/ServerOperationsKt { public static final fun panes (Lio/github/libtmux/kotlin/Server;Lio/github/libtmux/query/FilterExpr;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun panes (Lio/github/libtmux/kotlin/Server;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun requireAlive (Lio/github/libtmux/kotlin/Server;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun requireCompatibleTmux (Lio/github/libtmux/kotlin/Server;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun run (Lio/github/libtmux/kotlin/Server;Ljava/util/List;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun session (Lio/github/libtmux/kotlin/Server;Lio/github/libtmux/SessionId;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun session (Lio/github/libtmux/kotlin/Server;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; diff --git a/libtmux-kotlin/build.gradle.kts b/libtmux-kotlin/build.gradle.kts index f6a06b443..52bfbe1b6 100644 --- a/libtmux-kotlin/build.gradle.kts +++ b/libtmux-kotlin/build.gradle.kts @@ -270,6 +270,20 @@ val documentedKotlin = val unknown = given - fixtures.keys require(unknown.isEmpty()) { "$where line $line: Given names $unknown; a snippet may assume ${fixtures.keys}" } val declared = given.joinToString("") { " val $it = ${fixtures.getValue(it)}\n" } + val controlRefusal = if (directive.startsWith("control:")) { + """ + | val version = javaServer.version() + | if (!io.github.libtmux.internal.ControlAcquisition.supports(version)) { + | assertFailsWith { + | runBlocking { + |${body.joinToString("\n") { " $it" }} + | } + | } + | assertEquals(0, javaServer.clients().size) + | return + | } + """.trimMargin() + } else "" val file = generated.get().asFile.resolve("io/github/libtmux/docs/kotlin/s$found/Snippet.kt") file.parentFile.mkdirs() file.writeText( @@ -290,6 +304,7 @@ val documentedKotlin = appendLine("import io.github.libtmux.junit5.TmuxExtension") appendLine("import io.github.libtmux.junit5.TmuxSocketPath") appendLine("import kotlin.test.assertEquals") + appendLine("import kotlin.test.assertFailsWith") appendLine("import kotlinx.coroutines.runBlocking") appendLine("import org.junit.jupiter.api.Test") appendLine("import org.junit.jupiter.api.extension.ExtendWith") @@ -301,6 +316,7 @@ val documentedKotlin = | @Test | fun `$name`(javaServer: io.github.libtmux.Server, socketPath: TmuxSocketPath) { |$declared + |$controlRefusal | // runBlocking is inline, so a snippet's own declarations shadow the | // ones above rather than colliding with them, and a bare return still | // leaves the test. Every call in this module's own surface is diff --git a/libtmux-kotlin/src/test/kotlin/io/github/libtmux/kotlin/FakeControlClients.kt b/libtmux-kotlin/src/test/kotlin/io/github/libtmux/kotlin/FakeControlClients.kt index a147da950..6e27ba86e 100644 --- a/libtmux-kotlin/src/test/kotlin/io/github/libtmux/kotlin/FakeControlClients.kt +++ b/libtmux-kotlin/src/test/kotlin/io/github/libtmux/kotlin/FakeControlClients.kt @@ -3,6 +3,9 @@ package io.github.libtmux.kotlin import io.github.libtmux.ServerConfig import io.github.libtmux.SessionId import io.github.libtmux.control.ControlClient +import io.github.libtmux.transport.ControlCarrier +import java.time.Duration +import java.util.OptionalLong import java.nio.file.Files import java.nio.file.Path import java.nio.file.attribute.PosixFilePermissions @@ -32,7 +35,16 @@ internal object FakeControlClients { ) Files.setPosixFilePermissions(fake, PosixFilePermissions.fromString("rwx------")) val config = ServerConfig.builder().binary(fake.toString()).build() - return ControlClient.attachUnfenced(config, SessionId("\$0")) + val carrier = object : ControlCarrier { + override fun start(command: List): Process = ProcessBuilder(command).start() + + override fun pinSocket(socketPath: String, remaining: Duration): ControlCarrier.PinnedSocket = + object : ControlCarrier.PinnedSocket { + override fun socketPath(): String = "/fake-control-pin" + override fun close() = Unit + } + } + return ControlClient.attach(carrier, config, SessionId("\$0"), 4242, OptionalLong.of(10), "3.7c", Duration.ofSeconds(30)) } /** @@ -42,13 +54,32 @@ internal object FakeControlClients { * this client's command, then the marker's block. */ private val PRELUDE = - "printf '%%begin 100 1 0\\n%%end 100 1 0\\n'\n" + - "read_request() { IFS= read -r request && IFS= read -r marker; }\n" + - "answer() {\n" + - " printf '%%begin 1 1 1\\n'\n" + - " for line in \"\$@\"; do printf '%s\\n' \"\$line\"; done\n" + - " printf '%%end 1 1 1\\n'\n" + - " token=\${marker##*\"' '\"}\n" + - " printf '%%begin 1 2 1\\n%s\\n%%end 1 2 1\\n' \"\${token%\"'\"}\"\n" + - "}\n" + """ + for argument in "${'$'}@"; do + case "${'$'}argument" in + libtmux-version-*'#{version}') + printf 'unknown command: %s3.7c\n' "${'$'}{argument%'#{version}'}" >&2 + exit 1;; + esac + case "${'$'}argument" in + libtmux-identity-*) + printf 'unknown command: %s3.7c,4242,10,/fake-control\n' "${'$'}{argument%%'#{version}'*}" >&2 + exit 1;; + esac + done + printf '%%begin 100 1 0\n%%end 100 1 0\n' + read_request() { + while IFS= read -r request && IFS= read -r marker; do + return 0 + done + return 1 + } + answer() { + printf '%%begin 1 1 1\n' + for line in "${'$'}@"; do printf '%s\n' "${'$'}line"; done + printf '%%end 1 1 1\n' + token=${'$'}{marker##*"' '"} + printf '%%begin 1 2 1\n%s\n%%end 1 2 1\n' "${'$'}{token%"'"}" + } + """.trimIndent() + "\n" } diff --git a/libtmux-kotlin/src/test/kotlin/io/github/libtmux/kotlin/LiveStateTest.kt b/libtmux-kotlin/src/test/kotlin/io/github/libtmux/kotlin/LiveStateTest.kt index 945ab3b35..c2791b259 100644 --- a/libtmux-kotlin/src/test/kotlin/io/github/libtmux/kotlin/LiveStateTest.kt +++ b/libtmux-kotlin/src/test/kotlin/io/github/libtmux/kotlin/LiveStateTest.kt @@ -9,6 +9,7 @@ import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeout +import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import io.github.libtmux.Server as JavaServer @@ -19,6 +20,7 @@ import io.github.libtmux.Server as JavaServer * `coroutineScope { liveState(...) }` never returns — structured concurrency waits for every child, * the pump included, and nothing inside it ever asks the pump to stop. */ +@Tag("control") @ExtendWith(TmuxExtension::class) class LiveStateTest { diff --git a/libtmux-kotlin/src/test/kotlin/io/github/libtmux/kotlin/ReadmeExamplesTest.kt b/libtmux-kotlin/src/test/kotlin/io/github/libtmux/kotlin/ReadmeExamplesTest.kt index fad6c748f..b1fb6029e 100644 --- a/libtmux-kotlin/src/test/kotlin/io/github/libtmux/kotlin/ReadmeExamplesTest.kt +++ b/libtmux-kotlin/src/test/kotlin/io/github/libtmux/kotlin/ReadmeExamplesTest.kt @@ -20,6 +20,7 @@ import kotlin.test.assertTrue import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import io.github.libtmux.Server as JavaServer @@ -130,6 +131,7 @@ class ReadmeExamplesTest { } @Test + @Tag("control") fun `a subscription as a cold Flow`(javaServer: JavaServer) = runBlocking { withServer(javaServer.config()) { server -> val session = server.newSession("flow-demo") @@ -147,6 +149,7 @@ class ReadmeExamplesTest { } @Test + @Tag("control") fun `the live server as a StateFlow`(javaServer: JavaServer) = runBlocking { withServer(javaServer.config()) { server -> val session = server.newSession("live-demo") diff --git a/libtmux-kotlin/src/test/kotlin/io/github/libtmux/kotlin/WaitsTest.kt b/libtmux-kotlin/src/test/kotlin/io/github/libtmux/kotlin/WaitsTest.kt index 5f138ced7..a2663ded7 100644 --- a/libtmux-kotlin/src/test/kotlin/io/github/libtmux/kotlin/WaitsTest.kt +++ b/libtmux-kotlin/src/test/kotlin/io/github/libtmux/kotlin/WaitsTest.kt @@ -25,6 +25,7 @@ import kotlinx.coroutines.async import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeout +import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import io.github.libtmux.Server as JavaServer @@ -130,6 +131,7 @@ class WaitsTest { } @Test + @Tag("control") fun `control attaches a working client against real tmux`(javaServer: JavaServer) = runBlocking { val server = Server.open(javaServer.config()) val session = server.sessions().first() @@ -155,6 +157,7 @@ class WaitsTest { * wait is stalled by construction rather than by timing. */ @Test + @Tag("control") fun `cancelling control ends promptly and kills the stalled process`() { val stall = StalledControlFixture.start() try { @@ -163,7 +166,7 @@ class WaitsTest { val session = Session(stall.javaSession, server) val attaching = async(Dispatchers.IO) { server.control(session, 30.seconds) } - val pid = awaitControlClientPid(stall.socket, 5.seconds) + val pid = awaitControlClientPid(stall.controlCommand, 5.seconds) assertNotNull(pid, "the wrapped control client never started") assertFalse(attaching.isCompleted, "attach finished before it could be caught stalled") @@ -196,7 +199,7 @@ private class StalledControlFixture private constructor( val javaServer: JavaServer, val javaSession: JavaSession, ) { - val socket: String = root.resolve("s").toString() + val controlCommand: String = root.resolve("wrap.sh").toString() fun close() { try { @@ -219,7 +222,7 @@ private class StalledControlFixture private constructor( """ #!/bin/sh for arg; do - if [ "${'$'}arg" = "-C" ]; then + if [ "${'$'}arg" = "attach-session" ]; then read line < "$gate" break fi @@ -255,21 +258,21 @@ private fun waiterPresent(socket: String): Boolean = } // A direct child of this JVM, not a system-wide scan of every process on the machine. -private fun controlClientPid(socket: String): Long? = +private fun controlClientPid(command: String): Long? = ProcessHandle.current() .children() .filter { process -> val argv = process.info().arguments().orElse(emptyArray()) - "-C" in argv && socket in argv + "-C" in argv && "attach-session" in argv && command in argv } .findFirst() .map(ProcessHandle::pid) .orElse(null) -private suspend fun awaitControlClientPid(socket: String, timeout: Duration): Long? { +private suspend fun awaitControlClientPid(command: String, timeout: Duration): Long? { val end = System.nanoTime() + timeout.inWholeNanoseconds while (System.nanoTime() < end) { - val pid = controlClientPid(socket) + val pid = controlClientPid(command) if (pid != null) { return pid } diff --git a/libtmux-mcp/src/test/java/io/github/libtmux/mcp/CapabilityRegistryTest.java b/libtmux-mcp/src/test/java/io/github/libtmux/mcp/CapabilityRegistryTest.java index 1abc4701f..1822f10d8 100644 --- a/libtmux-mcp/src/test/java/io/github/libtmux/mcp/CapabilityRegistryTest.java +++ b/libtmux-mcp/src/test/java/io/github/libtmux/mcp/CapabilityRegistryTest.java @@ -780,7 +780,7 @@ void anAggregateIncludedAloneDispatchesRepeatedNestedCallsWithoutAdvertisingThem TmuxTransport absent = new TmuxTransport() { @Override public CommandResult execute(CommandRequest request) { - return new CommandResult(1, List.of(), List.of("absent")); + return new CommandResult(1, List.of(), List.of("no server running")); } @Override @@ -828,7 +828,7 @@ void readBatchUsesCamelCaseOnErrorForDispatchAndDisclosure() { TmuxTransport absent = new TmuxTransport() { @Override public CommandResult execute(CommandRequest request) { - return new CommandResult(1, List.of(), List.of("absent")); + return new CommandResult(1, List.of(), List.of("no server running")); } @Override @@ -870,6 +870,9 @@ void aggregateWireLimiterReportsTruncation() throws Exception { TmuxTransport oversizedEnvironment = new TmuxTransport() { @Override public CommandResult execute(CommandRequest request) { + if (VersionProbes.is(request)) { + return VersionProbes.reply(request, "3.6"); + } return new CommandResult(0, List.of("VALUE=" + "x".repeat(1_048_576)), List.of()); } @@ -912,8 +915,11 @@ void aggregateOverflowRollsBackOnlyTheRowThatCrossedTheCap() { TmuxTransport secondResultIsOversized = new TmuxTransport() { @Override public CommandResult execute(CommandRequest request) { + if (VersionProbes.is(request)) { + return VersionProbes.reply(request, "3.6"); + } return calls.getAndIncrement() == 0 - ? new CommandResult(1, List.of(), List.of("absent")) + ? new CommandResult(1, List.of(), List.of("no server running")) : new CommandResult(0, List.of("VALUE=" + "x".repeat(1_048_576)), List.of()); } @@ -1016,7 +1022,7 @@ void theOnlyResourceDisclosesTheEffectiveSurfaceAndCurrentSocketHonestly() { TmuxTransport absent = new TmuxTransport() { @Override public CommandResult execute(CommandRequest request) { - return new CommandResult(1, List.of(), List.of("absent")); + return new CommandResult(1, List.of(), List.of("no server running")); } @Override @@ -1084,6 +1090,9 @@ void capabilityReportFreezesOneSocketObservation() { TmuxTransport changing = new TmuxTransport() { @Override public CommandResult execute(CommandRequest request) { + if (VersionProbes.is(request)) { + return VersionProbes.reply(request, "3.6"); + } return probes.getAndIncrement() == 0 ? new CommandResult(0, List.of("123"), List.of()) : new CommandResult(1, List.of(), List.of("gone")); diff --git a/libtmux-mcp/src/test/java/io/github/libtmux/mcp/MainTest.java b/libtmux-mcp/src/test/java/io/github/libtmux/mcp/MainTest.java index f41aa04a9..8ae088664 100644 --- a/libtmux-mcp/src/test/java/io/github/libtmux/mcp/MainTest.java +++ b/libtmux-mcp/src/test/java/io/github/libtmux/mcp/MainTest.java @@ -278,7 +278,10 @@ void missingStartupMetadataFailsClosed() { TmuxTransport broken = new TmuxTransport() { @Override public CommandResult execute(CommandRequest request) { - List command = request.commands().getFirst(); + if (VersionProbes.is(request)) { + return VersionProbes.reply(request, "3.6"); + } + List command = request.commands().getLast(); return command.getFirst().equals("start-server") ? new CommandResult(0, List.of(), List.of()) : new CommandResult(1, List.of(), List.of("metadata unavailable")); @@ -301,6 +304,9 @@ void aReportedSocketPathThatNamesNoFileFailsClosed() { TmuxTransport escaping = new TmuxTransport() { @Override public CommandResult execute(CommandRequest request) { + if (VersionProbes.is(request)) { + return VersionProbes.reply(request, "3.6"); + } return new CommandResult(0, List.of("/tmp/libtmux-java-dev/escaped\\001/s"), List.of()); } @@ -319,6 +325,9 @@ void explicitSocketMetadataErrorsAreNotMisreportedAsAnAbsentServer() { TmuxTransport denied = new TmuxTransport() { @Override public CommandResult execute(CommandRequest request) { + if (VersionProbes.is(request)) { + return VersionProbes.reply(request, "3.6"); + } return new CommandResult(1, List.of(), List.of("permission denied")); } @@ -393,6 +402,9 @@ void startupRejectsControlCharactersReportedInAResolvedSocket() { TmuxTransport reporting = new TmuxTransport() { @Override public CommandResult execute(CommandRequest request) { + if (VersionProbes.is(request)) { + return VersionProbes.reply(request, "3.6"); + } return new CommandResult(0, List.of("/tmp/reported\nsocket"), List.of()); } @@ -411,7 +423,10 @@ private static SocketProfile profile(LaunchConfiguration launch, String marker) TmuxTransport transport = new TmuxTransport() { @Override public CommandResult execute(CommandRequest request) { - List command = request.commands().getFirst(); + if (VersionProbes.is(request)) { + return VersionProbes.reply(request, "3.6"); + } + List command = request.commands().getLast(); if (command.getFirst().equals("start-server")) { return new CommandResult(0, List.of(), List.of()); } diff --git a/libtmux-mcp/src/test/java/io/github/libtmux/mcp/PaneInputCohortTest.java b/libtmux-mcp/src/test/java/io/github/libtmux/mcp/PaneInputCohortTest.java index 2d5981100..2efb5d1ae 100644 --- a/libtmux-mcp/src/test/java/io/github/libtmux/mcp/PaneInputCohortTest.java +++ b/libtmux-mcp/src/test/java/io/github/libtmux/mcp/PaneInputCohortTest.java @@ -383,7 +383,7 @@ public void close() {} } } - assertEquals(1, requests.size()); + assertEquals(1, requests.size(), "one pane/client snapshot on a daemon already checked"); List> commands = requests.stream() .flatMap(request -> request.commands().stream()) .filter(command -> command.getFirst().startsWith("list-")) diff --git a/libtmux-mcp/src/test/java/io/github/libtmux/mcp/RunningCommandsTest.java b/libtmux-mcp/src/test/java/io/github/libtmux/mcp/RunningCommandsTest.java index 194a9b18a..937e533e6 100644 --- a/libtmux-mcp/src/test/java/io/github/libtmux/mcp/RunningCommandsTest.java +++ b/libtmux-mcp/src/test/java/io/github/libtmux/mcp/RunningCommandsTest.java @@ -347,7 +347,7 @@ void successfulRunUsesExactlyTwoPreflights(Server server) { assertEquals(1, sends.size()); assertTrue(preflights.get(0) < discoveries.getFirst()); assertTrue(discoveries.getFirst() < preflights.get(1)); - assertEquals(preflights.get(1) + 1, sends.getFirst(), "a tmux query followed the final preflight"); + assertEquals(preflights.get(1) + 1, sends.getFirst(), "nothing may follow the final preflight but the send"); assertEquals(1, directCommandCount(requests, "wait-for")); } @@ -590,6 +590,19 @@ void completionSignalWithoutAValidStatusRetainsThePane( } } + @Test + void completionCaptureKeepsSoftWrappedMarkers(Server server) throws Exception { + Pane pane = server.panes().getFirst(); + pane.window().resizeTo(new io.github.libtmux.Dimensions(20, 24)); + String marker = "lt" + "a".repeat(32) + "-e:00"; + pane.respawn("sh", "-c", "printf '%s' " + Shell.quote(marker) + "; exec cat"); + assertTrue(await(() -> "cat".equals(pane.expand("#{pane_current_command}")))); + + assertTrue( + capture(server, pane.id().value()).contains(marker), + capture(server, pane.id().value())); + } + @Test void cancelledRunRetainsItsPaneUntilTheStatusMarker(Server server, @TempDir Path temporary) throws Exception { String pane = server.panes().getFirst().id().value(); @@ -621,7 +634,9 @@ void uncertainCommandDeliveryStillRunsTheAcceptedCommand(Server server, @TempDir try (ProcessTransport processes = new ProcessTransport()) { TmuxTransport uncertain = borrowing(request -> { CommandResult result = processes.execute(request); - if (request.commands().get(0).stream().anyMatch(argument -> argument.contains("ch_lt"))) { + if (request.commands().stream() + .flatMap(List::stream) + .anyMatch(argument -> argument.contains("ch_lt"))) { throw new DispatchException.Failed( "simulated failure after delivery", DispatchOutcome.UNKNOWN, null); } @@ -967,7 +982,8 @@ private static void armReadonly(Pane pane, String nonce) { } private static Optional nonce(CommandRequest request) { - return request.commands().getFirst().stream() + return request.commands().stream() + .flatMap(List::stream) .filter(argument -> argument.contains("ch_lt")) .flatMap(argument -> NONCE.matcher(argument).results()) .map(MatchResult::group) @@ -975,7 +991,8 @@ private static Optional nonce(CommandRequest request) { } private static String capture(Server server, String paneId) { - return String.join("\n", server.cmd("capture-pane", "-p", "-t", paneId).stdout()); + return String.join( + "\n", server.cmd("capture-pane", "-p", "-J", "-t", paneId).stdout()); } private static void assertRefusedBeforeRunWork( diff --git a/libtmux-mcp/src/test/java/io/github/libtmux/mcp/TmuxMcpServerTest.java b/libtmux-mcp/src/test/java/io/github/libtmux/mcp/TmuxMcpServerTest.java index 81df0b5ef..39ec181e8 100644 --- a/libtmux-mcp/src/test/java/io/github/libtmux/mcp/TmuxMcpServerTest.java +++ b/libtmux-mcp/src/test/java/io/github/libtmux/mcp/TmuxMcpServerTest.java @@ -49,6 +49,9 @@ void readBatchBoundsTheCompleteJsonRpcLine() throws Exception { TmuxTransport environment = new TmuxTransport() { @Override public CommandResult execute(CommandRequest request) { + if (VersionProbes.is(request)) { + return VersionProbes.reply(request, "3.6"); + } return new CommandResult(0, List.of("VALUE=" + payload), List.of()); } @@ -254,6 +257,9 @@ void oversizedRequestIdFailsBeforeToolDispatch() throws Exception { TmuxTransport environment = new TmuxTransport() { @Override public CommandResult execute(CommandRequest request) { + if (VersionProbes.is(request)) { + return VersionProbes.reply(request, "3.6"); + } calls.incrementAndGet(); return new CommandResult(0, List.of("VALUE=kept"), List.of()); } diff --git a/libtmux-mcp/src/test/java/io/github/libtmux/mcp/TypingTest.java b/libtmux-mcp/src/test/java/io/github/libtmux/mcp/TypingTest.java index 2dabf92da..4263a3763 100644 --- a/libtmux-mcp/src/test/java/io/github/libtmux/mcp/TypingTest.java +++ b/libtmux-mcp/src/test/java/io/github/libtmux/mcp/TypingTest.java @@ -290,7 +290,7 @@ void sendReservationCoversItsFinalPreflight(Server server) throws Exception { List sends = indexes(requests, request -> hasCommand(request, "send-keys")); assertEquals(2, preflights.size()); assertEquals(1, sends.size()); - assertEquals(preflights.getLast() + 1, sends.getFirst()); + assertEquals(preflights.getLast() + 1, sends.getFirst(), "the daemon is known, so nothing separates them"); } @Test @@ -627,6 +627,45 @@ void synchronizedKeysReachOnlyTheEffectiveOnCohort(Server server) throws Excepti assertFalse(captureOf(server, disabled.id().value()).contains(marker)); } + @Test + void typingCapturePreservesSoftWrappedInput(Server server) throws Exception { + Pane source = server.panes().getFirst(); + source.window().resizeTo(new io.github.libtmux.Dimensions(80, 24)); + String prefix = "P".repeat(70); + String consumer = "stty raw -echo; printf '%s' '" + prefix + "'; exec cat"; + source.respawn("sh", "-c", consumer); + Pane peer = + source.split(SplitSpec.builder().running("sh", "-c", consumer).build()); + Pane disabled = + source.split(SplitSpec.builder().running("sh", "-c", consumer).build()); + assertTrue(await(() -> List.of(source, peer, disabled).stream() + .allMatch(pane -> "cat".equals(pane.expand("#{pane_current_command}")) + && captureOf(server, pane.id().value()).contains(prefix)))); + source.window().synchronizePanes(); + disabled.options().set("synchronize-panes", "off"); + String marker = "batch-first-marker"; + + Typing.Sent sent = sendKeys(server, source.id().value(), marker); + + assertEquals(sorted(source.id().value(), peer.id().value()), sent.resolvedPaneIds()); + assertTrue( + await(() -> String.join( + "\n", + server.cmd( + "capture-pane", + "-p", + "-t", + source.id().value()) + .stdout()) + .contains("batch-firs\nt-marker")), + "the fixture did not soft-wrap its input"); + assertTrue( + captureOf(server, source.id().value()).contains(marker), + captureOf(server, source.id().value())); + assertTrue(await(() -> captureOf(server, peer.id().value()).contains(marker))); + assertFalse(captureOf(server, disabled.id().value()).contains(marker)); + } + @Test void sourceOffIgnoresATruePeer(Server server) throws Exception { var source = server.panes().getFirst(); @@ -954,7 +993,7 @@ void eachConcurrentPasteReachesOnlyItsOwnPane(Server server) throws Exception { TmuxTransport interleaving = new TmuxTransport() { @Override public CommandResult execute(CommandRequest request) { - if (String.join(" ", request.commands().get(0)).contains("paste-buffer")) { + if (hasCommand(request, "paste-buffer")) { // Hold both pastes open at once, so a shared buffer name would collide. bothPending.countDown(); await(bothPending); @@ -983,7 +1022,8 @@ public void close() {} } static String captureOf(Server server, String pane) { - return String.join("\n", server.cmd("capture-pane", "-p", "-t", pane).stdout()); + return String.join( + "\n", server.cmd("capture-pane", "-p", "-J", "-t", pane).stdout()); } private static List sorted(String... paneIds) { diff --git a/libtmux-mcp/src/test/java/io/github/libtmux/mcp/VersionProbes.java b/libtmux-mcp/src/test/java/io/github/libtmux/mcp/VersionProbes.java new file mode 100644 index 000000000..11ac6b302 --- /dev/null +++ b/libtmux-mcp/src/test/java/io/github/libtmux/mcp/VersionProbes.java @@ -0,0 +1,39 @@ +package io.github.libtmux.mcp; + +import io.github.libtmux.transport.CommandRequest; +import io.github.libtmux.transport.CommandResult; +import java.util.List; + +/** + * The version probe as a test double sees it, and tmux's answer to it. + * + *

The probe is {@code run-shell -C '#{version}'}: the daemon expands the format, fails to + * parse the result as a command, and spells the version in {@code unknown command: }. + */ +final class VersionProbes { + + private static final String MARKER = "libtmux-version-"; + private static final String FORMAT = "#{version}"; + + private VersionProbes() {} + + /** Whether this request is the version probe and nothing else. */ + static boolean is(CommandRequest request) { + return request.commands().size() == 1 && is(request.commands().getFirst()); + } + + static boolean is(List command) { + return command.size() == 3 + && command.get(0).equals("run-shell") + && command.get(1).equals("-C") + && command.get(2).startsWith(MARKER) + && command.get(2).endsWith(FORMAT); + } + + /** The reply of a daemon that reports this version. */ + static CommandResult reply(CommandRequest request, String version) { + String argument = request.commands().getFirst().get(2); + String marker = argument.substring(0, argument.length() - FORMAT.length()); + return new CommandResult(1, List.of(), List.of("unknown command: " + marker + version)); + } +} diff --git a/libtmux-mcp/src/test/java/io/github/libtmux/mcp/WaitingForTextTest.java b/libtmux-mcp/src/test/java/io/github/libtmux/mcp/WaitingForTextTest.java index ef08d66d7..2f370ef1c 100644 --- a/libtmux-mcp/src/test/java/io/github/libtmux/mcp/WaitingForTextTest.java +++ b/libtmux-mcp/src/test/java/io/github/libtmux/mcp/WaitingForTextTest.java @@ -152,6 +152,7 @@ void sendThenWaitDoesNotMatchTheEchoedCommandLineOnAWarmShellEither(Server serve * shell would otherwise print it first. */ private static void typeAndSubmit(Server server, Pane pane, String line) { + awaitQuietShell(server, pane.id().value()); Typing.sendKeys(TestCalls.on(server, "pane_id", pane.id().value(), "keys", List.of(line), "literal", true)); Typing.sendKeys(TestCalls.on(server, "pane_id", pane.id().value(), "keys", List.of("Enter"), "literal", false)); } @@ -310,7 +311,38 @@ private static boolean onScreen(Server server, String pane, String text) { * only a call that goes through it gets recorded there. */ private static void send(Server server, String pane, String command) { + awaitQuietShell(server, pane); Typing.sendKeys(TestCalls.on(server, "pane_id", pane, "keys", List.of(command), "literal", true)); Typing.sendKeys(TestCalls.on(server, "pane_id", pane, "keys", List.of("Enter"), "literal", false)); } + + /** + * Returns once the shell has drawn its prompt and the pane has stopped changing. + * + *

Keys sent to a shell that is still starting are echoed by the terminal, not the shell, and + * the shell then draws its prompt straight after that echo on the same row. The echo is then + * glued to the prompt and no longer stands apart as a typed line, which is a different case from + * the one these tests pin. A slow runner reaches it by sending the keys a moment too early. + */ + private static void awaitQuietShell(Server server, String pane) { + long deadline = System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(30); + String last = null; + int steady = 0; + while (System.nanoTime() < deadline) { + String now = String.join( + "\n", server.cmd("capture-pane", "-p", "-t", pane).stdout()); + steady = now.equals(last) && !now.isBlank() ? steady + 1 : 0; + if (steady >= 5) { + return; + } + last = now; + try { + Thread.sleep(100); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw new AssertionError("interrupted waiting for the shell to settle", interrupted); + } + } + throw new AssertionError("the shell never settled: " + last); + } } diff --git a/libtmux-scala-cats/README.md b/libtmux-scala-cats/README.md index 057059eea..db236e8d3 100644 --- a/libtmux-scala-cats/README.md +++ b/libtmux-scala-cats/README.md @@ -55,12 +55,13 @@ Server.resource[IO](config).use { server => ## Observe a session rename +Managed control requires tmux 3.7+ and a carrier-owned pinned socket. Register the subscription and start its reader before changing tmux state. Match the typed notification before acting on it. The stream suspends the fiber, not a platform thread, while idle — `Async[F].async` over the subscription's own `poll()`/`onReady()`, not a dedicated blocking pool. - + ```scala import _root_.cats.effect.IO import _root_.cats.syntax.all._ diff --git a/libtmux-scala-cats/src/test/scala/io/github/libtmux/scaladsl/cats/LifecycleSuite.scala b/libtmux-scala-cats/src/test/scala/io/github/libtmux/scaladsl/cats/LifecycleSuite.scala index d460b02c4..813d310e3 100644 --- a/libtmux-scala-cats/src/test/scala/io/github/libtmux/scaladsl/cats/LifecycleSuite.scala +++ b/libtmux-scala-cats/src/test/scala/io/github/libtmux/scaladsl/cats/LifecycleSuite.scala @@ -43,9 +43,24 @@ final class LifecycleSuite extends FunSuite { @volatile var thread = "" override def execute(request: CommandRequest): CommandResult = { + val first = request.commands().getFirst() + if ( + first.size() == 3 && first + .get(0) == "run-shell" && first.get(1) == "-C" && + first.get(2).startsWith("libtmux-version-") + ) + return new CommandResult( + 1, + List.of(), + List.of( + "unknown command: " + first + .get(2) + .stripSuffix("#{version}") + "3.7c" + ) + ) calls.incrementAndGet() thread = Thread.currentThread().getName - request.commands().get(0).get(0) match { + request.commands().getLast().getFirst() match { case "block" => entered.complete(()) try { diff --git a/libtmux-scala-cats/src/test/scala/io/github/libtmux/scaladsl/cats/ObservationSuite.scala b/libtmux-scala-cats/src/test/scala/io/github/libtmux/scaladsl/cats/ObservationSuite.scala index a464ecf77..8d52abc93 100644 --- a/libtmux-scala-cats/src/test/scala/io/github/libtmux/scaladsl/cats/ObservationSuite.scala +++ b/libtmux-scala-cats/src/test/scala/io/github/libtmux/scaladsl/cats/ObservationSuite.scala @@ -1,11 +1,14 @@ package io.github.libtmux.scaladsl.cats import io.github.libtmux.exception.ControlEndedException -import _root_.cats.effect.{Deferred, IO} +import _root_.cats.effect.{Deferred, IO, Resource} import _root_.cats.effect.unsafe.implicits.global import _root_.cats.syntax.all._ import io.github.libtmux.ServerConfig import io.github.libtmux.SessionId +import io.github.libtmux.transport.ControlCarrier +import java.time.Duration +import java.util.OptionalLong import io.github.libtmux.control.{ControlClient, Delivery, PaneOutput} import java.nio.file.Files import java.nio.file.attribute.PosixFilePermissions @@ -38,8 +41,8 @@ final class ObservationSuite extends FunSuite { PosixFilePermissions.fromString("rwx------") ) val config = ServerConfig.builder().binary(fake.toString).build() - val program = Control - .attachUnfenced[IO](config, new SessionId("$0")) + val program = ObservationSuite + .attached(config) .use(control => control.output(1).use(_.stream.compile.drain)) val ran = try program.timeout(3.seconds).attempt.unsafeRunSync() @@ -75,7 +78,7 @@ final class ObservationSuite extends FunSuite { PosixFilePermissions.fromString("rwx------") ) val config = ServerConfig.builder().binary(fake.toString).build() - val client = ControlClient.attachUnfenced(config, new SessionId("$0")) + val client = ObservationSuite.client(config) val subscription = client.subscribeOutput(1) val program = Observation .resource[IO, PaneOutput](subscription, new AtomicBoolean(false)) @@ -143,7 +146,7 @@ final class ObservationSuite extends FunSuite { ) val config = ServerConfig.builder().binary(fake.toString).build() val program = - Control.attachUnfenced[IO](config, new SessionId("$0")).use { control => + ObservationSuite.attached(config).use { control => for { alive <- control.isAlive watched <- control.watch("cmd", "%1", "#{pane_current_command}") @@ -195,7 +198,7 @@ final class ObservationSuite extends FunSuite { ) val config = ServerConfig.builder().binary(fake.toString).build() val program = - Control.attachUnfenced[IO](config, new SessionId("$0")).use { control => + ObservationSuite.attached(config).use { control => for { _ <- IO.sleep(200.millis) text <- control.standardError @@ -217,6 +220,43 @@ final class ObservationSuite extends FunSuite { object ObservationSuite { + // Protocol fixtures qualify their model realm; native pinning is tested against real sockets. + def client(config: ServerConfig): ControlClient = { + val carrier = new ControlCarrier { + override def start(command: java.util.List[String]): Process = + new ProcessBuilder(command).start() + override def pinSocket( + socket: String, + budget: Duration + ): ControlCarrier.PinnedSocket = + new ControlCarrier.PinnedSocket { + override def socketPath(): String = "/fake-control-pin" + override def close(): Unit = () + } + } + ControlClient.attach( + carrier, + config, + new SessionId("$0"), + 4242L, + OptionalLong.of(10L), + "3.7c", + Duration.ofSeconds(30) + ) + } + + def attached(config: ServerConfig): Resource[IO, Control[IO]] = + for { + closed <- Resource.eval(IO(new AtomicBoolean(false))) + underlying <- Resource.make(IO.interruptible(client(config)))(client => + IO.interruptible(client.close()) + ) + execution <- Execution.resource[IO](4) + control <- Resource.make( + IO(new Control[IO](underlying, closed, execution)) + )(_.markClosed) + } yield control + /** What every fake starts with. tmux follows each request line with a marker * line, a `display-message -p` of a token, and a reply ends with the * marker's block: `read_request` reads a request and its marker, and @@ -224,7 +264,22 @@ object ObservationSuite { * marker's block. */ val prelude: String = - """|read_request() { IFS= read -r request && IFS= read -r marker; } + """|for argument in "$@"; do + | case "$argument" in + | libtmux-version-*'#{version}') + | printf 'unknown command: %s3.7c\n' "${argument%'#{version}'}" >&2; exit 1;; + | esac + | case "$argument" in + | libtmux-identity-*) + | printf 'unknown command: %s3.7c,4242,10,/fake-control\n' "${argument%%'#{version}'*}" >&2; exit 1;; + | esac + |done + |read_request() { + | while IFS= read -r request && IFS= read -r marker; do + | return 0 + | done + | return 1 + |} |answer() { | printf '%%begin 1 1 1\n' | for line in "$@"; do printf '%s\n' "$line"; done diff --git a/libtmux-scala-ox/README.md b/libtmux-scala-ox/README.md index 0c402e9db..b71d12e52 100644 --- a/libtmux-scala-ox/README.md +++ b/libtmux-scala-ox/README.md @@ -19,7 +19,9 @@ concurrency on virtual threads.** This optional module adds no handle type: the direct-style facade's opaque handles work unchanged inside an Ox `supervised` scope and its `fork`s. What it adds is a cold `Flow` for the two things that push rather than answer: a control -client's subscription, and a live view of a server. +client's subscription, and a live view of a server. Acquiring either requires +tmux 3.7+ and a carrier-owned pinned socket. Ordinary direct-style +operations retain the core 3.3a minimum. ```sbt diff --git a/libtmux-scala/README.md b/libtmux-scala/README.md index f13267436..975f716e3 100644 --- a/libtmux-scala/README.md +++ b/libtmux-scala/README.md @@ -30,7 +30,7 @@ for production. ## Requirements -Scala 3.9 and JDK 25 or newer; there is no Scala 2.13 build. tmux 3.2a through +Scala 3.9 and JDK 25 or newer; there is no Scala 2.13 build. tmux 3.3a through 3.7c, which the Java library supports and every tmux lane in CI runs these suites against. See [Compatibility](../docs/guide/scala/compatibility.md). diff --git a/libtmux-workspace-cli/README.md b/libtmux-workspace-cli/README.md index 549baf41e..3f817c5cf 100644 --- a/libtmux-workspace-cli/README.md +++ b/libtmux-workspace-cli/README.md @@ -18,7 +18,15 @@ Command names and flags follow [tmuxp](https://tmuxp.git-pull.com/) 1.74.0, and every command answers in JSON or NDJSON on request. Loading, capture, conversion, imports, discovery and search run on the JVM alone; only the -Python shell, tmuxp plugins and custom builders need Python. JDK 25 or newer, tmux 3.2a or newer. +Python shell, tmuxp plugins and custom builders need Python. JDK 25 or newer, +tmux 3.3a or newer. + +The Python bridge requires tmuxp 1.74.0 and Python libtmux with the core version check +for tmux 3.3a or newer and the integer optional-feature policy marker at least +2. Both the runtime probe and the actual child check before extension or shell +code runs. Until that coordinated Python change is released, load its source +in the selected interpreter; see [migration notes](../MIGRATION.md). +Native loading retains the core floor and does not acquire control. `io.github.libtmux:libtmux-workspace-cli` — [on Maven Central](https://central.sonatype.com/artifact/io.github.libtmux/libtmux-workspace-cli). @@ -378,8 +386,11 @@ finish later when the caller drains it; cancellation does not reap that write. ## Python Python shell commands and extension workspaces use `TMUX_WORKSPACE_PYTHON` -(default `python3`) with tmuxp 1.74.0 installed. The version is checked before -any input workspace changes. Nonempty `plugins`, `workspace_builder`, or +(default `python3`) with tmuxp 1.74.0 and the coordinated Python libtmux core +and optional-feature policy. The child checks `TMUX_MIN_VERSION >= 3.3a` and +an integer `TMUX_OPTIONAL_FEATURE_POLICY_VERSION >= 2` before loading external +code. These are library capabilities; a newer tmux executable alone does not +provide them. Nonempty `plugins`, `workspace_builder`, or `workspace_builder_paths` select the bridge. Builder paths resolve relative to the workspace file; custom builders receive their own configuration keys. Plugin import or version failures stop the load instead of prompting to skip @@ -407,10 +418,6 @@ does not produce native pane progress. Java retains observed sessions after extension failure. Python builders and plugins remain executable user code with their own mutation and cleanup behavior. -On tmux 3.2a, the Python classic builder can fail after tmux rewrites a session -name containing `$`. Use a name without `$` for extension workspaces on that -version; the bridge does not alter tmux's naming behavior. - ## Subprocesses Captured subprocesses retain bounded output and report truncation. Linux uses @@ -475,7 +482,9 @@ $ ./gradlew :libtmux-workspace-cli:check \ The tests use isolated sockets under the Java test root. PTY regressions use Python 3 standard-library helpers; native application commands do not require -Python. Set `TMUX_WORKSPACE_TEST_PYTHON` to an interpreter with tmuxp 1.74.0 to -enable the extension fixtures. Tests replace `HOME`; user-installed Python +Python. Set `TMUX_WORKSPACE_TEST_PYTHON` to an interpreter with tmuxp 1.74.0 +and coordinated Python libtmux to enable the extension fixtures. CI verifies a +reviewed source overlay; see [migration notes](../MIGRATION.md) for its +unreleased dependency boundary. Tests replace `HOME`; user-installed Python packages also need an explicit `PYTHONUSERBASE`. The repository-wide `check` remains the full gate; a focused CLI check does not replace it. diff --git a/libtmux-workspace-cli/build.gradle.kts b/libtmux-workspace-cli/build.gradle.kts index 7567166e0..a14b49fe1 100644 --- a/libtmux-workspace-cli/build.gradle.kts +++ b/libtmux-workspace-cli/build.gradle.kts @@ -77,3 +77,6 @@ tasks.register("runDevelopment") { ) jvmArgs("-XX:TieredStopAtLevel=1") } + +// Shared native terminal driver; test resources never enter a published artifact. +sourceSets.test { resources.srcDir(rootProject.file("gradle/test-fixtures")) } diff --git a/libtmux-workspace-cli/src/main/java/io/github/libtmux/workspace/cli/Children.java b/libtmux-workspace-cli/src/main/java/io/github/libtmux/workspace/cli/Children.java index e3b180f15..ad7734860 100644 --- a/libtmux-workspace-cli/src/main/java/io/github/libtmux/workspace/cli/Children.java +++ b/libtmux-workspace-cli/src/main/java/io/github/libtmux/workspace/cli/Children.java @@ -291,7 +291,11 @@ static String python(Main.Context context, Reporter report) throws InterruptedEx executable(context, context.environment().getOrDefault("TMUX_WORKSPACE_PYTHON", "python3")); Output version = run( context, - List.of(executable, "-c", "import importlib.metadata; print(importlib.metadata.version('tmuxp'))"), + List.of( + executable, + "-c", + PythonExtensions.CORE_PRELUDE + + "import importlib.metadata; print(importlib.metadata.version('tmuxp'))"), context.directory(), report, Duration.ofSeconds(5), @@ -301,8 +305,9 @@ static String python(Main.Context context, Reporter report) throws InterruptedEx throw new Main.Failure( Machine.Code.SCRIPT_FAILED, 1, - "tmuxp 1.74.0 is required: " + refusal(version) - + "; set TMUX_WORKSPACE_PYTHON to an interpreter with tmuxp 1.74.0 installed"); + "tmuxp 1.74.0 is required with Python libtmux's core version check and optional feature policy v2: " + + refusal(version) + + "; set TMUX_WORKSPACE_PYTHON to an interpreter with tmuxp 1.74.0 and the coordinated Python libtmux installed"); return executable; } catch (IOException absent) { throw unusableRuntime(absent.getMessage(), absent); @@ -345,8 +350,12 @@ static void shell(Main.Context context, picocli.CommandLine.ParseResult args, Re if (interactive && !Main.controllingTerminal()) throw Main.usage("interactive Python shell requires a terminal; use -c for captured output"); Main.Context bridge = withSelectedTmuxFirstOnPath(context); - var command = new ArrayList<>( - List.of(python(bridge, report), "-u", "-c", "from tmuxp.cli import cli; cli()", "shell")); + var command = new ArrayList<>(List.of( + python(bridge, report), + "-u", + "-c", + PythonExtensions.CORE_PRELUDE + "from tmuxp.cli import cli; cli()", + "shell")); if (!interactive) { command.add("-c"); command.add(code); @@ -371,6 +380,9 @@ static void shell(Main.Context context, picocli.CommandLine.ParseResult args, Re String value = args.matchedPositionalValue(index, ""); if (!value.isEmpty()) command.add(value); } + try (var server = Execution.server(bridge, args)) { + Execution.requireTmuxForExternalPython(server); + } Output output = interactive ? terminal(bridge, command) : run(bridge, command, bridge.directory(), report, Duration.ofHours(24)); diff --git a/libtmux-workspace-cli/src/main/java/io/github/libtmux/workspace/cli/Execution.java b/libtmux-workspace-cli/src/main/java/io/github/libtmux/workspace/cli/Execution.java index f587195a5..140decbc8 100644 --- a/libtmux-workspace-cli/src/main/java/io/github/libtmux/workspace/cli/Execution.java +++ b/libtmux-workspace-cli/src/main/java/io/github/libtmux/workspace/cli/Execution.java @@ -60,6 +60,11 @@ static String tmuxExecutable(Main.Context context) { } } + /** Validates the selected tmux without starting a daemon before Python runs. */ + static void requireTmuxForExternalPython(Server server) { + server.requireCompatibleTmux(); + } + static void load(Main.Context context, ParseResult args, Reporter report) throws IOException, InterruptedException { if (Main.flag(args, "-8")) throw new Main.Failure( @@ -587,29 +592,12 @@ private static int freeIndex(Set occupied, int first) { return index; } - /** - * A new owned session, sized like tmuxp's terminal-size detection where {@code Server} - * can honor it, and plain otherwise. - * - *

{@code TMUXP_DEFAULT_COLUMNS}/{@code TMUXP_DEFAULT_ROWS} (else {@code COLUMNS}/{@code ROWS}, - * else 80x24) seed the size; the real stdout's terminal overrides it where there is one, and - * {@code COLUMNS}/{@code LINES} override that. {@code TMUXP_DETECT_TERMINAL_SIZE} set to anything - * but {@code 1} disables detection outright. Below tmux 3.3a, {@code Server} refuses a size; that - * refusal is caught here rather than predicted, because it also carries the running version. - */ + /** A new owned session, using tmuxp's terminal-size detection. */ private static Session newOwnedSession(Main.Context context, Server server, WorkspacePlan plan) throws IOException, InterruptedException { SessionSpec.Builder spec = SessionSpec.builder().named(plan.name()).in(plan.directory()); - Optional size = sessionDimensions(context); - if (size.isEmpty()) return server.newSession(spec.build()); - try { - return server.newSession(spec.sized(size.orElseThrow()).build()); - } catch (UnsupportedFeatureException tooOld) { - return server.newSession(SessionSpec.builder() - .named(plan.name()) - .in(plan.directory()) - .build()); - } + sessionDimensions(context).ifPresent(spec::sized); + return server.newSession(spec.build()); } private static Optional sessionDimensions(Main.Context context) diff --git a/libtmux-workspace-cli/src/main/java/io/github/libtmux/workspace/cli/PythonExtensions.java b/libtmux-workspace-cli/src/main/java/io/github/libtmux/workspace/cli/PythonExtensions.java index a68edf529..f1aedc698 100644 --- a/libtmux-workspace-cli/src/main/java/io/github/libtmux/workspace/cli/PythonExtensions.java +++ b/libtmux-workspace-cli/src/main/java/io/github/libtmux/workspace/cli/PythonExtensions.java @@ -19,7 +19,20 @@ final class PythonExtensions { private PythonExtensions() {} - private static final String BUILD = """ + static final String CORE_PRELUDE = """ + import re as _libtmux_re + from libtmux.common import TMUX_MIN_VERSION as _libtmux_floor + import libtmux.common as _libtmux_common + _libtmux_floor = str(_libtmux_floor) + _libtmux_release = _libtmux_re.fullmatch(r'(\\d+)\\.(\\d+)([a-z]?)', _libtmux_floor) + if not _libtmux_release or (int(_libtmux_release[1]), int(_libtmux_release[2]), _libtmux_release[3]) < (3, 3, 'a'): + raise RuntimeError('Python libtmux with a tmux 3.3a minimum is required; install a compatible Python libtmux in TMUX_WORKSPACE_PYTHON (loaded floor: ' + _libtmux_floor + ')') + _libtmux_optional = getattr(_libtmux_common, 'TMUX_OPTIONAL_FEATURE_POLICY_VERSION', None) + if type(_libtmux_optional) is not int or _libtmux_optional < 2: + raise RuntimeError('Python libtmux optional feature policy v2 is required; install the coordinated Python libtmux in TMUX_WORKSPACE_PYTHON') + """; + + private static final String BUILD = CORE_PRELUDE + """ import importlib, json, sys from pathlib import Path from libtmux import Server @@ -97,6 +110,7 @@ static Session build( Reporter report, ObjectNode effects) throws IOException, InterruptedException { + Execution.requireTmuxForExternalPython(server); Set windows = new HashSet<>(); Set panes = new HashSet<>(); String daemon = ""; diff --git a/libtmux-workspace-cli/src/test/java/io/github/libtmux/workspace/cli/ExecutionTest.java b/libtmux-workspace-cli/src/test/java/io/github/libtmux/workspace/cli/ExecutionTest.java index 4ecde6d62..0c38e76a2 100644 --- a/libtmux-workspace-cli/src/test/java/io/github/libtmux/workspace/cli/ExecutionTest.java +++ b/libtmux-workspace-cli/src/test/java/io/github/libtmux/workspace/cli/ExecutionTest.java @@ -21,6 +21,7 @@ import java.nio.file.Path; import java.time.Duration; import java.util.HashMap; +import java.util.List; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; @@ -72,6 +73,162 @@ private static void assertReportedObjects(Server server, JsonNode effects) { effects.path("pane_ids").valueStream().map(JsonNode::asText).collect(toSet())); } + @ParameterizedTest + @ValueSource(strings = {"load", "shell"}) + void obsoletePythonBackendIsRefusedBeforeExternalCode(String entry) throws Exception { + Path marker = directory.resolve("backend-entered"); + Path backend = pythonBackend("3.2a", marker); + Path source = directory.resolve("external.yaml"); + Files.writeString( + source, + "session_name: external\nplugins: [forbidden.Plugin]\nwindows:\n- window_name: main\n panes: [null]\n"); + String argument = entry.equals("load") ? source.toString() : "-c"; + String value = entry.equals("load") ? "-d" : "print('must-not-run')"; + Result result = invoke( + java.util.Map.of("PYTHONPATH", backend.toString(), "TMUX_WORKSPACE_PYTHON", "python3"), + entry, + "--json", + "-S", + directory.resolve("s").toString(), + argument, + value); + + assertFalse(Files.exists(marker), "obsolete Python code ran before the version check: " + result.err()); + assertTrue(result.code() != 0, result.out()); + assertTrue( + (result.out() + result.err()).contains("Python libtmux with a tmux 3.3a minimum is required"), + result.out() + result.err()); + } + + @Test + void aQualifiedPythonBackendCanEnterTheShell() throws Exception { + Path marker = directory.resolve("qualified-backend-entered"); + Path backend = pythonBackend("3.3a", marker); + Result result = invoke( + java.util.Map.of("PYTHONPATH", backend.toString(), "TMUX_WORKSPACE_PYTHON", "python3"), + "shell", + "--json", + "-S", + directory.resolve("s").toString(), + "-c", + "print('qualified')"); + + assertEquals(0, result.code(), result.out() + result.err()); + assertTrue(Files.exists(marker), "the qualified child was not entered"); + } + + @ParameterizedTest + @ValueSource(strings = {"missing", "0", "1", "False", "True", "'2'", "2.0"}) + void pythonOptionalPolicyIsRequiredBeforeExternalCode(String policy) throws Exception { + Path marker = directory.resolve("optional-policy-entered"); + Path backend = pythonBackend("3.3a", marker, policy.equals("missing") ? "" : policy); + assertPythonPolicyRefused(backend, "python3", marker); + } + + @Test + void pythonChildRechecksOptionalPolicyAfterTheRuntimeProbe() throws Exception { + Path marker = directory.resolve("optional-child-entered"); + Path backend = pythonBackend("3.3a", marker, "1"); + Path python = directory.resolve("python-probe-only"); + Files.writeString(python, """ + #!/bin/sh + case "$*" in *importlib.metadata*) printf '1.74.0\\n'; exit;; esac + exec python3 "$@" + """); + assertTrue(python.toFile().setExecutable(true)); + assertPythonPolicyRefused(backend, python.toString(), marker); + } + + private void assertPythonPolicyRefused(Path backend, String python, Path marker) throws Exception { + Path source = directory.resolve("optional-policy.yaml"); + Files.writeString( + source, + "session_name: external\nplugins: [forbidden.Plugin]\nwindows:\n- window_name: main\n panes: [null]\n"); + for (String entry : List.of("load", "shell")) { + Result result = invoke( + java.util.Map.of("PYTHONPATH", backend.toString(), "TMUX_WORKSPACE_PYTHON", python), + entry, + "--json", + "-S", + directory.resolve("s").toString(), + entry.equals("load") ? source.toString() : "-c", + entry.equals("load") ? "-d" : "print('must-not-run')"); + assertFalse(Files.exists(marker), "unqualified backend code ran: " + result.err()); + assertTrue(result.code() != 0, result.out()); + assertTrue( + (result.out() + result.err()).contains("optional feature policy v2"), result.out() + result.err()); + } + } + + private Path pythonBackend(String floor, Path marker) throws Exception { + return pythonBackend(floor, marker, "2"); + } + + private Path pythonBackend(String floor, Path marker, String policy) throws Exception { + Path backend = directory.resolve("python-" + floor); + Files.createDirectories(backend.resolve("libtmux")); + Files.writeString( + backend.resolve("libtmux/__init__.py"), "class Server:\n def __init__(self, **kwargs): pass\n"); + Files.writeString( + backend.resolve("libtmux/common.py"), + "TMUX_MIN_VERSION = '" + floor + "'\n" + + (policy.isEmpty() ? "" : "TMUX_OPTIONAL_FEATURE_POLICY_VERSION = " + policy + "\n")); + Files.createDirectories(backend.resolve("tmuxp/workspace")); + Files.writeString(backend.resolve("tmuxp/__init__.py"), ""); + String entered = "from pathlib import Path\nPath(" + Documents.JSON.writeValueAsString(marker.toString()) + + ").write_text('entered')\n"; + Files.writeString(backend.resolve("tmuxp/workspace/__init__.py"), entered); + Files.writeString( + backend.resolve("tmuxp/cli.py"), "def cli():\n" + entered.indent(4) + " print('qualified')\n"); + Files.createDirectories(backend.resolve("tmuxp-1.74.0.dist-info")); + Files.writeString( + backend.resolve("tmuxp-1.74.0.dist-info/METADATA"), + "Metadata-Version: 2.1\nName: tmuxp\nVersion: 1.74.0\n"); + return backend; + } + + @ParameterizedTest + @ValueSource(strings = {"load", "shell"}) + void externalPythonCannotStartAnUnsupportedFreshTmux(String entry) throws Exception { + Path marker = directory.resolve("python-builder-started"); + Path python = directory.resolve("python"); + Files.writeString( + python, + "#!/bin/sh\ncase \"$*\" in *importlib.metadata*) printf '1.74.0\\n'; exit;; esac\nprintf started > '" + + marker + "'\nexit 1\n"); + assertTrue(python.toFile().setExecutable(true)); + Path tmux = directory.resolve("old-tmux"); + Files.writeString( + tmux, + "#!/bin/sh\nfor argument in \"$@\"; do\n if [ \"$argument\" = -V ]; then printf 'tmux 3.3\\n'; exit; fi\ndone\nprintf 'no server running\\n' >&2\nexit 1\n"); + assertTrue(tmux.toFile().setExecutable(true)); + Path source = directory.resolve("external.yaml"); + Files.writeString(source, "session_name: external\nworkspace_builder: external:Builder\nwindows: [{}]\n"); + String[] arguments = entry.equals("load") + ? new String[] { + "load", + source.toString(), + "-d", + "-S", + directory.resolve("s").toString(), + "--json" + } + : new String[] { + "shell", + "-c", + "print('must not run')", + "-S", + directory.resolve("s").toString(), + "--json" + }; + Result result = invoke( + java.util.Map.of("LIBTMUX_TEST_TMUX", tmux.toString(), "TMUX_WORKSPACE_PYTHON", python.toString()), + arguments); + assertEquals(1, result.code(), result.toString()); + assertTrue(result.err().contains("3.3a"), result.toString()); + assertFalse(Files.exists(marker), "external Python started before the minimum-version check"); + } + @ParameterizedTest @CsvSource({"false,false", "false,true", "true,false", "true,true"}) void pythonExtensionsUseTheSelectedServerAndReportObservedEffects(boolean append, boolean fail) throws Exception { @@ -495,9 +652,9 @@ void loadRejectsANonNumericColumnsAsUsage() throws Exception { "usage", new ObjectMapper().readTree(result.err()).path("code").asText()); } - /** Below 3.3a a size is silently skipped, not sent and ignored, matching {@code SessionSpec}. */ + /** An unsupported executable is refused before session creation. */ @Test - void loadSkipsSizingOnATmuxOlderThan33a() throws Exception { + void loadRejectsAnUnsupportedTmuxInsteadOfRetryingWithoutSize() throws Exception { Path source = directory.resolve("size-old.yaml"); Path socket = directory.resolve("size-old-socket"); Path trace = directory.resolve("size-old-arguments"); @@ -522,8 +679,10 @@ void loadSkipsSizingOnATmuxOlderThan33a() throws Exception { "-f", "/dev/null", "--json"); - assertEquals(0, result.code(), result.err()); - assertFalse(Files.readString(trace).lines().anyMatch("-x"::equals), Files.readString(trace)); + assertEquals(1, result.code(), result.err()); + assertTrue(result.err().contains("3.3a"), result.err()); + assertFalse(Files.readString(trace).lines().anyMatch("new-session"::equals), Files.readString(trace)); + assertFalse(server.isAlive()); } finally { if (server.isAlive()) server.killServer(); } diff --git a/libtmux-workspace-cli/src/test/java/io/github/libtmux/workspace/cli/ProcessTest.java b/libtmux-workspace-cli/src/test/java/io/github/libtmux/workspace/cli/ProcessTest.java index 59591d0a1..346105edd 100644 --- a/libtmux-workspace-cli/src/test/java/io/github/libtmux/workspace/cli/ProcessTest.java +++ b/libtmux-workspace-cli/src/test/java/io/github/libtmux/workspace/cli/ProcessTest.java @@ -10,7 +10,9 @@ import io.github.libtmux.Server; import io.github.libtmux.ServerEndpoint; import java.io.BufferedReader; +import java.io.IOException; import java.io.InputStreamReader; +import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -26,29 +28,16 @@ /** Drives the installed launcher as a real process, which is why the whole class is tagged. */ @org.junit.jupiter.api.Tag("distribution") final class ProcessTest { - private static final String PTY = """ - import errno, os, select, subprocess, time - def drain_pty(fd): - chunks = [] - while select.select([fd], [], [], 0)[0]: - try: chunk = os.read(fd, 65536) - except OSError as error: - if error.errno == errno.EIO: break - raise - if not chunk: break - chunks.append(chunk) - return b''.join(chunks) - def wait_pty(child, master, timeout): - until = time.monotonic() + timeout - while child.poll() is None: - drain_pty(master) - remaining = until - time.monotonic() - if remaining <= 0: - raise subprocess.TimeoutExpired(child.args, timeout) - select.select([master], [], [], min(remaining, .025)) - drain_pty(master) - return child.returncode - """; + private static final String PTY = ptyHelper(); + + private static String ptyHelper() { + try (var source = ProcessTest.class.getResourceAsStream("/io/github/libtmux/fixtures/pty.py")) { + if (source == null) throw new IllegalStateException("missing PTY test helper"); + return new String(source.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException failure) { + throw new UncheckedIOException(failure); + } + } @TempDir Path directory; diff --git a/libtmux-workspace/src/main/java/io/github/libtmux/workspace/WorkspaceApplier.java b/libtmux-workspace/src/main/java/io/github/libtmux/workspace/WorkspaceApplier.java index cf5fc58fc..bcc641a5b 100644 --- a/libtmux-workspace/src/main/java/io/github/libtmux/workspace/WorkspaceApplier.java +++ b/libtmux-workspace/src/main/java/io/github/libtmux/workspace/WorkspaceApplier.java @@ -44,6 +44,7 @@ static Session build(Server server, Workspace workspace) { private static void validate(Server server, Workspace workspace) { for (WindowSpec window : workspace.windows()) { + io.github.libtmux.Names.require(window.name(), server); window.layout() .ifPresent(value -> Layouts.require(value, server, window.panes().size())); diff --git a/libtmux-workspace/src/test/java/io/github/libtmux/workspace/VersionProbes.java b/libtmux-workspace/src/test/java/io/github/libtmux/workspace/VersionProbes.java new file mode 100644 index 000000000..d664f3235 --- /dev/null +++ b/libtmux-workspace/src/test/java/io/github/libtmux/workspace/VersionProbes.java @@ -0,0 +1,39 @@ +package io.github.libtmux.workspace; + +import io.github.libtmux.transport.CommandRequest; +import io.github.libtmux.transport.CommandResult; +import java.util.List; + +/** + * The version probe as a test double sees it, and tmux's answer to it. + * + *

The probe is {@code run-shell -C '#{version}'}: the daemon expands the format, fails to + * parse the result as a command, and spells the version in {@code unknown command: }. + */ +final class VersionProbes { + + private static final String MARKER = "libtmux-version-"; + private static final String FORMAT = "#{version}"; + + private VersionProbes() {} + + /** Whether this request is the version probe and nothing else. */ + static boolean is(CommandRequest request) { + return request.commands().size() == 1 && is(request.commands().getFirst()); + } + + static boolean is(List command) { + return command.size() == 3 + && command.get(0).equals("run-shell") + && command.get(1).equals("-C") + && command.get(2).startsWith(MARKER) + && command.get(2).endsWith(FORMAT); + } + + /** The reply of a daemon that reports this version. */ + static CommandResult reply(CommandRequest request, String version) { + String argument = request.commands().getFirst().get(2); + String marker = argument.substring(0, argument.length() - FORMAT.length()); + return new CommandResult(1, List.of(), List.of("unknown command: " + marker + version)); + } +} diff --git a/libtmux-workspace/src/test/java/io/github/libtmux/workspace/WorkspaceBuilderTest.java b/libtmux-workspace/src/test/java/io/github/libtmux/workspace/WorkspaceBuilderTest.java index f83fb8ab4..439a109f3 100644 --- a/libtmux-workspace/src/test/java/io/github/libtmux/workspace/WorkspaceBuilderTest.java +++ b/libtmux-workspace/src/test/java/io/github/libtmux/workspace/WorkspaceBuilderTest.java @@ -350,16 +350,8 @@ void anUnsupportedBuiltInLayoutIsRejectedBeforeAnyEffect() { TmuxTransport transport = new TmuxTransport() { @Override public CommandResult execute(CommandRequest request) { - if (request.commands().get(0).get(0).equals("display-message")) { - return new CommandResult( - 0, - List.of(String.join( - io.github.libtmux.format.RowFormat.of("field") - .separator(), - "4242", - "3.4", - "1790000000")), - List.of()); + if (VersionProbes.is(request)) { + return VersionProbes.reply(request, "3.4"); } effected.set(true); return new CommandResult(0, List.of(), List.of()); @@ -386,16 +378,8 @@ void aJsonLayoutFromAnOldServerIsRefusedForItsVersionNotAsAnUnknownName() { TmuxTransport transport = new TmuxTransport() { @Override public CommandResult execute(CommandRequest request) { - if (request.commands().get(0).get(0).equals("display-message")) { - return new CommandResult( - 0, - List.of(String.join( - io.github.libtmux.format.RowFormat.of("field") - .separator(), - "4242", - "3.4", - "1790000000")), - List.of()); + if (VersionProbes.is(request)) { + return VersionProbes.reply(request, "3.4"); } effected.set(true); return new CommandResult(0, List.of(), List.of()); @@ -445,16 +429,16 @@ void anUncertainCreationStillTargetsItsUniqueStagingSessionForCleanup() { TmuxTransport transport = new TmuxTransport() { @Override public CommandResult execute(CommandRequest request) { - if (request.commands().get(0).get(0).equals("new-session")) { - staged.set(request.commands() - .get(0) - .get(request.commands().get(0).indexOf("-s") + 1)); + if (VersionProbes.is(request)) { + return VersionProbes.reply(request, "3.7c"); + } + List command = request.commands().getLast(); + if (command.getFirst().equals("new-session")) { + staged.set(command.get(command.indexOf("-s") + 1)); throw new DispatchException.Failed("reply lost", DispatchOutcome.UNKNOWN, null); } - if (request.commands().get(0).get(0).equals("kill-session")) { - cleaned.set(request.commands() - .get(0) - .get(request.commands().get(0).indexOf("-t") + 1)); + if (command.getFirst().equals("kill-session")) { + cleaned.set(command.get(command.indexOf("-t") + 1)); return new CommandResult(0, List.of(), List.of()); } return new CommandResult(0, List.of(), List.of()); diff --git a/libtmux/build.gradle.kts b/libtmux/build.gradle.kts index b58b2c1ff..b04eb8e99 100644 --- a/libtmux/build.gradle.kts +++ b/libtmux/build.gradle.kts @@ -48,10 +48,11 @@ tasks.jar { "the published module is named ${descriptor.name()}" } val exported = descriptor.exports().map { it.source() }.toSet() - require("io.github.libtmux.internal" !in exported) { - "the published module exports io.github.libtmux.internal" + val internal = { name: String -> name == "io.github.libtmux.internal" || name.startsWith("io.github.libtmux.internal.") } + require(exported.none(internal)) { + "the published module exports ${exported.filter(internal)}" } - val packages = descriptor.packages().filterNot { it == "io.github.libtmux.internal" } + val packages = descriptor.packages().filterNot(internal) val unexported = packages - exported require(unexported.isEmpty()) { "the published module hides packages a caller needs: $unexported" diff --git a/libtmux/src/main/java/io/github/libtmux/Buffers.java b/libtmux/src/main/java/io/github/libtmux/Buffers.java index 758aad16e..872be3ae0 100644 --- a/libtmux/src/main/java/io/github/libtmux/Buffers.java +++ b/libtmux/src/main/java/io/github/libtmux/Buffers.java @@ -24,17 +24,6 @@ public final class Buffers { private static final RowFormat LISTING = RowFormat.of("buffer_name", "buffer_size"); - /** - * tmux's own history dates the fix to {@code 0f6227f4} ("When deleting or renaming a buffer and - * a buffer name is specified, complain if the buffer doesn't exist instead of silently deleting - * or renaming the most recent buffer", GitHub issue 3205); {@code git tag --contains} places it - * on {@code 3.4} and nothing earlier, so 3.3 and 3.3a both have the bug. Confirmed on the matrix: - * {@code set-buffer -b a x; set-buffer -b b y; delete-buffer -b nope} answers {@code unknown - * buffer: nope} on 3.4 and leaves both buffers, but on 3.3a it exits 0 and takes {@code b}, the - * one {@code delete-buffer} was never told to touch. - */ - static final TmuxVersion EXACT_NAMED_DELETE = new TmuxVersion(3, 4, ""); - private final Server server; Buffers(Server server) { @@ -104,10 +93,6 @@ public String show(String name) { */ @Operation(Kind.MUTATION) public void delete(String name) { - TmuxVersion running = server.version(); - if (!running.atLeast(EXACT_NAMED_DELETE)) { - throw new UnsupportedFeatureException("deleting a buffer by exact name", EXACT_NAMED_DELETE, running); - } CommandResult result = server.cmd(List.of("delete-buffer", "-b", name)); if (!result.succeeded() && result.stderr().stream().anyMatch(line -> line.equals("unknown buffer: " + name))) { throw new TargetGoneException("no buffer named '" + name + "'"); diff --git a/libtmux/src/main/java/io/github/libtmux/CapturedServer.java b/libtmux/src/main/java/io/github/libtmux/CapturedServer.java new file mode 100644 index 000000000..6eb83e9b9 --- /dev/null +++ b/libtmux/src/main/java/io/github/libtmux/CapturedServer.java @@ -0,0 +1,73 @@ +package io.github.libtmux; + +import io.github.libtmux.snapshot.ServerSnapshot; +import java.util.List; +import kotlin.annotations.jvm.ReadOnly; + +/** + * Retains metadata and handles from one authenticated server capture. + * + *

Lists are immutable and preserve tmux order, including window links and pane occurrences. + * Accessors and captured handle relations issue no commands and remain readable after the server + * closes. Explicit handle operations still use the owning server and its PID fence. + * + *

The capture is an observed consistent graph, not a tmux transaction. Its identity carries + * the observed daemon PID, not a generation that distinguishes PID reuse. + */ +public final class CapturedServer { + + private final Server server; + private final ServerSnapshot snapshot; + + CapturedServer(Server server, ServerSnapshot snapshot) { + this.server = server; + this.snapshot = snapshot; + } + + /** Returns the server that acquired this capture. */ + public Server server() { + return server; + } + + /** Returns the identity bound to the captured daemon PID without probing tmux. */ + public ServerIdentity identity() { + return server.identity(snapshot); + } + + /** Returns the immutable snapshot that supplies every handle in this view. */ + public ServerSnapshot snapshot() { + return snapshot; + } + + /** Returns captured sessions in tmux order. */ + @ReadOnly + public List sessions() { + return snapshot.sessions().stream() + .map(state -> new Session(server, snapshot, state)) + .toList(); + } + + /** Returns captured window links in tmux order, retaining each placement. */ + @ReadOnly + public List windows() { + return snapshot.windows().stream() + .map(state -> new Window(server, snapshot, state)) + .toList(); + } + + /** Returns captured pane occurrences in tmux order, retaining each window placement. */ + @ReadOnly + public List panes() { + return snapshot.panes().stream() + .map(state -> new Pane(server, snapshot, state)) + .toList(); + } + + /** Returns captured clients in tmux order. */ + @ReadOnly + public List clients() { + return snapshot.clients().stream() + .map(state -> new Client(server, snapshot, state)) + .toList(); + } +} diff --git a/libtmux/src/main/java/io/github/libtmux/IncarnationFence.java b/libtmux/src/main/java/io/github/libtmux/IncarnationFence.java index f2d735a64..6286eabcf 100644 --- a/libtmux/src/main/java/io/github/libtmux/IncarnationFence.java +++ b/libtmux/src/main/java/io/github/libtmux/IncarnationFence.java @@ -2,6 +2,7 @@ import io.github.libtmux.exception.TargetGoneException; import io.github.libtmux.internal.CommandStrings; +import io.github.libtmux.internal.TmuxFeatures; import io.github.libtmux.snapshot.ServerSnapshot; import io.github.libtmux.snapshot.WindowContext; import io.github.libtmux.transport.CommandResult; @@ -66,17 +67,26 @@ CommandResult guarded(ServerSnapshot snapshot, String command) { } CommandResult guarded(ServerSnapshot snapshot, String command, String input) { + return guarded(snapshot, command, input, TmuxFeatures.Intent.literal(List.of())); + } + + CommandResult guarded(ServerSnapshot snapshot, String command, String input, TmuxFeatures.Intent intent) { long pid = snapshot.serverPid() .orElseThrow(() -> new IllegalStateException("a live handle has no server process identity")); - return guarded(pid, incarnation(pid, snapshot.serverStartTime()), command, input); + return guarded(pid, incarnation(pid, snapshot.serverStartTime()), command, input, intent); } CommandResult guarded(long pid, String condition, String command, String input) { + return guarded(pid, condition, command, input, TmuxFeatures.Intent.literal(List.of())); + } + + CommandResult guarded(long pid, String condition, String command, String input, TmuxFeatures.Intent intent) { String stale = "libtmux-stale-handle-" + pid; CommandResult result = server.cmd( List.of("if-shell", "-F", condition, command, stale), server.config().defaultTimeout(), - input); + input, + intent); if (!result.succeeded() && result.stderr().stream().anyMatch(line -> line.contains(stale))) { throw new TargetGoneException("the tmux server this handle belonged to has ended"); } @@ -91,8 +101,11 @@ CommandResult run(ServerSnapshot snapshot, WindowContext expected, List String stale = "libtmux-stale-winlink-" + pid + "-" + expected.window().value(); String condition = "#{&&:" + incarnation(pid, snapshot.serverStartTime()) + ",#{==:#{window_id}," + expected.window().value() + "}}"; - CommandResult result = - server.cmd(List.of("if-shell", "-F", "-t", target, condition, CommandStrings.stringify(argv), stale)); + CommandResult result = server.cmd( + List.of("if-shell", "-F", "-t", target, condition, CommandStrings.stringify(argv), stale), + server.config().defaultTimeout(), + "", + TmuxFeatures.Intent.literal(List.of(argv))); if (!result.succeeded() && result.stderr().stream().anyMatch(line -> line.contains(stale))) { throw new TargetGoneException("window " + expected.window() + " no longer exists here"); } diff --git a/libtmux/src/main/java/io/github/libtmux/Layouts.java b/libtmux/src/main/java/io/github/libtmux/Layouts.java index 226639e6c..bef9e89d4 100644 --- a/libtmux/src/main/java/io/github/libtmux/Layouts.java +++ b/libtmux/src/main/java/io/github/libtmux/Layouts.java @@ -190,7 +190,7 @@ private static IllegalArgumentException ambiguous(String layout, TmuxVersion run private static TmuxVersion version(Server server) { try { - return SessionCreation.versionForCreation(server); + return server.versionForCreation(); } catch (IllegalArgumentException invalid) { throw new MalformedResponseException("could not read tmux version", invalid); } diff --git a/libtmux/src/main/java/io/github/libtmux/Names.java b/libtmux/src/main/java/io/github/libtmux/Names.java new file mode 100644 index 000000000..cb16ec387 --- /dev/null +++ b/libtmux/src/main/java/io/github/libtmux/Names.java @@ -0,0 +1,44 @@ +package io.github.libtmux; + +import java.util.Objects; + +/** + * What the running tmux does with a session or window name. + * + *

Exactly tmux 3.7 refuses a name containing {@code .} or {@code :}; 3.7a and later keep it and + * earlier releases store {@code _} in its place. Refusing it here, before dispatch, keeps a batch or + * a workspace build from running the commands ahead of the one tmux would reject. + */ +public final class Names { + + private static final TmuxVersion REFUSES = new TmuxVersion(3, 7, ""); + + private Names() {} + + /** + * As {@link #require(String, TmuxVersion)}, for the daemon this server talks to, read without + * running a command that fires a user's hooks. + * + * @throws IllegalArgumentException if the daemon refuses a name with {@code .} or {@code :} + */ + public static String require(String name, Server server) { + Objects.requireNonNull(name, "name"); + return name.indexOf('.') < 0 && name.indexOf(':') < 0 ? name : require(name, server.versionForCreation()); + } + + /** + * Returns the name when the running tmux accepts it. + * + * @param name the session or window name to be sent + * @param running the version of the daemon that would receive it + * @throws IllegalArgumentException if the running tmux refuses a name with {@code .} or {@code :} + */ + public static String require(String name, TmuxVersion running) { + Objects.requireNonNull(name, "name"); + if (running.sameRelease(REFUSES) && (name.indexOf('.') >= 0 || name.indexOf(':') >= 0)) { + throw new IllegalArgumentException( + "tmux 3.7 refuses '.' and ':' in session and window names (3.7a and later keep them): " + name); + } + return name; + } +} diff --git a/libtmux/src/main/java/io/github/libtmux/OptionKey.java b/libtmux/src/main/java/io/github/libtmux/OptionKey.java index 09c87f3ac..e2e9f108b 100644 --- a/libtmux/src/main/java/io/github/libtmux/OptionKey.java +++ b/libtmux/src/main/java/io/github/libtmux/OptionKey.java @@ -20,7 +20,7 @@ * on {@link Options} remain for anything else. * *

The constants are the ones whose type is the same on every supported release, checked against - * tmux's own option table from 3.2a to 3.7c. {@code status} is not among the flags for that reason: + * tmux's own option table across the supported range. {@code status} is not among the flags for that reason: * tmux reads it as a choice of {@code off}, {@code on} and a line count. * * @param what the value reads as diff --git a/libtmux/src/main/java/io/github/libtmux/Pane.java b/libtmux/src/main/java/io/github/libtmux/Pane.java index 7ec230741..5ed49fdb2 100644 --- a/libtmux/src/main/java/io/github/libtmux/Pane.java +++ b/libtmux/src/main/java/io/github/libtmux/Pane.java @@ -696,22 +696,12 @@ ServerIdentity identity() { /** * Moves this pane into a window of its own, leaving tmux to name it. * - *

On tmux 3.7 the name is supplied rather than left to tmux, because letting tmux choose it - * there ends the server and every session on it. The name supplied is the one tmux would have - * chosen, so the result is the same window either way. + * @throws UnsupportedFeatureException on exact tmux 3.7, whose unnamed break can end the server; + * use {@link #breakOut(String)} or tmux 3.7a or newer */ @Operation(Kind.MUTATION) public Window breakOut() { - // The name tmux would have chosen anyway, supplied only so 3.7 does not have to choose it. - // Read live rather than from the capture: a pane's command changes as its shell starts, and - // a captured value can be older than the break by any amount. - return breakNamed(Optional.empty(), currentCommandNow()); - } - - /** What tmux says is running here at this moment, rather than when the capture was taken. */ - private String currentCommandNow() { - String reported = expand("#{pane_current_command}"); - return reported.isEmpty() ? state.currentCommand() : reported; + return breakNamed(Optional.empty()); } /** @@ -820,23 +810,28 @@ public void stopPiping() { @Operation(Kind.MUTATION) public Window breakOut(String windowName) { Objects.requireNonNull(windowName, "windowName"); - return breakNamed(Optional.of(windowName), windowName); + return breakNamed(Optional.of(windowName)); } - /** - * @param wanted the name the caller asked for, if they asked - * @param supplied the name to hand tmux, which is never absent because 3.7 crashes without one - */ - private Window breakNamed(Optional wanted, String supplied) { - List argv = new ArrayList<>(List.of("break-pane", "-d", "-n", supplied)); - argv.addAll(List.of("-s", state.id().value(), "-P", "-F", BROKEN_OUT.template())); - List fields = - BROKEN_OUT.split(server.run(snapshot, argv).stdout().get(0)); + private Window breakNamed(Optional wanted) { + TmuxVersion running = server.version(snapshot); + if (wanted.isEmpty() && running.sameRelease(BREAK_PANE_NAMING_BROKEN)) { + throw new UnsupportedFeatureException( + "unnamed break-pane is unreliable on tmux 3.7; use breakOut(name) or tmux 3.7a+"); + } + List argv = new ArrayList<>(List.of("break-pane", "-d")); + wanted.ifPresent(name -> argv.addAll(List.of("-n", name))); + argv.addAll(List.of("-s", state.id().value())); + List> commands = + List.of(argv, List.of("display-message", "-p", "-t", state.id().value(), BROKEN_OUT.template())); + List fields = BROKEN_OUT.split(server.runBreak(snapshot, commands, running, wanted.isPresent()) + .stdout() + .getFirst()); WindowContext created = new WindowContext( new SessionId(fields.get(0)), new WindowIndex(Integer.parseInt(fields.get(2))), new WindowId(fields.get(1))); - if (server.version(snapshot).equals(BREAK_PANE_NAMING_BROKEN)) { + if (running.sameRelease(BREAK_PANE_NAMING_BROKEN)) { // 3.7 took the name and ignored it, so the caller's choice is applied afterwards. wanted.ifPresent(name -> server.run( snapshot, List.of("rename-window", "-t", fields.get(1), "--", TmuxFormats.literal(name)))); @@ -958,10 +953,6 @@ public void paste(String text) { if (text.indexOf('\0') >= 0) { throw new IllegalArgumentException("pasted text cannot contain NUL"); } - TmuxVersion running = server.version(snapshot); - if (!running.atLeast(Buffers.EXACT_NAMED_DELETE)) { - throw new UnsupportedFeatureException("pasting text", Buffers.EXACT_NAMED_DELETE, running); - } String buffer = "libtmux-paste-" + UUID.randomUUID(); // Recorded before dispatch: a wait already watching this pane must never see the pasted // content on screen before the record that discounts it exists. Rolled back below if the @@ -981,7 +972,8 @@ public void paste(String text) { "-b", buffer, "-t", - state.id().value()))); + state.id().value())), + true); } catch (RuntimeException failure) { settleFailure(recorded, failure); try { @@ -1011,25 +1003,23 @@ public void paste(String text, Runnable beforePaste) { if (text.indexOf('\0') >= 0) { throw new IllegalArgumentException("pasted text cannot contain NUL"); } - TmuxVersion running = server.version(snapshot); - if (!running.atLeast(Buffers.EXACT_NAMED_DELETE)) { - throw new UnsupportedFeatureException("pasting text", Buffers.EXACT_NAMED_DELETE, running); - } String buffer = "libtmux-paste-" + UUID.randomUUID(); try { - server.runTogether(snapshot, text, List.of(List.of("load-buffer", "-b", buffer, "-"))); + server.runTogether(snapshot, text, List.of(List.of("load-buffer", "-b", buffer, "-")), true); beforePaste.run(); PaneEcho.Recorded recorded = server.echo().recordLiteral(identity(), state.id(), text); try { - server.run( + server.runTogether( snapshot, - List.of( + "", + List.of(List.of( "paste-buffer", "-d", "-b", buffer, "-t", - state.id().value())); + state.id().value())), + true); } catch (RuntimeException failure) { settleFailure(recorded, failure); throw failure; diff --git a/libtmux/src/main/java/io/github/libtmux/Prompt.java b/libtmux/src/main/java/io/github/libtmux/Prompt.java index 489de75c4..205e37a5b 100644 --- a/libtmux/src/main/java/io/github/libtmux/Prompt.java +++ b/libtmux/src/main/java/io/github/libtmux/Prompt.java @@ -2,59 +2,28 @@ import io.github.libtmux.catalog.Kind; import io.github.libtmux.catalog.Operation; -import io.github.libtmux.exception.UnsupportedFeatureException; import java.util.List; import kotlin.annotations.jvm.ReadOnly; -/** - * The server's command-prompt history. - * - *

Whether the commands exist is asked of the daemon, not inferred from a version string. - */ +/** The server's command-prompt history. */ public final class Prompt { - /** The release a refusal names. The daemon's command list is what actually decides. */ - private static final TmuxVersion SINCE = new TmuxVersion(3, 3, ""); - private final Server server; Prompt(Server server) { this.server = server; } - /** - * What has been typed at the prompt, oldest first. - * - * @throws UnsupportedFeatureException if this tmux has no such command - */ + /** What has been typed at the prompt, oldest first. */ @ReadOnly @Operation(Kind.READ) public List history() { - require(); return server.run(List.of("show-prompt-history")).stdout(); } - /** - * Forgets what has been typed at the prompt. - * - * @throws UnsupportedFeatureException if this tmux has no such command - */ + /** Forgets what has been typed at the prompt. */ @Operation(Kind.MUTATION) public void clear() { - require(); server.run(List.of("clear-prompt-history")); } - - private void require() { - if (server.commands().list().stream().anyMatch(line -> named(line, "show-prompt-history"))) { - return; - } - throw new UnsupportedFeatureException("the command prompt's history", SINCE, server.version()); - } - - /** Whether a {@code list-commands} line names this command, not one of its aliases. */ - private static boolean named(String line, String name) { - int space = line.indexOf(' '); - return (space < 0 ? line : line.substring(0, space)).equals(name); - } } diff --git a/libtmux/src/main/java/io/github/libtmux/Server.java b/libtmux/src/main/java/io/github/libtmux/Server.java index cfe53f25e..72e58fcd0 100644 --- a/libtmux/src/main/java/io/github/libtmux/Server.java +++ b/libtmux/src/main/java/io/github/libtmux/Server.java @@ -16,6 +16,8 @@ import io.github.libtmux.format.RowFormat; import io.github.libtmux.internal.CommandStrings; import io.github.libtmux.internal.ErrorText; +import io.github.libtmux.internal.TmuxFeatures; +import io.github.libtmux.internal.support.Versions; import io.github.libtmux.query.FilterExpr; import io.github.libtmux.query.TmuxFilters; import io.github.libtmux.snapshot.ServerSnapshot; @@ -119,7 +121,7 @@ SnapshotCapture capture() { * already carries the same name. * *

Keep {@code :} and {@code .} out of the name. tmux decides what to do with them and changes - * its mind across the supported range: 3.2a through 3.6 rewrite each one to {@code _}, 3.7 + * its mind across the supported range: 3.3a through 3.6 rewrite each one to {@code _}, 3.7 * refuses the name, and 3.7a onwards keeps it — where a bare {@code -t} target still misreads the * delimiter, so {@link #hasSession} and {@link #killSession} resolve the id by comparing names * instead of building one. {@link Session#name()} reports what tmux settled on. @@ -153,7 +155,7 @@ public Session newSession(Consumer configure) { */ @Operation(Kind.MUTATION) public Session newSession(SessionSpec spec) { - CommandResult result = run(spec.argv("#{session_id}", () -> SessionCreation.versionForCreation(this))); + CommandResult result = run(spec.argv("#{session_id}")); List reported = result.stdout(); if (reported.isEmpty()) { throw new MalformedResponseException(SessionCreation.failureMessage(config, result)); @@ -213,7 +215,13 @@ public boolean isAlive() { */ @Operation(Kind.READ) public boolean isAlive(Duration timeout) { - return cmd(List.of("display-message", "-p", "#{pid}"), timeout).succeeded(); + try { + return cmd(List.of("display-message", "-p", "#{pid}"), timeout).succeeded(); + } catch (CommandRejectedException refused) { + // The version check asks tmux for its version first, and tmux refusing that is the same answer. + // A daemon that exits under the probe is the common case. + return false; + } } /** @@ -310,7 +318,7 @@ public void killServer() { */ @Operation(Kind.MUTATION) public void killServer(Duration timeout) { - CommandResult result = transport.execute( + CommandResult result = execute( request(List.of(List.of("display-message", "-p", "#{pid}"), List.of("kill-server")), timeout, "")); if (result.succeeded()) { // Only a process transport runs tmux on this host; another transport's pid belongs to @@ -320,6 +328,9 @@ public void killServer(Duration timeout) { } return; } + if (Versions.absent(result)) { + return; + } if (!isAlive(timeout)) { return; } @@ -385,7 +396,18 @@ private static void awaitExit(List reported, Duration timeout) { */ @Operation(Kind.CAPTURED) public Batch batch() { - return new Batch(commands -> transport.execute(request(commands, config.defaultTimeout(), ""))); + return new Batch(commands -> execute(request(commands, config.defaultTimeout(), ""))); + } + + /** + * Collects a group that may wait for a release through this server's transport. + * + *

The transport reserves admission capacity for ordinary release commands. Encoding and + * positional attribution match {@link #batch()}. + */ + @Operation(Kind.CAPTURED) + public Batch batchReservingCapacity() { + return new Batch(commands -> executeWaiting(request(commands, config.defaultTimeout(), ""))); } /** @@ -396,7 +418,13 @@ public Batch batch() { */ @Operation(Kind.CAPTURED) public CommandChain chain() { - return new CommandChain(batch(), () -> SessionCreation.versionForCreation(this)); + return new CommandChain(batch(), this::versionForCreation); + } + + /** Starts an implicit-target chain while reserving transport capacity for its release. */ + @Operation(Kind.CAPTURED) + public CommandChain chainReservingCapacity() { + return new CommandChain(batchReservingCapacity(), this::versionForCreation); } /** @@ -424,8 +452,8 @@ public String expand(String format) { CommandResult printed(@Nullable ServerSnapshot snapshot, List argv) { List> group = PrintedText.framed(argv); CommandResult result = snapshot == null - ? transport.execute(request(group, config.defaultTimeout(), "")) - : fence.guarded(snapshot, CommandStrings.group(group)); + ? execute(request(group, config.defaultTimeout(), "")) + : fence.guarded(snapshot, CommandStrings.group(group), "", TmuxFeatures.Intent.literal(group)); return PrintedText.unwrap(result); } @@ -471,8 +499,7 @@ public Prompt prompt() { CommandResult withoutStartingServer(List command) { List argv = new ArrayList<>(config.endpointCommand()); argv.add(1, "-N"); - CommandResult result = - transport.execute(new CommandRequest(argv, List.of(command), config.defaultTimeout(), "")); + CommandResult result = execute(new CommandRequest(argv, List.of(command), config.defaultTimeout(), "")); if (result.succeeded()) { return result; } @@ -568,9 +595,9 @@ WakeReason awaitChannel(String channel, Duration timeout, boolean reserveSignalC try { CommandRequest request = request(List.of("wait-for", "--", channel), timeout); if (reserveSignalCapacity) { - transport.executeWaiting(request); + executeWaiting(request); } else { - transport.execute(request); + execute(request); } } catch (DispatchException.TimedOut e) { if (Thread.interrupted()) { @@ -670,6 +697,24 @@ public TmuxVersion version() { .orElseThrow(() -> new ServerUnavailableException("no tmux server is answering on this endpoint")); } + /** + * Requires a compatible daemon, or a compatible executable when this endpoint is unused. + * + *

Starts no daemon. This checks compatibility at call time; every later operation checks + * the daemon's version again. + * + * @throws UnsupportedFeatureException if the selected tmux is older than the core minimum + * @throws MalformedResponseException if tmux reports an unrecognized version + */ + @Operation(Kind.READ) + public void requireCompatibleTmux() { + versionForCreation(); + } + + TmuxVersion versionForCreation() { + return Versions.versionForCreation(transport, request(List.of("-V"), config.defaultTimeout())); + } + TmuxVersion version(ServerSnapshot snapshot) { return snapshot.serverVersion().orElseGet(this::version); } @@ -780,7 +825,22 @@ public CommandResult cmd(List argv, Duration timeout) { /** Package-private so {@link IncarnationFence} can send its guard with the caller's input. */ CommandResult cmd(List argv, Duration timeout, String input) { - return transport.execute(request(List.of(argv), timeout, input)); + return execute(request(List.of(argv), timeout, input)); + } + + CommandResult cmd(List argv, Duration timeout, String input, TmuxFeatures.Intent intent) { + requireOpen(); + return Versions.execute(transport, request(List.of(argv), timeout, input), false, intent); + } + + private CommandResult execute(CommandRequest request) { + requireOpen(); + return Versions.execute(transport, request, false); + } + + private CommandResult executeWaiting(CommandRequest request) { + requireOpen(); + return Versions.execute(transport, request, true); } private CommandRequest request(List argv, Duration timeout) { @@ -831,31 +891,17 @@ public ControlClient control(Session session, Duration timeout) { } requireOpen(); ServerSnapshot captured = session.snapshot(); + // Before the identity read below, so an unsupported daemon is refused without running anything on it. + // The hook-free probe, not version(captured): an unversioned capture would read it with display-message. + TmuxVersion daemon = captured.serverVersion().orElseGet(this::versionForCreation); + io.github.libtmux.internal.ControlAcquisition.requireSupported(daemon); long pid = captured.serverPid() .orElseThrow(() -> new IllegalStateException("a live handle has no server process identity")); - CommandResult live = fence.guarded(captured, "display-message -p '#{pid} #{version}'", ""); - if (!live.succeeded() || live.stdout().size() != 1) { - throw failed("display-message", live); - } - String reported = live.stdout().get(0); - int space = reported.indexOf(' '); - if (space < 1) { - throw new MalformedResponseException("tmux reported a malformed server identity"); - } - long seen; - try { - seen = Long.parseLong(reported.substring(0, space)); - } catch (NumberFormatException e) { - throw new MalformedResponseException("tmux reported a malformed server pid"); - } - if (seen != pid) { - throw new TargetGoneException("the tmux server this handle belonged to has ended"); - } ControlCarrier carrier = transport .controlCarrier() .orElseThrow(() -> new IllegalStateException("this transport does not start control clients")); return ControlClient.attach( - carrier, config, session.id(), pid, captured.serverStartTime(), reported.substring(space + 1), timeout); + carrier, config, session.id(), pid, captured.serverStartTime(), daemon.toString(), timeout); } /** @@ -909,6 +955,20 @@ public List sessions(FilterExpr expression) { .toList(); } + /** + * Captures metadata and entity handles from one observed hierarchy. + * + *

Accessing the returned view or its captured relations issues no commands. Acquisition + * uses the same consistency checks and replacement retry as {@link #snapshot()}. + * + * @throws ServerNotRunningException if no daemon is running + * @throws LibTmuxException if the capture otherwise fails + */ + @Operation(Kind.READ) + public CapturedServer captureServer() { + return new CapturedServer(this, snapshot()); + } + /** * Every session, captured now. * @@ -1227,12 +1287,13 @@ public CommandResult run(List argv) { } CommandResult cmd(ServerSnapshot snapshot, List argv) { - return fence.guarded(snapshot, CommandStrings.stringify(argv)); + return fence.guarded(snapshot, CommandStrings.stringify(argv), "", TmuxFeatures.Intent.literal(List.of(argv))); } /** A batch whose every command is refused once this handle's server has been replaced. */ Batch batch(ServerSnapshot snapshot) { - return new Batch(commands -> fence.guarded(snapshot, CommandStrings.group(commands), "")); + return new Batch(commands -> + fence.guarded(snapshot, CommandStrings.group(commands), "", TmuxFeatures.Intent.literal(commands))); } /** @@ -1244,7 +1305,8 @@ Batch batch(ServerSnapshot snapshot) { */ Batch batch(long pid, long startTime) { String condition = IncarnationFence.incarnation(pid, java.util.OptionalLong.of(startTime)); - return new Batch(commands -> fence.guarded(pid, condition, CommandStrings.group(commands), "")); + return new Batch(commands -> fence.guarded( + pid, condition, CommandStrings.group(commands), "", TmuxFeatures.Intent.literal(commands))); } CommandResult run(ServerSnapshot snapshot, List argv) { @@ -1268,7 +1330,16 @@ CommandResult runTogether(ServerSnapshot snapshot, List> commands) /** As {@link #runTogether}, with {@code input} on tmux's standard input for the group to read. */ CommandResult runTogether(ServerSnapshot snapshot, String input, List> commands) { - CommandResult result = fence.guarded(snapshot, CommandStrings.group(commands), input); + return runTogether(snapshot, input, commands, false); + } + + CommandResult runTogether( + ServerSnapshot snapshot, String input, List> commands, boolean namedBufferCleanup) { + CommandResult result = fence.guarded( + snapshot, + CommandStrings.group(commands), + input, + new TmuxFeatures.Intent(commands, false, namedBufferCleanup)); if (!result.succeeded()) { String verbs = commands.stream().map(argv -> argv.get(0)).collect(Collectors.joining(" then ")); throw failed(verbs, result); @@ -1276,6 +1347,16 @@ CommandResult runTogether(ServerSnapshot snapshot, String input, List> commands, TmuxVersion version, boolean named) { + long pid = snapshot.serverPid().orElseThrow(); + String condition = "#{&&:" + IncarnationFence.incarnation(pid, snapshot.serverStartTime()) + ",#{==:#{version}," + + version + "}}"; + CommandResult result = fence.guarded( + pid, condition, CommandStrings.group(commands), "", new TmuxFeatures.Intent(commands, named, false)); + if (!result.succeeded()) throw failed(commands.getFirst().getFirst(), result); + return result; + } + /** As {@link #run(ServerSnapshot, List)}, refused as well once the window has been replaced. */ CommandResult run(ServerSnapshot snapshot, WindowContext expected, List argv) { return fence.run(snapshot, expected, argv); diff --git a/libtmux/src/main/java/io/github/libtmux/SessionCreation.java b/libtmux/src/main/java/io/github/libtmux/SessionCreation.java index b28423988..b6074b1a5 100644 --- a/libtmux/src/main/java/io/github/libtmux/SessionCreation.java +++ b/libtmux/src/main/java/io/github/libtmux/SessionCreation.java @@ -1,13 +1,8 @@ package io.github.libtmux; -import io.github.libtmux.exception.LibTmuxException; -import io.github.libtmux.exception.MalformedResponseException; -import io.github.libtmux.exception.ServerUnavailableException; -import io.github.libtmux.internal.ErrorText; import io.github.libtmux.transport.CommandResult; import java.nio.file.Files; import java.nio.file.Path; -import java.util.List; /** Assembles the pieces {@link Server#newSession(SessionSpec)} needs before tmux has a session yet. */ final class SessionCreation { @@ -38,48 +33,4 @@ static String failureMessage(ServerConfig config, CommandResult result) { } return config.binary() + " exited 0 and reported no session id; is it tmux?"; } - - /** - * The version to gate a session-creation spec against. - * - *

{@code new-session} may be the first command said to a fresh socket, where {@link - * Server#version()} fails for want of a daemon. So this asks a running daemon when one answers, - * since it may be a different build than the configured binary, and otherwise asks the binary - * through {@link #binaryVersion}. - * - *

Reads {@link SnapshotCapture#processForCreation()} rather than {@link Server#version()}: a - * daemon too old to talk to fails the identity probe as one never started does ("server exited - * unexpectedly"), and {@link ServerUnavailableException} does not keep that distinction. - * Falling back to {@code binaryVersion} for such a daemon would report the binary's version as - * the daemon's. - */ - static TmuxVersion versionForCreation(Server server) { - return server.capture() - .processForCreation() - .map(SnapshotCapture.ServerProcess::version) - .orElseGet(() -> binaryVersion(server)); - } - - /** - * Which tmux the configured binary is, without asking any server. - * - *

{@code -V} is handled before tmux touches a socket at all, so it answers even against an - * endpoint nothing is listening on yet, with or without a {@code -f} pointed at a config file - * that does not exist. - * - * @throws LibTmuxException if the binary could not be run or did not report a version - */ - private static TmuxVersion binaryVersion(Server server) { - CommandResult result = server.cmd(List.of("-V")); - if (!result.succeeded() || result.stdout().isEmpty()) { - throw new MalformedResponseException( - "tmux -V did not report a version" + ErrorText.suffix(result.stderr())); - } - String reported = result.stdout().get(0); - int space = reported.indexOf(' '); - if (space < 0) { - throw new MalformedResponseException("tmux -V reported an unexpected line: " + reported); - } - return TmuxVersion.parse(reported.substring(space + 1)); - } } diff --git a/libtmux/src/main/java/io/github/libtmux/SessionSpec.java b/libtmux/src/main/java/io/github/libtmux/SessionSpec.java index b362d1c6b..383799691 100644 --- a/libtmux/src/main/java/io/github/libtmux/SessionSpec.java +++ b/libtmux/src/main/java/io/github/libtmux/SessionSpec.java @@ -1,6 +1,5 @@ package io.github.libtmux; -import io.github.libtmux.exception.UnsupportedFeatureException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; @@ -9,7 +8,6 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.function.Supplier; import kotlin.annotations.jvm.ReadOnly; import org.jspecify.annotations.Nullable; @@ -28,14 +26,6 @@ */ public final class SessionSpec { - /** - * 3.2a accepts {@code -x}/{@code -y} for a detached session and then ignores them; upstream's - * {@code df3fe2aa} ("Only use client for sizing when not detached") is the fix, and it is - * already in tag 3.3 - absent from 3.2a, and {@code git log 3.3..3.3a} touches nothing under - * {@code spawn.c} or {@code cmd-new-session.c} - so the floor is 3.3, not 3.3a. - */ - private static final TmuxVersion SIZE_SINCE = new TmuxVersion(3, 3, ""); - private final @Nullable String name; private final @Nullable Path directory; private final Map environment; @@ -96,7 +86,7 @@ public Optional windowName() { return Optional.ofNullable(windowName); } - /** How big the session is, or empty for tmux's default. Requires tmux 3.3. */ + /** How big the session is, or empty for tmux's default. */ public Optional size() { return Optional.ofNullable(size); } @@ -123,24 +113,8 @@ public List command() { return command; } - /** - * The command that makes this session, reporting it through {@code format}. - * - *

The version arrives as a supplier because this is the one creating call that may run when - * there is no server yet, and asking a socket with nothing behind it for its version fails. - * Nothing asks unless the spec depends on the answer. - * - * @param format the row format the caller will read the result back with - * @param running what the server about to run this is, asked only if it matters - * @throws UnsupportedFeatureException if the spec asks for something {@code running} does not have - */ - List argv(String format, Supplier running) { - if (size != null) { - TmuxVersion version = running.get(); - if (!version.atLeast(SIZE_SINCE)) { - throw new UnsupportedFeatureException("a size for a detached session", SIZE_SINCE, version); - } - } + /** The command that makes this session, reporting it through {@code format}. */ + List argv(String format) { List argv = new ArrayList<>(24); argv.add("new-session"); // Always: without -d, tmux tries to attach and fails outright with no terminal. diff --git a/libtmux/src/main/java/io/github/libtmux/SnapshotCapture.java b/libtmux/src/main/java/io/github/libtmux/SnapshotCapture.java index 53dc30f69..7ecfcb932 100644 --- a/libtmux/src/main/java/io/github/libtmux/SnapshotCapture.java +++ b/libtmux/src/main/java/io/github/libtmux/SnapshotCapture.java @@ -124,22 +124,6 @@ Optional process() { return identity(Server::serverAbsent); } - /** - * As {@link #process}, except a live daemon this client cannot actually talk to is never folded - * into "no daemon" - used only by {@link SessionCreation#versionForCreation}, the one caller for which - * that distinction matters. - * - *

tmux reports both the same way from a client's side: "no server running"/"(No such file or - * directory)" for a socket nothing is listening on, and "server exited unexpectedly" for a - * daemon that refused this client's handshake - confirmed against the matrix, a 3.2a client - * against a 3.7c daemon on the same socket fails the second way, rc 1. {@link #process} still - * folds both into "no daemon" for every other caller, which asks "is there a server I can read - * from" and for which a daemon this client cannot use is no more usable than none at all. - */ - Optional processForCreation() { - return identity(SnapshotCapture::daemonGenuinelyAbsent); - } - private Optional identity(Predicate absent) { CommandResult result = server.cmd("display-message", "-p", PROCESS.template()); if (!result.succeeded()) { @@ -169,10 +153,6 @@ private Optional identity(Predicate absent) { return Optional.of(new ServerProcess(pid, TmuxVersion.parse(version), started)); } - private static boolean daemonGenuinelyAbsent(String message) { - return message.contains("no server running") || message.contains("(No such file or directory)"); - } - /** The whole hierarchy in one invocation, fenced against the identity just read. */ private ServerSnapshot capture(ServerProcess process) { boolean floatingKnown = process.version().atLeast(FLOATING_SINCE); @@ -334,7 +314,7 @@ private Optional kept( * *

The filter loops each listed row's own session, so every listing keeps whole sessions, and a * window linked into two sessions brings both. tmux evaluates this loop inside {@code -f} the same - * way on every release from 3.2a; see {@code docs/decisions/0005-pushdown-lowering-is-exact-or-refused.md}. + * way on every supported release; see {@code docs/decisions/0005-pushdown-lowering-is-exact-or-refused.md}. */ Optional sessionsHolding(PaneId id) { ServerProcess process = process() diff --git a/libtmux/src/main/java/io/github/libtmux/TmuxFormats.java b/libtmux/src/main/java/io/github/libtmux/TmuxFormats.java index 024c2cb7d..a7aa716d8 100644 --- a/libtmux/src/main/java/io/github/libtmux/TmuxFormats.java +++ b/libtmux/src/main/java/io/github/libtmux/TmuxFormats.java @@ -57,12 +57,14 @@ public static String literal(String value) { * tmux printed it. */ static String printed(String line, TmuxVersion version) { - return version.equals(DOLLAR_ESCAPED) ? ESCAPED_DOLLAR.matcher(line).replaceAll("") : line; + return version.sameRelease(DOLLAR_ESCAPED) + ? ESCAPED_DOLLAR.matcher(line).replaceAll("") + : line; } /** As {@link #printed(String, TmuxVersion)}, for every line. */ static java.util.List printed(java.util.List lines, TmuxVersion version) { - return version.equals(DOLLAR_ESCAPED) + return version.sameRelease(DOLLAR_ESCAPED) ? lines.stream().map(line -> printed(line, version)).toList() : lines; } diff --git a/libtmux/src/main/java/io/github/libtmux/TmuxVersion.java b/libtmux/src/main/java/io/github/libtmux/TmuxVersion.java index d46175302..21821528a 100644 --- a/libtmux/src/main/java/io/github/libtmux/TmuxVersion.java +++ b/libtmux/src/main/java/io/github/libtmux/TmuxVersion.java @@ -16,13 +16,14 @@ * nothing later's patches guarantee, so it sorts strictly between the two — above every patch of the * release before it, below the release it names and every patch of that. * - *

A release candidate, {@code 3.8-rc}, compares equal to the release it names. tmux freezes - * features at the candidate, so it has everything the release will and differs only in fixes — - * measured, not assumed: a 3.8 candidate writes JSON layouts and empties a dead pane's pid exactly - * as 3.8 does, and a feature gated on 3.8 works on it. It is not {@code equals} to the release, and - * it is written back exactly as tmux wrote it, which matters beyond display: every capture is - * fenced on the version tmux reports, and a candidate printed as {@code 3.8} failed that fence on - * every read. + *

A release candidate, {@code 3.8-rc}, orders below the release it names ({@code next-3.8 < + * 3.8-rc < 3.8-rc2 < 3.8 < 3.8a}), so {@link #compareTo} agrees with {@link #equals}. tmux freezes + * features at the candidate, so a feature gate asks {@link #atLeast}, which counts a candidate as its + * release: a 3.8 candidate writes JSON layouts and empties a dead pane's pid exactly as 3.8 does. A + * gate on one release's own bug asks {@link #sameRelease}. A support floor names a release and uses + * the strict order. The version is written back exactly as tmux wrote it, which matters beyond + * display: every capture is fenced on the version tmux reports, and a candidate printed as {@code + * 3.8} failed that fence on every read. * * @param major the major number * @param minor the minor number @@ -35,7 +36,7 @@ public record TmuxVersion(int major, int minor, String patch, boolean development, String preRelease) implements Comparable { - private static final Pattern RELEASE = Pattern.compile("^(next-)?(\\d+)\\.(\\d+)([a-z]*)(?:-(rc\\d*))?"); + private static final Pattern RELEASE = Pattern.compile("(next-)?(\\d+)\\.(\\d+)([a-z]?)(?:-(rc\\d*))?"); public TmuxVersion { Objects.requireNonNull(patch, "patch"); @@ -61,7 +62,7 @@ public TmuxVersion(int major, int minor, String patch) { */ public static TmuxVersion parse(String reported) { Matcher release = RELEASE.matcher(reported); - if (!release.find()) { + if (!release.matches()) { throw new IllegalArgumentException("not a tmux version: " + reported); } return new TmuxVersion( @@ -72,13 +73,36 @@ public static TmuxVersion parse(String reported) { release.group(5) == null ? "" : release.group(5)); } - /** Whether this version has everything the given one has. */ + /** + * Whether this version has everything the given one has. + * + *

A release candidate counts as its release here, because a feature gate asks what the build + * ships. That is the one place this differs from {@link #compareTo}, which orders a candidate + * below its release. + */ public boolean atLeast(TmuxVersion other) { - return compareTo(other) >= 0; + return compare(other, false) >= 0; + } + + /** + * Whether this is the named release, a release candidate for it included. {@code next-} builds + * and other releases are not. For a gate on one release's own bug. + */ + public boolean sameRelease(TmuxVersion other) { + return major == other.major + && minor == other.minor + && patch.equals(other.patch) + && !development + && !other.development; } + /** {@code next-X.Y < X.Y-rc < X.Y-rcN < X.Y < X.Ya}, so it agrees with {@link #equals}. */ @Override public int compareTo(TmuxVersion other) { + return compare(other, true); + } + + private int compare(TmuxVersion other, boolean candidates) { int byMajor = Integer.compare(major, other.major); if (byMajor != 0) { return byMajor; @@ -89,12 +113,27 @@ public int compareTo(TmuxVersion other) { } if (development != other.development) { // A next-M.m build has not shipped everything M.m's own patches will carry, so it - // sorts below the release it names, whatever either side's patch letter is. A release - // candidate is not ordered apart from its release at all: features are frozen at it. + // sorts below the release it names, whatever either side's patch letter is. return development ? -1 : 1; } // An unlettered release precedes its own patches, which empty-string ordering already gives. - return patch.compareTo(other.patch); + int byPatch = patch.compareTo(other.patch); + if (byPatch != 0 || !candidates) { + return byPatch; + } + return Integer.compare(candidate(preRelease), candidate(other.preRelease)); + } + + /** A release outranks every candidate; a bare {@code rc} precedes {@code rc1}, {@code rc2}, ... */ + private static int candidate(String preRelease) { + if (preRelease.isEmpty()) { + return Integer.MAX_VALUE; + } + try { + return preRelease.length() <= 2 ? 0 : Math.max(1, Integer.parseInt(preRelease.substring(2))); + } catch (NumberFormatException unnumbered) { + return 0; + } } @Override diff --git a/libtmux/src/main/java/io/github/libtmux/catalog/Operation.java b/libtmux/src/main/java/io/github/libtmux/catalog/Operation.java index 162ac39bd..80343f21b 100644 --- a/libtmux/src/main/java/io/github/libtmux/catalog/Operation.java +++ b/libtmux/src/main/java/io/github/libtmux/catalog/Operation.java @@ -23,7 +23,7 @@ /** * The first tmux release that has this operation, when it is newer than the supported floor, - * 3.2a. Empty when the whole supported range has it. + * 3.3a. Empty when the whole supported range has it. */ String tmuxSince() default ""; } diff --git a/libtmux/src/main/java/io/github/libtmux/control/ControlClient.java b/libtmux/src/main/java/io/github/libtmux/control/ControlClient.java index 2268dee88..8dc697e07 100644 --- a/libtmux/src/main/java/io/github/libtmux/control/ControlClient.java +++ b/libtmux/src/main/java/io/github/libtmux/control/ControlClient.java @@ -3,6 +3,7 @@ import io.github.libtmux.PaneId; import io.github.libtmux.ServerConfig; import io.github.libtmux.SessionId; +import io.github.libtmux.TmuxVersion; import io.github.libtmux.batch.OperationOutcome; import io.github.libtmux.catalog.Kind; import io.github.libtmux.catalog.Operation; @@ -10,10 +11,11 @@ import io.github.libtmux.exception.ControlEndedException; import io.github.libtmux.exception.DispatchException; import io.github.libtmux.exception.LibTmuxException; -import io.github.libtmux.exception.MalformedResponseException; -import io.github.libtmux.exception.TargetGoneException; +import io.github.libtmux.internal.ControlAcquisition; import io.github.libtmux.internal.ErrorText; +import io.github.libtmux.internal.LocalSocketPin; import io.github.libtmux.internal.ProcessTree; +import io.github.libtmux.internal.TmuxFeatures; import io.github.libtmux.internal.Utf8; import io.github.libtmux.transport.ControlCarrier; import io.github.libtmux.transport.DispatchOutcome; @@ -70,13 +72,24 @@ public final class ControlClient implements AutoCloseable { private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(30); - private static final ControlCarrier LOCAL = command -> new ProcessBuilder(command).start(); + private static final ControlCarrier LOCAL = new ControlCarrier() { + @Override + public Process start(List command) throws IOException { + return new ProcessBuilder(command).start(); + } + + @Override + public PinnedSocket pinSocket(String socketPath, Duration timeout) throws IOException { + return LocalSocketPin.retain(socketPath); + } + }; /** The same seam as the process transport's, since this client starts its own process. */ private static final System.Logger LOG = System.getLogger(ControlClient.class.getName()); private static final long EXIT_MILLIS = 5_000; + private final ControlAcquisition acquisition; private final Process process; private final ProcessTree processTree; private final InputStream standardOutput; @@ -103,7 +116,8 @@ public final class ControlClient implements AutoCloseable { private int stderrLength; private boolean stderrTruncated; - private ControlClient(Process process, OperationObserver observer) { + private ControlClient(Process process, OperationObserver observer, ControlAcquisition acquisition) { + this.acquisition = acquisition; this.process = process; this.observer = observer; this.processTree = new ProcessTree(process); @@ -153,9 +167,7 @@ public static ControlClient attachUnfenced(ServerConfig config, SessionId sessio */ @Operation(Kind.LIFECYCLE) public static ControlClient attachUnfenced(ServerConfig config, SessionId session, Duration timeout) { - ControlClient client = connect(LOCAL, config, session, timeout); - client.finishAttach(session); - return client; + return connect(LOCAL, config, session, timeout, null); } /** @@ -193,93 +205,70 @@ public static ControlClient attach( if (serverPid < 1) { throw new IllegalArgumentException("serverPid is not positive: " + serverPid); } - ControlClient client = connect(carrier, config, session, timeout); - try { - client.confirmIncarnation(serverPid, serverStartTime, serverVersion); - } catch (RuntimeException failure) { - client.closeAfterFailure(failure); - throw failure; - } - client.finishAttach(session); - return client; + return connect(carrier, config, session, timeout, new Expected(serverPid, serverStartTime, serverVersion)); } + private record Expected(long pid, java.util.OptionalLong start, String version) {} + private static ControlClient connect( - ControlCarrier carrier, ServerConfig config, SessionId session, Duration timeout) { + ControlCarrier carrier, + ServerConfig config, + SessionId session, + Duration timeout, + @org.jspecify.annotations.Nullable Expected expected) { Objects.requireNonNull(carrier, "carrier"); Objects.requireNonNull(config, "config"); Objects.requireNonNull(session, "session"); Objects.requireNonNull(timeout, "timeout"); - if (timeout.isZero() || timeout.isNegative()) { - throw new IllegalArgumentException("timeout is not positive"); - } - List command = new ArrayList<>(config.endpointCommand()); - command.addAll(List.of("-C", "attach-session", "-t", session.value())); - // The same guard CommandRequest applies to every other process this library starts. A - // control client's own commands travel as UTF-8 over its standard input and are unaffected, - // but this argv is encoded by the JVM like any other. - Utf8.requireEncodableArguments(command); - Process process; + if (timeout.isZero() || timeout.isNegative()) throw new IllegalArgumentException("timeout is not positive"); + ControlAcquisition acquisition = ControlAcquisition.acquire(carrier, config, timeout); try { - process = carrier.start(command); - } catch (IOException e) { - throw new DispatchException.Failed("could not start a control client", DispatchOutcome.NOT_DISPATCHED, e); - } - ControlClient client = new ControlClient(process, config.observer()); - // Attaching produces a reply of its own. It is awaited like any other, which is also what - // proves the client is up before the first command is written. - ControlWriter.Request attached = client.writer.expectInitial(timeout); - client.reader.start(); - ControlReply reply; - try { - reply = client.writer.await(attached); - } catch (DispatchException e) { - client.closeAfterFailure(e); - throw e; - } - if (reply.outcome() != OperationOutcome.COMPLETE) { - LibTmuxException failure = new CommandRejectedException( - "the control client did not become ready" + ErrorText.suffix(reply.lines()), - "attach-session", - -1, - reply.lines()); - client.closeAfterFailure(failure); + if (expected != null) acquisition.verify(expected.pid(), expected.start(), expected.version()); + List command = new ArrayList<>(acquisition.command()); + command.addAll(List.of("-C", "attach-session", "-t", session.value())); + Utf8.requireEncodableArguments(command); + acquisition.remaining(false); + Process process; + try { + process = carrier.start(command); + } catch (IOException failure) { + throw new DispatchException.Failed( + "could not start a control client", DispatchOutcome.NOT_DISPATCHED, failure); + } + ControlClient client = new ControlClient(process, config.observer(), acquisition); + try { + ControlWriter.Request opening = client.writer.expectInitial(acquisition.remaining(true)); + client.reader.start(); + ControlReply reply = client.writer.await(opening); + if (reply.outcome() != OperationOutcome.COMPLETE) { + throw new CommandRejectedException( + "the control client did not become ready" + ErrorText.suffix(reply.lines()), + "attach-session", + -1, + reply.lines()); + } + client.writer.start(); + // Not over the control line: run-shell -C reports through stderr, which a control reply + // does not carry, and display-message there would run the user's hooks. + acquisition.recheck(carrier); + client.requestJsonLayouts(acquisition.remaining(true)); + client.finishAttach(session); + return client; + } catch (RuntimeException failure) { + client.closeAfterFailure(failure); + throw failure; + } + } catch (RuntimeException failure) { + try { + acquisition.close(); + } catch (RuntimeException cleanup) { + failure.addSuppressed(cleanup); + } throw failure; } - client.writer.start(); - return client; - } - - /** Reads the live server and detaches, via the caller, when it is not the one that was named. */ - private void confirmIncarnation(long expectedPid, java.util.OptionalLong expectedStart, String expectedVersion) { - // The start time leads: it is a number, and the version, compared as text, may hold a space. - ControlReply reply = send("display-message", "-p", "#{start_time} #{pid} #{version}"); - if (!reply.succeeded() || reply.lines().size() != 1) { - throw new CommandRejectedException( - "could not read the control client's server" + ErrorText.suffix(reply.lines()), - "display-message", - -1, - reply.lines()); - } - String[] reported = reply.lines().get(0).split(" ", 3); - long started; - long pid; - try { - started = Long.parseLong(reported[0]); - pid = Long.parseLong(reported.length > 1 ? reported[1] : ""); - } catch (NumberFormatException e) { - throw new MalformedResponseException("tmux reported a malformed server identity"); - } - String version = reported.length > 2 ? reported[2] : ""; - if (pid != expectedPid - || !version.equals(expectedVersion) - || (expectedStart.isPresent() && started != expectedStart.getAsLong())) { - throw new TargetGoneException("the tmux server this handle belonged to has ended"); - } } private void finishAttach(SessionId session) { - requestJsonLayouts(); LOG.log(System.Logger.Level.DEBUG, "tmux control client attached to session {0}", session.value()); } @@ -294,8 +283,8 @@ private void finishAttach(SessionId session) { * new-layouts} completes with no error on every supported release, just with nothing to change * before 3.8, so this is sent unconditionally rather than gated on a version. */ - private void requestJsonLayouts() { - ControlReply reply = send("refresh-client", "-f", "new-layouts"); + private void requestJsonLayouts(Duration timeout) { + ControlReply reply = send(List.of("refresh-client", "-f", "new-layouts"), timeout); if (reply.outcome() != OperationOutcome.COMPLETE) { LibTmuxException failure = new CommandRejectedException( "could not request JSON layouts on attach" + ErrorText.suffix(reply.lines()), @@ -341,6 +330,8 @@ public ControlReply send(List argv, Duration timeout) { if (closed.get() || failed) { throw new IllegalStateException("control client is not usable"); } + TmuxFeatures.require( + TmuxVersion.parse(acquisition.identity().version()), TmuxFeatures.Intent.literal(List.of(argv))); long started = System.nanoTime(); try { ControlReply reply = writer.exchange(line(argv), timeout); @@ -477,32 +468,36 @@ public ControlReply unwatch(String name) { @Operation(Kind.LIFECYCLE) @Override public void close() { - boolean closeOwner = closed.compareAndSet(false, true); - if (closeOwner) { - processTree.captureDescendants(); - writer.close(); - closeSubscriptions(null); - } - boolean reclaimed = processTree.terminate(); - if (!closeOwner) { + try { + boolean closeOwner = closed.compareAndSet(false, true); + if (closeOwner) { + processTree.captureDescendants(); + writer.close(); + closeSubscriptions(null); + } + boolean reclaimed = processTree.terminate(); + if (!closeOwner) { + if (!reclaimed) { + throw new IllegalStateException("control process tree was not reclaimed"); + } + return; + } + AtomicBoolean interrupted = new AtomicBoolean(Thread.interrupted()); + if (!Thread.currentThread().equals(reader)) { + join(reader, EXIT_MILLIS, interrupted); + } + if (!Thread.currentThread().equals(errorReader)) { + join(errorReader, EXIT_MILLIS, interrupted); + } + join(writer, EXIT_MILLIS, interrupted); + if (interrupted.get()) { + Thread.currentThread().interrupt(); + } if (!reclaimed) { throw new IllegalStateException("control process tree was not reclaimed"); } - return; - } - AtomicBoolean interrupted = new AtomicBoolean(Thread.interrupted()); - if (!Thread.currentThread().equals(reader)) { - join(reader, EXIT_MILLIS, interrupted); - } - if (!Thread.currentThread().equals(errorReader)) { - join(errorReader, EXIT_MILLIS, interrupted); - } - join(writer, EXIT_MILLIS, interrupted); - if (interrupted.get()) { - Thread.currentThread().interrupt(); - } - if (!reclaimed) { - throw new IllegalStateException("control process tree was not reclaimed"); + } finally { + acquisition.close(); } } @@ -540,6 +535,12 @@ private void read() { } catch (IOException | ControlProtocol.LimitExceeded e) { readerFailure = e; } finally { + try { + acquisition.close(); + } catch (RuntimeException cleanup) { + if (readerFailure == null) readerFailure = cleanup; + else readerFailure.addSuppressed(cleanup); + } readerEnded = true; closeSubscriptions( readerFailure == null && closed.get() diff --git a/libtmux/src/main/java/io/github/libtmux/internal/ControlAcquisition.java b/libtmux/src/main/java/io/github/libtmux/internal/ControlAcquisition.java new file mode 100644 index 000000000..3b7f47231 --- /dev/null +++ b/libtmux/src/main/java/io/github/libtmux/internal/ControlAcquisition.java @@ -0,0 +1,204 @@ +package io.github.libtmux.internal; + +import io.github.libtmux.ServerConfig; +import io.github.libtmux.TmuxVersion; +import io.github.libtmux.exception.CommandRejectedException; +import io.github.libtmux.exception.DispatchException; +import io.github.libtmux.exception.MalformedResponseException; +import io.github.libtmux.exception.TargetGoneException; +import io.github.libtmux.exception.UnsupportedFeatureException; +import io.github.libtmux.internal.support.Versions; +import io.github.libtmux.transport.CommandRequest; +import io.github.libtmux.transport.CommandResult; +import io.github.libtmux.transport.ControlCarrier; +import io.github.libtmux.transport.DispatchOutcome; +import io.github.libtmux.transport.ProcessTransport; +import java.io.IOException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.OptionalLong; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicBoolean; + +/** Qualifies a carrier-owned endpoint before the first control effect. */ +public final class ControlAcquisition implements AutoCloseable { + private static final String FORMAT = "#{version},#{pid},#{start_time},'#{s/'/'\\\\''/:socket_path}'"; + private static final TmuxVersion MINIMUM = new TmuxVersion(3, 7, ""); + private final ControlCarrier.PinnedSocket pin; + private final Identity identity; + private final List command; + private final Duration budget; + private final long began; + private final AtomicBoolean closed = new AtomicBoolean(); + + private ControlAcquisition( + ControlCarrier.PinnedSocket pin, Identity identity, List command, Duration budget, long began) { + this.pin = pin; + this.identity = identity; + this.command = command; + this.budget = budget; + this.began = began; + } + + public static ControlAcquisition acquire(ControlCarrier carrier, ServerConfig config, Duration budget) { + long began = System.nanoTime(); + // The hook-free probe decides first: a daemon below the floor must not run a user's + // after-display-message hook for the identity read that follows. + TmuxVersion version = Versions.requireCarrier(carrier, config.endpointCommand(), budget); + requireSupported(version); + Identity identity = read(carrier, config.endpointCommand(), remaining(budget, began, false)); + if (!identity.version().equals(version.toString())) { + throw new TargetGoneException("the tmux endpoint changed during control acquisition"); + } + ControlCarrier.PinnedSocket pin; + try { + pin = carrier.pinSocket(identity.socket(), remaining(budget, began, false)); + } catch (UnsupportedOperationException failure) { + throw new UnsupportedFeatureException( + "control acquisition requires endpoint pinning in the carrier's realm"); + } catch (IOException failure) { + throw new DispatchException.Failed( + "cannot retain the control socket", DispatchOutcome.NOT_DISPATCHED, failure); + } + try { + List command = new ArrayList<>(config.endpointCommand()); + int selectorEnd = command.size() - (config.configFile().isPresent() ? 2 : 0); + int selectorStart = selectorEnd - config.endpoint().flags().size(); + command.subList(selectorStart, selectorEnd).clear(); + command.addAll(selectorStart, List.of("-S", pin.socketPath())); + command.add(1, "-N"); + ControlAcquisition acquisition = new ControlAcquisition(pin, identity, List.copyOf(command), budget, began); + Identity retained = read(carrier, acquisition.command(), acquisition.remaining(false)); + if (!identity.equals(retained)) + throw new TargetGoneException("the tmux endpoint changed during control acquisition"); + return acquisition; + } catch (RuntimeException failure) { + try { + pin.close(); + } catch (IOException cleanup) { + failure.addSuppressed(cleanup); + } + throw failure; + } + } + + /** + * Whether control clients are qualified on this daemon: tmux 3.7 or newer, where a development + * or candidate build of 3.7 itself ranks below it ({@code next-3.7 < 3.7-rc < 3.7}) and + * {@code 3.8-rc2} or {@code next-3.8} rank above. + */ + public static boolean supports(TmuxVersion version) { + return version.compareTo(MINIMUM) >= 0; + } + + /** + * Refuses a daemon whose control clients are not qualified, before any control client exists. + * + * @throws UnsupportedFeatureException below tmux 3.7 + */ + public static void requireSupported(TmuxVersion version) { + if (!supports(version)) { + throw new UnsupportedFeatureException( + "managed control acquisition requires tmux 3.7 or newer; daemon reports " + version); + } + } + + /** + * Reads the daemon identity without firing a user's hooks. + * + *

As the version probe does, this uses {@code run-shell -C}: tmux parses the expansion as a + * command and names the first word it cannot parse, so every field travels in one word. The + * fields are joined with commas, and the socket path comes last inside single quotes with its own + * quotes closed, escaped and reopened, so spaces and quotes in it survive the parse. + */ + private static Identity read(ControlCarrier carrier, List command, Duration timeout) { + List endpoint = new ArrayList<>(command); + endpoint.add(1, "-N"); + String marker = "libtmux-identity-" + + Long.toHexString(ThreadLocalRandom.current().nextLong()) + "-"; + CommandResult result; + try { + result = ProcessTransport.executeThroughCarrier( + carrier, + new CommandRequest(endpoint, List.of(List.of("run-shell", "-C", marker + FORMAT)), timeout, "")); + } catch (DispatchException failure) { + DispatchException denied = failure instanceof DispatchException.TimedOut + ? new DispatchException.TimedOut( + "control identity probe timed out", DispatchOutcome.NOT_DISPATCHED, failure) + : new DispatchException.Failed( + "control identity probe failed", DispatchOutcome.NOT_DISPATCHED, failure); + throw denied; + } + String prefix = "unknown command: " + marker; + List answers = + result.stderr().stream().filter(line -> line.startsWith(prefix)).toList(); + if (result.succeeded() || answers.size() != 1 || result.stderr().size() != 1) { + throw new CommandRejectedException( + "cannot query the control endpoint" + ErrorText.suffix(result.stderr()), + "run-shell", + result.exitCode(), + result.stderr()); + } + String[] fields = answers.getFirst().substring(prefix.length()).split(",", 4); + try { + if (fields.length != 4 || fields[3].isEmpty()) throw new NumberFormatException(); + long pid = Long.parseLong(fields[1]); + long start = Long.parseLong(fields[2]); + if (pid < 1 || start < 1) throw new NumberFormatException(); + return new Identity(fields[0], pid, start, fields[3]); + } catch (NumberFormatException failure) { + throw new MalformedResponseException("tmux reported an invalid control identity", failure); + } + } + + /** Rereads the identity through the pin and refuses a daemon that is no longer the qualified one. */ + public void recheck(ControlCarrier carrier) { + if (!identity.equals(read(carrier, command, remaining(true)))) { + throw new TargetGoneException("the tmux endpoint changed during control acquisition"); + } + } + + public List command() { + return command; + } + + public Identity identity() { + return identity; + } + + public void verify(long pid, OptionalLong start, String version) { + if (identity.pid() != pid + || !identity.version().equals(version) + || (start.isPresent() && identity.start() != start.getAsLong())) { + throw new TargetGoneException("the tmux server this handle belonged to has ended"); + } + } + + public Duration remaining(boolean dispatched) { + return remaining(budget, began, dispatched); + } + + private static Duration remaining(Duration budget, long began, boolean dispatched) { + Duration left = budget.minusNanos(System.nanoTime() - began); + if (left.isZero() || left.isNegative()) { + throw new DispatchException.TimedOut( + "control acquisition exceeded its deadline", + dispatched ? DispatchOutcome.UNKNOWN : DispatchOutcome.NOT_DISPATCHED, + null); + } + return left; + } + + @Override + public void close() { + if (!closed.compareAndSet(false, true)) return; + try { + pin.close(); + } catch (IOException failure) { + throw new IllegalStateException("could not remove the owned control endpoint", failure); + } + } + + public record Identity(String version, long pid, long start, String socket) {} +} diff --git a/libtmux/src/main/java/io/github/libtmux/internal/LocalSocketPin.java b/libtmux/src/main/java/io/github/libtmux/internal/LocalSocketPin.java new file mode 100644 index 000000000..2ba84d48f --- /dev/null +++ b/libtmux/src/main/java/io/github/libtmux/internal/LocalSocketPin.java @@ -0,0 +1,95 @@ +package io.github.libtmux.internal; + +import io.github.libtmux.transport.ControlCarrier; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; + +/** A pin created only by carriers that own this filesystem. */ +public final class LocalSocketPin implements ControlCarrier.PinnedSocket { + private final Path directory; + private final Path alias; + private final AtomicBoolean closed = new AtomicBoolean(); + + private LocalSocketPin(Path directory, Path alias) { + this.directory = directory; + this.alias = alias; + } + + public static LocalSocketPin retain(String socketPath) throws IOException { + Path socket = Path.of(socketPath).toAbsolutePath(); + Map original = socketAttributes(socket); + List parents = new ArrayList<>(); + parents.add(Path.of(System.getProperty("java.io.tmpdir")).toAbsolutePath()); + Path peer = Objects.requireNonNull(socket.getParent()); + if (!parents.contains(peer)) parents.add(peer); + IOException rejected = new IOException("cannot retain a short Unix socket alias on the same filesystem"); + for (Path parent : parents) { + try { + return retainAt(parent, socket, original); + } catch (IOException failure) { + rejected.addSuppressed(failure); + } + } + throw rejected; + } + + private static LocalSocketPin retainAt(Path parent, Path socket, Map original) throws IOException { + Path directory = Files.createTempDirectory( + parent, ".lc-", PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------"))); + Path alias = directory.resolve("s"); + try { + // Darwin allows 103 pathname bytes; Linux's limit is slightly larger. + if (alias.toString().getBytes(StandardCharsets.UTF_8).length > 103) { + throw new IOException("the retained socket alias exceeds the Unix pathname limit"); + } + Files.createLink(alias, socket); + Map pinned = socketAttributes(alias); + if (!Objects.equals(original.get("dev"), pinned.get("dev")) + || !Objects.equals(original.get("ino"), pinned.get("ino")) + || !Files.isSameFile(socket, alias)) { + throw new IOException("the tmux socket changed while it was being retained"); + } + return new LocalSocketPin(directory, alias); + } catch (IOException | RuntimeException failure) { + try { + Files.deleteIfExists(alias); + Files.deleteIfExists(directory); + } catch (IOException cleanup) { + failure.addSuppressed(cleanup); + } + throw failure; + } + } + + private static Map socketAttributes(Path path) throws IOException { + Map attributes = Files.readAttributes(path, "unix:mode,dev,ino", LinkOption.NOFOLLOW_LINKS); + if (!(attributes.get("mode") instanceof Number mode) + || (mode.intValue() & 0xF000) != 0xC000 + || attributes.get("dev") == null + || attributes.get("ino") == null) { + throw new IOException("control acquisition requires a Unix socket with a stable inode"); + } + return attributes; + } + + @Override + public String socketPath() { + return alias.toString(); + } + + @Override + public void close() throws IOException { + if (!closed.compareAndSet(false, true)) return; + Files.deleteIfExists(alias); + Files.deleteIfExists(directory); + } +} diff --git a/libtmux/src/main/java/io/github/libtmux/internal/TmuxFeatures.java b/libtmux/src/main/java/io/github/libtmux/internal/TmuxFeatures.java new file mode 100644 index 000000000..fd7ed7940 --- /dev/null +++ b/libtmux/src/main/java/io/github/libtmux/internal/TmuxFeatures.java @@ -0,0 +1,168 @@ +package io.github.libtmux.internal; + +import io.github.libtmux.Layouts; +import io.github.libtmux.Names; +import io.github.libtmux.TmuxVersion; +import io.github.libtmux.exception.UnsupportedFeatureException; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** Feature boundaries for literal commands and the commands hidden by managed guards. */ +public final class TmuxFeatures { + private static final TmuxVersion EXACT_BUFFER_LOOKUP = new TmuxVersion(3, 4, ""); + private static final TmuxVersion LAYOUT_ERROR_REPORTING = new TmuxVersion(3, 4, ""); + private static final TmuxVersion SPLIT_PERCENT_BROKEN = new TmuxVersion(3, 4, ""); + private static final TmuxVersion BREAK_BROKEN = new TmuxVersion(3, 7, ""); + + private TmuxFeatures() {} + + /** Original commands remain separate from their encoded incarnation guard. */ + public record Intent(List> commands, boolean repairedBreak, boolean namedBufferCleanup) { + public Intent { + commands = commands.stream().map(List::copyOf).toList(); + } + + public static Intent literal(List> commands) { + return new Intent(commands, false, false); + } + } + + public static void require(TmuxVersion version, Intent intent) { + if (intent.namedBufferCleanup() && !version.atLeast(EXACT_BUFFER_LOOKUP)) { + throw new UnsupportedFeatureException("managed paste buffer cleanup", EXACT_BUFFER_LOOKUP, version); + } + for (List command : intent.commands()) { + if (command.isEmpty()) continue; + String verb = command.getFirst(); + if (matches(verb, "delete-buffer", "deleteb", 3)) { + if (flags(command, "b").contains('b') && !version.atLeast(EXACT_BUFFER_LOOKUP)) { + throw new UnsupportedFeatureException( + "deleting a buffer by exact name", EXACT_BUFFER_LOOKUP, version); + } + } else if (matches(verb, "set-buffer", "setb", 5)) { + Set flags = flags(command, "btn"); + if (flags.contains('b') && flags.contains('n') && !version.atLeast(EXACT_BUFFER_LOOKUP)) { + throw new UnsupportedFeatureException( + "renaming a buffer by exact name", EXACT_BUFFER_LOOKUP, version); + } + } else if (matches(verb, "split-window", "splitw", 2)) { + if (version.sameRelease(SPLIT_PERCENT_BROKEN) + && flags(command, "ceFlpsStRmxyXYL").contains('p')) { + throw new UnsupportedFeatureException( + "tmux 3.4 split-window -p is unreliable; use -l with a percentage"); + } + } else if (matches(verb, "break-pane", "breakp", 2) + && version.sameRelease(BREAK_BROKEN) + && !intent.repairedBreak()) { + throw new UnsupportedFeatureException( + "literal break-pane and unnamed breaks are unreliable on tmux 3.7; use Pane.breakOut(name) or tmux 3.7a+"); + } else if (matches(verb, "new-session", "new", 3)) { + names(version, command, "cefFnstxy", "ns"); + } else if (matches(verb, "new-window", "neww", 5)) { + names(version, command, "cenFt", "n"); + } else if (matches(verb, "rename-session", "rename", 8) || matches(verb, "rename-window", "renamew", 8)) { + renamed(version, command); + } else if (matches(verb, "select-layout", "selectl", 8) && !version.atLeast(LAYOUT_ERROR_REPORTING)) { + Set flags = new HashSet<>(); + int operand = options(command, "t", flags); + if (operand < command.size() && !flags.contains('n') && !flags.contains('p') && !flags.contains('E')) { + String layout = command.get(operand); + // 3.3a ends the server on an operand that is neither a layout name nor + // checksummed. A checksummed string is tmux's to accept or report, geometry + // included, so only the checksum is checked here. + if (!checksummed(layout)) { + Layouts.require(layout, version); + if (flags.contains('o') + && Layouts.builtIn(layout, version).isPresent()) { + throw new IllegalArgumentException( + "select-layout -o requires a layout tmux wrote: " + layout); + } + } + } + } + } + } + + /** Refuses a name flag's value that the exact release would reject; {@code wanted} lists the flags. */ + private static void names(TmuxVersion version, List command, String valued, String wanted) { + int index = 1; + for (; index < command.size(); index++) { + String word = command.get(index); + if (word.equals("--") || !word.startsWith("-") || word.equals("-")) break; + for (int offset = 1; offset < word.length(); offset++) { + char flag = word.charAt(offset); + if (valued.indexOf(flag) < 0) continue; + String value; + if (offset + 1 < word.length()) { + value = word.substring(offset + 1); + } else if (index + 1 < command.size()) { + value = command.get(++index); + } else { + return; + } + if (wanted.indexOf(flag) >= 0) Names.require(value, version); + break; + } + } + } + + /** The new name is the operand after the options of rename-session and rename-window. */ + private static void renamed(TmuxVersion version, List command) { + Set ignored = new HashSet<>(); + int operand = options(command, "t", ignored); + if (operand < command.size()) Names.require(command.get(operand), version); + } + + /** Whether the string leads with the four-digit checksum tmux computes over the rest. */ + private static boolean checksummed(String layout) { + if (layout.length() < 6 || layout.charAt(4) != ',') { + return false; + } + int declared; + try { + declared = Integer.parseInt(layout.substring(0, 4), 16); + } catch (NumberFormatException notHex) { + return false; + } + for (int index = 0; index < 4; index++) { + if (Character.digit(layout.charAt(index), 16) < 0) { + return false; + } + } + int checksum = 0; + for (int index = 5; index < layout.length(); index++) { + checksum = ((checksum >> 1) + ((checksum & 1) << 15) + layout.charAt(index)) & 0xffff; + } + return checksum == declared; + } + + private static boolean matches(String verb, String full, String alias, int minimumPrefix) { + return verb.equals(alias) || (verb.length() >= minimumPrefix && full.startsWith(verb)); + } + + /** Stops at values and --, so names such as -p or -n are never interpreted as flags. */ + private static Set flags(List command, String valued) { + Set result = new HashSet<>(); + options(command, valued, result); + return result; + } + + private static int options(List command, String valued, Set result) { + int index = 1; + for (; index < command.size(); index++) { + String word = command.get(index); + if (word.equals("--")) return index + 1; + if (!word.startsWith("-") || word.equals("-")) break; + for (int offset = 1; offset < word.length(); offset++) { + char flag = word.charAt(offset); + result.add(flag); + if (valued.indexOf(flag) >= 0) { + if (offset + 1 == word.length()) index++; + break; + } + } + } + return index; + } +} diff --git a/libtmux/src/main/java/io/github/libtmux/internal/support/Versions.java b/libtmux/src/main/java/io/github/libtmux/internal/support/Versions.java new file mode 100644 index 000000000..5edab3be4 --- /dev/null +++ b/libtmux/src/main/java/io/github/libtmux/internal/support/Versions.java @@ -0,0 +1,399 @@ +package io.github.libtmux.internal.support; + +import io.github.libtmux.TmuxVersion; +import io.github.libtmux.exception.CommandRejectedException; +import io.github.libtmux.exception.DispatchException; +import io.github.libtmux.exception.LibTmuxException; +import io.github.libtmux.exception.MalformedResponseException; +import io.github.libtmux.exception.ServerUnavailableException; +import io.github.libtmux.exception.TargetGoneException; +import io.github.libtmux.exception.UnsupportedFeatureException; +import io.github.libtmux.internal.ErrorText; +import io.github.libtmux.internal.TmuxFeatures; +import io.github.libtmux.transport.CommandRequest; +import io.github.libtmux.transport.CommandResult; +import io.github.libtmux.transport.ControlCarrier; +import io.github.libtmux.transport.DispatchOutcome; +import io.github.libtmux.transport.Idempotence; +import io.github.libtmux.transport.ProcessTransport; +import io.github.libtmux.transport.TmuxTransport; +import java.io.File; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ThreadLocalRandom; +import java.util.regex.Pattern; +import org.jspecify.annotations.Nullable; + +/** The tmux version check shared by command dispatch and control-client startup. */ +public final class Versions { + + private static final Pattern VERSION = Pattern.compile("(?:next-\\d+\\.\\d+|\\d+\\.\\d+[a-z]?(?:-rc\\d*)?)"); + private static final TmuxVersion MINIMUM = new TmuxVersion(3, 3, "a"); + private static final String PROBE_MARKER = "libtmux-version-"; + private static final String UNKNOWN_COMMAND = "unknown command: "; + private static final String CHANGED = "libtmux-stale-version"; + + private Versions() {} + + /** + * What the version check last learned about each daemon, so a call does not spend a process asking again. + * The fence on every dispatch is what keeps it honest: a daemon that has been replaced fails the + * fence before any command runs, and the entry is dropped and read again. + */ + private static final Map, String>> KNOWN = + Collections.synchronizedMap(new WeakHashMap<>()); + + /** The fence refused: the daemon is not the one checked. Nothing ran. */ + private static final class Replaced extends RuntimeException { + private static final long serialVersionUID = 1L; + + Replaced() { + super("the tmux daemon version changed before dispatch", null, false, false); + } + } + + /** Checks the live daemon before dispatch and fences the requested commands on that version. */ + public static CommandResult execute(TmuxTransport transport, CommandRequest request, boolean waiting) { + return execute(transport, request, waiting, TmuxFeatures.Intent.literal(request.commands())); + } + + /** As above, refusing first any optional feature the checked version cannot safely run. */ + public static CommandResult execute( + TmuxTransport transport, CommandRequest request, boolean waiting, TmuxFeatures.Intent intent) { + long started = System.nanoTime(); + if (request.commands().equals(List.of(List.of("-V"))) + || request.commands().equals(List.of(List.of("-h")))) { + return transport.execute(request); + } + String known = recall(transport, request); + if (known != null) { + try { + CommandResult result = dispatchChecked(transport, request, known, started, waiting, intent); + // A daemon that has gone answers without running anything, so asking again is safe. + if (!gone(result)) return result; + } catch (Replaced replaced) { + // Nothing ran; read the daemon again below. + } + forget(transport, request); + } + // A replacement between the read and the dispatch means the read was stale, and nothing ran: + // read once more. A refused version surfaces from that read as the version error it is. + for (int attempt = 0; ; attempt++) { + try { + return checkVersion(transport, request, waiting, started, intent); + } catch (Replaced replaced) { + forget(transport, request); + if (attempt == 1) { + throw new TargetGoneException("the tmux daemon version changed before dispatch"); + } + } + } + } + + private static CommandResult checkVersion( + TmuxTransport transport, + CommandRequest request, + boolean waiting, + long started, + TmuxFeatures.Intent intent) { + Probe probe = probe(transport, request); + String version; + if (probe.version().isPresent()) { + version = probe.version().get(); + require(version); + remember(transport, request, version); + } else if (absent(probe.result())) { + if (noStart(request.endpoint()) || request.idempotence() == Idempotence.IDEMPOTENT || cleanup(request)) { + CommandRequest absent = new CommandRequest( + withoutStarting(request.endpoint()), + request.commands(), + remaining(request, started), + request.input()); + try { + return dispatch(transport, absent, prefix(request.commands(), "0"), absent.timeout(), waiting); + } catch (Replaced appeared) { + throw new TargetGoneException("the tmux daemon version changed before dispatch"); + } + } + version = binaryVersion(transport, request, remaining(request, started)); + } else { + throw rejected(request, probe.result()); + } + if (probe.version().isPresent()) { + return dispatchChecked(transport, request, version, started, waiting, intent); + } + TmuxFeatures.require(TmuxVersion.parse(version), intent); + return dispatch(transport, request, guarded(request.commands(), version), remaining(request, started), waiting); + } + + private static CommandResult dispatchChecked( + TmuxTransport transport, + CommandRequest request, + String version, + long started, + boolean waiting, + TmuxFeatures.Intent intent) { + TmuxFeatures.require(TmuxVersion.parse(version), intent); + CommandRequest live = new CommandRequest( + withoutStarting(request.endpoint()), request.commands(), request.timeout(), request.input()); + return dispatch(transport, live, guarded(request.commands(), version), remaining(request, started), waiting); + } + + private static @Nullable String recall(TmuxTransport transport, CommandRequest request) { + Map, String> known = KNOWN.get(transport); + return known == null ? null : known.get(request.endpoint()); + } + + private static void remember(TmuxTransport transport, CommandRequest request, String version) { + synchronized (KNOWN) { + KNOWN.computeIfAbsent(transport, ignored -> new ConcurrentHashMap<>()) + .put(request.endpoint(), version); + } + } + + private static void forget(TmuxTransport transport, CommandRequest request) { + Map, String> known = KNOWN.get(transport); + if (known != null) known.remove(request.endpoint()); + } + + /** Whether tmux found no daemon to run the request on, which leaves no effect behind. */ + private static boolean gone(CommandResult result) { + return !result.succeeded() + && result.stderr().stream() + .anyMatch(line -> line.contains("no server running") || line.contains("error connecting to")); + } + + /** Reports the daemon version, or validates the executable when the endpoint is unused. */ + public static TmuxVersion versionForCreation(TmuxTransport transport, CommandRequest request) { + long started = System.nanoTime(); + Probe daemon = probe(transport, request); + if (daemon.version().isPresent()) return require(daemon.version().get()); + if (!absent(daemon.result())) throw rejected(request, daemon.result()); + return TmuxVersion.parse(binaryVersion(transport, request, remaining(request, started))); + } + + /** What the version probe learned: the daemon's version, or the failure that says why not. */ + private record Probe(Optional version, CommandResult result) {} + + /** + * Asks the daemon for its version without running a command that fires a user's hooks. + * + *

{@code run-shell -C} parses its expanded argument as a command, so the format expands in + * the daemon and the parse failure carries the answer on stderr as + * {@code unknown command: }. Unlike {@code display-message} the command runs + * no {@code after-*} hook, and the parse failure never reaches {@code command-error}. + */ + private static Probe probe(TmuxTransport transport, CommandRequest request) { + String marker = marker(); + CommandResult result; + try { + result = transport.execute(new CommandRequest( + withoutStarting(request.endpoint()), List.of(probeCommand(marker)), request.timeout(), "")); + } catch (DispatchException failure) { + throw beforeDispatch(failure, request); + } + return new Probe(answer(result, marker, request.endpoint().getFirst()), result); + } + + private static String marker() { + return PROBE_MARKER + Long.toHexString(ThreadLocalRandom.current().nextLong()) + "-"; + } + + private static List probeCommand(String marker) { + return List.of("run-shell", "-C", marker + "#{version}"); + } + + /** The version a probe's refusal spelled, or empty when tmux answered anything else. */ + private static Optional answer(CommandResult result, String marker, String binary) { + if (result.succeeded()) { + throw new MalformedResponseException(binary + " exited 0 and reported no tmux version; is it tmux?"); + } + String expected = UNKNOWN_COMMAND + marker; + for (String line : result.stderr()) { + if (line.startsWith(expected) && line.length() > expected.length()) { + return Optional.of(line.substring(expected.length())); + } + } + return Optional.empty(); + } + + /** Checks through the carrier without taking ownership of the caller's carrier. */ + public static TmuxVersion requireCarrier(ControlCarrier carrier, List endpoint, Duration timeout) { + String marker = marker(); + CommandRequest request = + new CommandRequest(withoutStarting(endpoint), List.of(probeCommand(marker)), timeout, ""); + CommandResult result; + try { + result = ProcessTransport.executeThroughCarrier(carrier, request); + } catch (DispatchException failure) { + throw beforeDispatch(failure, request); + } + Optional version = answer(result, marker, endpoint.getFirst()); + if (version.isEmpty()) throw rejected(request, result); + return require(version.get()); + } + + private static boolean cleanup(CommandRequest request) { + return request.commands().stream() + .anyMatch(command -> command.getFirst().equals("kill-server")) + && request.commands().stream() + .allMatch(command -> command.getFirst().equals("kill-server") + || command.equals(List.of("display-message", "-p", "#{pid}"))); + } + + /** The minimum applies to the daemon's own report, including patch letters. */ + public static TmuxVersion require(String reported) { + if (!VERSION.matcher(reported).matches()) { + throw new MalformedResponseException("tmux reported an unrecognized version: " + reported); + } + TmuxVersion running; + try { + running = TmuxVersion.parse(reported); + } catch (IllegalArgumentException malformed) { + throw new MalformedResponseException("tmux reported an unrecognized version: " + reported, malformed); + } + if (running.compareTo(MINIMUM) < 0) { + throw new UnsupportedFeatureException("libtmux core", MINIMUM, running); + } + return running; + } + + private static String binaryVersion(TmuxTransport transport, CommandRequest request, Duration timeout) { + CommandResult result; + try { + result = transport.execute(new CommandRequest(request.endpoint(), List.of(List.of("-V")), timeout, "")); + } catch (DispatchException failure) { + throw beforeDispatch(failure, request); + } + if (!result.succeeded()) { + throw rejected(request, result); + } + String line = reported(result, request.endpoint().getFirst()); + // tmux 3.3a reports argv[0]'s basename; later releases print tmux. + String prefix = new File(request.endpoint().getFirst()).getName() + " "; + if (!line.startsWith(prefix)) prefix = "tmux "; + if (!line.startsWith(prefix)) { + throw new MalformedResponseException( + request.endpoint().getFirst() + " did not report a tmux version: " + line); + } + String version = line.substring(prefix.length()); + try { + require(version); + } catch (UnsupportedFeatureException unsupported) { + throw new UnsupportedFeatureException( + "libtmux core requires tmux " + MINIMUM + ", but the configured binary reports " + version); + } + return version; + } + + private static CommandResult dispatch( + TmuxTransport transport, + CommandRequest original, + List> commands, + Duration timeout, + boolean waiting) { + CommandRequest guarded = new CommandRequest(original.endpoint(), commands, timeout, original.input()); + CommandResult result = waiting ? transport.executeWaiting(guarded) : transport.execute(guarded); + if (!result.succeeded() + && result.stderr().stream() + .anyMatch(line -> line.contains(commands.getFirst().getLast()))) { + throw new Replaced(); + } + return result; + } + + private static List> guarded(List> commands, String version) { + return prefix(commands, "#{==:#{version}," + version + "}"); + } + + private static List> prefix(List> commands, String condition) { + List> fenced = new ArrayList<>(commands.size() + 1); + // A space is a tmux no-op and keeps JDK process-argument inspection intact. + fenced.add(List.of("if-shell", "-F", condition, " ", CHANGED + "-" + UUID.randomUUID())); + fenced.addAll(commands); + return List.copyOf(fenced); + } + + private static List withoutStarting(List endpoint) { + // A selector or filename may itself equal -N, so always insert a flag. + List selected = new ArrayList<>(endpoint); + selected.add(1, "-N"); + return List.copyOf(selected); + } + + /** Recognizes global -N without treating option values as flags. */ + private static boolean noStart(List endpoint) { + for (int index = 1; index < endpoint.size(); index++) { + String option = endpoint.get(index); + if (!option.startsWith("-") || option.equals("--")) return false; + for (int letter = 1; letter < option.length(); letter++) { + char flag = option.charAt(letter); + if (flag == 'N') return true; + if ("cfLST".indexOf(flag) >= 0) { + if (letter + 1 == option.length()) index++; + break; + } + } + } + return false; + } + + /** Whether tmux explicitly reported that nothing is serving the selected endpoint. */ + public static boolean absent(CommandResult result) { + return !result.succeeded() + && result.stderr().stream() + .anyMatch(line -> + line.contains("no server running") || line.contains("(No such file or directory)")); + } + + private static String reported(CommandResult result, String binary) { + if (result.stdout().size() != 1 || result.stdout().getFirst().isBlank()) { + throw new MalformedResponseException(binary + " exited 0 and reported no single tmux version; is it tmux?"); + } + return result.stdout().getFirst(); + } + + private static LibTmuxException rejected(CommandRequest request, CommandResult result) { + String error = ErrorText.suffix(result.stderr()); + String message = "tmux version probe failed (exit " + result.exitCode() + ")" + + (error.isEmpty() ? "; check that " + request.endpoint().getFirst() + " is tmux" : error); + return absent(result) + ? new ServerUnavailableException(message, "run-shell", result.exitCode(), result.stderr()) + : new CommandRejectedException(message, "run-shell", result.exitCode(), result.stderr()); + } + + private static Duration remaining(CommandRequest request, long started) { + Duration left = request.timeout().minusNanos(System.nanoTime() - started); + if (left.isZero() || left.isNegative()) { + throw new DispatchException.TimedOut( + "the tmux version check exceeded the request deadline", + DispatchOutcome.NOT_DISPATCHED, + request.idempotence(), + null); + } + return left; + } + + private static DispatchException beforeDispatch(DispatchException failure, CommandRequest request) { + DispatchException rejected = failure instanceof DispatchException.TimedOut + ? new DispatchException.TimedOut( + String.valueOf(failure.getMessage()), + DispatchOutcome.NOT_DISPATCHED, + request.idempotence(), + failure.getCause()) + : new DispatchException.Failed( + String.valueOf(failure.getMessage()), + DispatchOutcome.NOT_DISPATCHED, + request.idempotence(), + failure.getCause()); + rejected.addSuppressed(failure); + return rejected; + } +} diff --git a/libtmux/src/main/java/io/github/libtmux/internal/support/package-info.java b/libtmux/src/main/java/io/github/libtmux/internal/support/package-info.java new file mode 100644 index 000000000..9a1dee6d1 --- /dev/null +++ b/libtmux/src/main/java/io/github/libtmux/internal/support/package-info.java @@ -0,0 +1,13 @@ +/** + * The tmux version check: whether the daemon behind an endpoint runs a supported tmux release. + * + *

"Support" here means the tmux releases this library works with, not helper code. Like its + * parent package, {@code module-info.java} does not export it, and nothing in it carries a + * compatibility guarantee. + * + *

The package is null-marked: every type is non-null unless annotated otherwise. + */ +@NullMarked +package io.github.libtmux.internal.support; + +import org.jspecify.annotations.NullMarked; diff --git a/libtmux/src/main/java/io/github/libtmux/transport/ControlCarrier.java b/libtmux/src/main/java/io/github/libtmux/transport/ControlCarrier.java index 757a61ae9..6b720012a 100644 --- a/libtmux/src/main/java/io/github/libtmux/transport/ControlCarrier.java +++ b/libtmux/src/main/java/io/github/libtmux/transport/ControlCarrier.java @@ -1,6 +1,7 @@ package io.github.libtmux.transport; import java.io.IOException; +import java.time.Duration; import java.util.List; /** @@ -21,4 +22,31 @@ public interface ControlCarrier { * @throws IOException if the process cannot be started */ Process start(List command) throws IOException; + + /** + * Retains this realm's Unix socket against replacement before control initialization. + * + *

The alias must refer to the same socket inode for its lifetime, within a private directory + * on the same filesystem. Replacing the original pathname must never redirect the alias. + * The caller owns the returned pin; the carrier remains borrowed. + * + * @param socketPath the daemon's own socket path in this carrier's realm + * @param timeout the remaining acquisition budget + * @return the retained endpoint + * @throws IOException if pinning fails + * @throws UnsupportedOperationException when this carrier cannot retain an endpoint + */ + default PinnedSocket pinSocket(String socketPath, Duration timeout) throws IOException { + throw new UnsupportedOperationException("this carrier cannot retain a tmux socket"); + } + + /** A retained socket alias owned by one control client. */ + interface PinnedSocket extends AutoCloseable { + /** The alias pathname in the carrier's realm. */ + String socketPath(); + + /** Removes only the alias and directory created by this pin. */ + @Override + void close() throws IOException; + } } diff --git a/libtmux/src/main/java/io/github/libtmux/transport/Idempotence.java b/libtmux/src/main/java/io/github/libtmux/transport/Idempotence.java index fd478246e..c26a1abf6 100644 --- a/libtmux/src/main/java/io/github/libtmux/transport/Idempotence.java +++ b/libtmux/src/main/java/io/github/libtmux/transport/Idempotence.java @@ -19,6 +19,8 @@ public enum Idempotence { // Full names and their aliases. display-message and capture-pane read only with -p, and are // decided below; every other verb, run-shell and if-shell included, is treated as a change. private static final Set READS = Set.of( + "-V", + "-h", "has-session", "has", "list-buffers", @@ -99,7 +101,11 @@ private static boolean guardedRead(List argv) { List rest = argv.subList(Math.min(index + 1, argv.size()), argv.size()); return !rest.isEmpty() && rest.size() <= 2 - && rest.stream().allMatch(branch -> branch.startsWith("libtmux-stale-") || readsText(branch)); + && rest.stream() + .allMatch(branch -> branch.isEmpty() + || branch.equals(" ") + || branch.startsWith("libtmux-stale-") + || readsText(branch)); } private static boolean readsText(String command) { diff --git a/libtmux/src/main/java/io/github/libtmux/transport/ProcessTransport.java b/libtmux/src/main/java/io/github/libtmux/transport/ProcessTransport.java index 277958521..22d040853 100644 --- a/libtmux/src/main/java/io/github/libtmux/transport/ProcessTransport.java +++ b/libtmux/src/main/java/io/github/libtmux/transport/ProcessTransport.java @@ -3,6 +3,7 @@ import io.github.libtmux.exception.DispatchException; import io.github.libtmux.exception.ServerClosedException; import io.github.libtmux.internal.CommandStrings; +import io.github.libtmux.internal.LocalSocketPin; import io.github.libtmux.internal.ProcessTree; import io.github.libtmux.internal.Utf8; import java.io.ByteArrayOutputStream; @@ -115,6 +116,19 @@ public ProcessTransport(int maxConcurrentProcesses) { this(maxConcurrentProcesses, DEFAULT_MAX_OUTPUT_BYTES); } + /** + * Runs one bounded request through a caller-owned process carrier. + * + *

Reclaims only the processes this request started. The carrier remains owned by the caller + * and may continue starting other processes. + */ + public static CommandResult executeThroughCarrier(ControlCarrier carrier, CommandRequest request) { + try (ProcessTransport transport = + new ProcessTransport(1, DEFAULT_MAX_OUTPUT_BYTES, carrier::start, System::nanoTime)) { + return transport.execute(request); + } + } + /** * @param maxConcurrentProcesses how many tmux processes may run at once * @param maxOutputBytes maximum bytes accepted from each output channel of one process @@ -159,10 +173,21 @@ public ProcessTransport(int maxConcurrentProcesses, int maxOutputBytes) { } @Override + @SuppressWarnings("ReferenceEquality") // Only this starter owns the local filesystem. public Optional controlCarrier() { - return Optional.of(command -> { - requireOpen(); - return starter.start(command); + return Optional.of(new ControlCarrier() { + @Override + public Process start(List command) throws IOException { + requireOpen(); + return starter.start(command); + } + + @Override + public PinnedSocket pinSocket(String socketPath, Duration timeout) throws IOException { + requireOpen(); + if (starter != SYSTEM_STARTER) return ControlCarrier.super.pinSocket(socketPath, timeout); + return LocalSocketPin.retain(socketPath); + } }); } diff --git a/libtmux/src/test/java/io/github/libtmux/ArgumentTerminatorTest.java b/libtmux/src/test/java/io/github/libtmux/ArgumentTerminatorTest.java index 06a537acd..13f0fb1d1 100644 --- a/libtmux/src/test/java/io/github/libtmux/ArgumentTerminatorTest.java +++ b/libtmux/src/test/java/io/github/libtmux/ArgumentTerminatorTest.java @@ -103,7 +103,7 @@ void sessionNamesAreResolvedLocallyAndNeverBecomeArguments() { void creationCommandsEndOptionsBeforeTheProgram() { TmuxVersion version = new TmuxVersion(3, 7, "c"); List> commands = List.of( - SessionSpec.builder().running(DASHED).build().argv("#{pane_id}", () -> version), + SessionSpec.builder().running(DASHED).build().argv("#{pane_id}"), WindowSpec.builder().running(DASHED).build().argv("$0", "#{pane_id}"), SplitSpec.builder().running(DASHED).build().argv("%0", "#{pane_id}", version)); diff --git a/libtmux/src/test/java/io/github/libtmux/CapturedServerTest.java b/libtmux/src/test/java/io/github/libtmux/CapturedServerTest.java new file mode 100644 index 000000000..3b424401e --- /dev/null +++ b/libtmux/src/test/java/io/github/libtmux/CapturedServerTest.java @@ -0,0 +1,102 @@ +package io.github.libtmux; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import io.github.libtmux.format.RowFormat; +import io.github.libtmux.transport.CommandRequest; +import io.github.libtmux.transport.CommandResult; +import io.github.libtmux.transport.TmuxTransport; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; + +final class CapturedServerTest { + + @Test + void everyHandleAndRelationRetainsTheSameCaptureWithoutCommands() { + AtomicInteger commands = new AtomicInteger(); + TmuxTransport transport = new TmuxTransport() { + @Override + public CommandResult execute(CommandRequest request) { + if (!VersionProbes.is(request)) commands.incrementAndGet(); + return GroupedTmux.execute( + request, + 4242L, + argv -> new CommandResult( + 0, + switch (argv.getFirst()) { + case "display-message" -> + List.of(row("4242", "3.6", Long.toString(GroupedTmux.STARTED))); + case "list-sessions" -> + List.of(row("$0", "first", "1", "1"), row("$1", "second", "0", "1")); + case "list-windows" -> + List.of( + row("$0", "@0", "0", "shared", "1", "1", "1", "80", "24", "layout"), + row("$1", "@0", "9", "shared", "1", "1", "1", "80", "24", "layout")); + case "list-panes" -> List.of(pane("$0", "0"), pane("$1", "9")); + case "list-clients" -> List.of(row("client", "$0")); + default -> throw new AssertionError(argv); + }, + List.of())); + } + + @Override + public void close() {} + }; + try (Server server = Server.using(ServerConfig.builder().build(), transport)) { + CapturedServer captured = server.captureServer(); + var snapshot = captured.snapshot(); + assertEquals(2, commands.get()); + assertSame(server, captured.server()); + assertEquals(snapshot.serverPid(), captured.identity().processId()); + assertEquals(2, captured.sessions().size()); + assertEquals(2, captured.windows().size()); + assertEquals(2, captured.panes().size()); + assertEquals(1, captured.clients().size()); + captured.sessions().forEach(session -> { + assertSame(snapshot, session.snapshot()); + assertSame(snapshot, session.activeWindow().orElseThrow().snapshot()); + session.windows().forEach(window -> assertSame(snapshot, window.snapshot())); + }); + captured.windows().forEach(window -> { + assertSame(snapshot, window.snapshot()); + assertSame(snapshot, window.session().snapshot()); + assertSame(snapshot, window.activePane().orElseThrow().snapshot()); + window.panes().forEach(pane -> assertSame(snapshot, pane.snapshot())); + }); + captured.panes().forEach(pane -> { + assertSame(snapshot, pane.snapshot()); + assertSame(snapshot, pane.window().snapshot()); + }); + captured.clients().forEach(client -> { + assertSame(snapshot, client.snapshot()); + assertSame(snapshot, client.session().orElseThrow().snapshot()); + ClientAttachment attachment = client.attachment().orElseThrow(); + assertSame(snapshot, attachment.session().snapshot()); + assertSame(snapshot, attachment.activeWindow().snapshot()); + assertSame(snapshot, attachment.activePane().snapshot()); + }); + assertEquals( + List.of("$0", "$1"), + captured.sessions().stream().map(s -> s.id().value()).toList()); + assertEquals( + captured.panes().getFirst().id(), captured.panes().getLast().id()); + assertThrows( + UnsupportedOperationException.class, + () -> captured.clients().clear()); + assertEquals(2, commands.get(), "metadata, clients, and all captured relations must remain pure"); + } + } + + private static String pane(String session, String index) { + return row( + session, "@0", index, "%0", "0", "1", "sh", "80", "24", "0", "0", "title", "/", "42", "1", "1", "1", + "1"); + } + + private static String row(String... fields) { + return String.join(RowFormat.of("field").separator(), fields); + } +} diff --git a/libtmux/src/test/java/io/github/libtmux/ControlFloorVectorsTest.java b/libtmux/src/test/java/io/github/libtmux/ControlFloorVectorsTest.java new file mode 100644 index 000000000..571bec443 --- /dev/null +++ b/libtmux/src/test/java/io/github/libtmux/ControlFloorVectorsTest.java @@ -0,0 +1,31 @@ +package io.github.libtmux; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import io.github.libtmux.exception.UnsupportedFeatureException; +import io.github.libtmux.internal.ControlAcquisition; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** Control needs tmux 3.7: its own development and candidate builds do not qualify, later ones do. */ +final class ControlFloorVectorsTest { + + @Test + void theControlFloorFollowsTheReleaseTrain() { + List refused = List.of("3.6b", "next-3.7", "3.7-rc", "3.7-rc2", "3.7-rc9", "3.3a", "next-3.6"); + List supported = + List.of("3.7", "3.7a", "3.7d", "3.8-rc", "3.8-rc2", "next-3.8", "next-3.9", "3.10", "4.0"); + for (String version : refused) { + assertEquals(false, ControlAcquisition.supports(TmuxVersion.parse(version)), version); + assertThrows( + UnsupportedFeatureException.class, + () -> ControlAcquisition.requireSupported(TmuxVersion.parse(version)), + version); + } + for (String version : supported) { + assertEquals(true, ControlAcquisition.supports(TmuxVersion.parse(version)), version); + ControlAcquisition.requireSupported(TmuxVersion.parse(version)); + } + } +} diff --git a/libtmux/src/test/java/io/github/libtmux/CreationSpecTest.java b/libtmux/src/test/java/io/github/libtmux/CreationSpecTest.java index d00d0295e..add164723 100644 --- a/libtmux/src/test/java/io/github/libtmux/CreationSpecTest.java +++ b/libtmux/src/test/java/io/github/libtmux/CreationSpecTest.java @@ -6,10 +6,8 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import io.github.libtmux.exception.UnsupportedFeatureException; import java.nio.file.Path; import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.Test; /** @@ -21,8 +19,6 @@ final class CreationSpecTest { private static final TmuxVersion V32A = new TmuxVersion(3, 2, "a"); - private static final TmuxVersion V33 = new TmuxVersion(3, 3, ""); - private static final TmuxVersion V33A = new TmuxVersion(3, 3, "a"); private static final TmuxVersion V37B = new TmuxVersion(3, 7, "b"); private static final String FORMAT = "#{window_id}"; @@ -43,7 +39,7 @@ void aPlainWindowAsksForNothingBeyondTheWindow() { void onlyASessionIsDetachedByDefault() { assertFalse(WindowSpec.builder().build().argv("$1", FORMAT).contains("-d")); assertTrue(WindowSpec.builder().detached().build().argv("$1", FORMAT).contains("-d")); - assertTrue(SessionSpec.builder().build().argv(FORMAT, () -> V37B).contains("-d")); + assertTrue(SessionSpec.builder().build().argv(FORMAT).contains("-d")); } @Test @@ -106,7 +102,7 @@ void aRelativeDirectoryReachesTmuxResolvedAgainstThisProcess() { Path relative = Path.of("sub"); String resolved = relative.toAbsolutePath().toString(); - List session = SessionSpec.builder().in(relative).build().argv(FORMAT, () -> V32A); + List session = SessionSpec.builder().in(relative).build().argv(FORMAT); List window = WindowSpec.builder().in(relative).build().argv("$1", FORMAT); List split = SplitSpec.builder().in(relative).build().argv("%1", FORMAT, V32A); List respawn = Pane.respawnArgv(new PaneId("%1"), relative); @@ -126,7 +122,7 @@ void aWindowWithoutADirectoryUsesTmuxDefaults() { @Test void aPlainSessionIsAlwaysDetached() { - List argv = SessionSpec.builder().build().argv(FORMAT, () -> V37B); + List argv = SessionSpec.builder().build().argv(FORMAT); assertEquals(List.of("new-session", "-d", "-P", "-F", FORMAT), argv); } @@ -134,63 +130,18 @@ void aPlainSessionIsAlwaysDetached() { @Test void aSizeBecomesTheTwoFlagsTmuxWants() { List argv = - SessionSpec.builder().sized(new Dimensions(120, 40)).build().argv(FORMAT, () -> V37B); + SessionSpec.builder().sized(new Dimensions(120, 40)).build().argv(FORMAT); assertEquals("120", argv.get(argv.indexOf("-x") + 1)); assertEquals("40", argv.get(argv.indexOf("-y") + 1)); } - /** - * 3.2a accepts {@code -x}/{@code -y} for a detached session and gives it the default size. - * - *

3.2a and 3.3a alone cannot tell the real floor from one lettered patch too high - both - * take the same branch either way - so the discriminating case is the plain 3.3 answer neither - * lane in the matrix ever sends. - */ - @Test - void aSizeIsRefusedOnTheReleaseThatIgnoresIt() { - SessionSpec spec = SessionSpec.builder().sized(new Dimensions(120, 40)).build(); - - UnsupportedFeatureException refused = - assertThrows(UnsupportedFeatureException.class, () -> spec.argv(FORMAT, () -> V32A)); - - assertEquals( - "a size for a detached session requires tmux 3.3, but this server runs 3.2a", refused.getMessage()); - assertDoesNotThrow(() -> spec.argv(FORMAT, () -> V33)); - assertDoesNotThrow(() -> spec.argv(FORMAT, () -> V33A)); - } - - /** - * Creating the first session on a socket is the one call that runs before there is a server, and - * a socket with nothing behind it cannot answer what version it is. So nothing asks unless the - * spec depends on the answer. - */ - @Test - void theVersionIsNotAskedForUnlessTheSpecNeedsIt() { - AtomicInteger asked = new AtomicInteger(); - - SessionSpec.builder().named("bootstrap").build().argv(FORMAT, () -> { - asked.incrementAndGet(); - return V37B; - }); - - assertEquals(0, asked.get(), "a plain session must be creatable before a server exists"); - - SessionSpec.builder().sized(new Dimensions(80, 24)).build().argv(FORMAT, () -> { - asked.incrementAndGet(); - return V37B; - }); - - assertEquals(1, asked.get(), "a sized session does depend on the version"); - } - - /** tmux reads {@code -f} as one comma-separated list, not as a flag that may repeat. */ @Test void clientFlagsGoOutAsOneCommaSeparatedValue() { List argv = SessionSpec.builder() .clientFlags("no-detach-on-destroy", "active-pane") .build() - .argv(FORMAT, () -> V37B); + .argv(FORMAT); assertEquals(1, argv.stream().filter("-f"::equals).count()); assertEquals("no-detach-on-destroy,active-pane", argv.get(argv.indexOf("-f") + 1)); @@ -206,7 +157,7 @@ void aSessionCarriesItsDirectoryEnvironmentAndFirstWindowName() { .detachOthers() .withoutSize() .build() - .argv(FORMAT, () -> V37B); + .argv(FORMAT); assertEquals("build", argv.get(argv.indexOf("-s") + 1)); assertEquals("editor", argv.get(argv.indexOf("-n") + 1)); @@ -220,7 +171,7 @@ void callerDirectoriesAreLiteralizedExactlyOnceAtEverySpawnBoundary() { Path supplied = Path.of("/srv/#one/##two"); String literal = "/srv/##one/####two"; - List session = SessionSpec.builder().in(supplied).build().argv(FORMAT, () -> V37B); + List session = SessionSpec.builder().in(supplied).build().argv(FORMAT); List window = WindowSpec.builder().in(supplied).build().argv("$1", FORMAT); List split = SplitSpec.builder().in(supplied).build().argv("%1", FORMAT, V37B); List respawn = Pane.respawnArgv(new PaneId("%1"), supplied); @@ -243,7 +194,7 @@ void bothSpecsAreDescriptionsThatCanBeLoweredTwice() { SessionSpec session = SessionSpec.builder().named("shared").build(); assertEquals(window.argv("$1", FORMAT), window.argv("$1", FORMAT)); - assertEquals(session.argv(FORMAT, () -> V37B), session.argv(FORMAT, () -> V37B)); + assertEquals(session.argv(FORMAT), session.argv(FORMAT)); assertEquals( "$2", window.argv("$2", FORMAT).get(window.argv("$2", FORMAT).indexOf("-t") + 1)); } diff --git a/libtmux/src/test/java/io/github/libtmux/GroupedTmux.java b/libtmux/src/test/java/io/github/libtmux/GroupedTmux.java index 70c4fa3ba..17ebdc713 100644 --- a/libtmux/src/test/java/io/github/libtmux/GroupedTmux.java +++ b/libtmux/src/test/java/io/github/libtmux/GroupedTmux.java @@ -59,10 +59,21 @@ static CommandResult execute( String liveVersion, long liveStart, Function, CommandResult> command) { + if (request.endpoint().contains("-N") && VersionProbes.is(request)) { + return VersionProbes.reply(request, liveVersion); + } if (request.commands().size() > 1) { List stdout = new ArrayList<>(); for (List one : request.commands()) { - CommandResult answered = command.apply(one); + CommandResult answered = one.getFirst().equals("if-shell") + ? execute( + new CommandRequest( + request.endpoint(), List.of(one), request.timeout(), request.input()), + livePid, + liveVersion, + liveStart, + command) + : command.apply(one); stdout.addAll(answered.stdout()); if (!answered.succeeded()) { return new CommandResult(answered.exitCode(), stdout, answered.stderr()); @@ -75,7 +86,11 @@ static CommandResult execute( return command.apply(argv); } String stale = argv.get(argv.size() - 1); - Matcher fence = FENCED.matcher(argv.get(2)); + int condition = argv.get(2).equals("-t") ? 4 : 2; + if (argv.get(condition).equals("0")) { + return new CommandResult(1, List.of(), List.of("unknown command: " + stale)); + } + Matcher fence = FENCED.matcher(argv.get(condition)); while (fence.find()) { String live = switch (fence.group(1)) { @@ -87,7 +102,8 @@ static CommandResult execute( return new CommandResult(1, List.of(), List.of("unknown command: " + stale)); } } - return group(argv.get(argv.size() - 2), command); + String branch = argv.get(argv.size() - 2); + return branch.isEmpty() ? new CommandResult(0, List.of(), List.of()) : group(branch, command); } /** Runs each command in turn, and discards the rest once one fails, which is what tmux does. */ @@ -102,7 +118,9 @@ private static CommandResult group(String commands, Function, Comma if (words.isEmpty()) { continue; } - CommandResult answered = words.get(0).equals("display-message") && words.size() == 3 + CommandResult answered = words.get(0).equals("display-message") + && words.size() == 3 + && !words.get(2).contains("#{") ? new CommandResult(0, List.of(words.get(2)), List.of()) : command.apply(words); stdout.addAll(answered.stdout()); diff --git a/libtmux/src/test/java/io/github/libtmux/HandleTest.java b/libtmux/src/test/java/io/github/libtmux/HandleTest.java index 65960363c..8a6533fb5 100644 --- a/libtmux/src/test/java/io/github/libtmux/HandleTest.java +++ b/libtmux/src/test/java/io/github/libtmux/HandleTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -228,7 +229,10 @@ void aHandleUsesTheVersionCapturedWithItsIdentity() { window.selectLayout(Layout.MAIN_HORIZONTAL_MIRRORED); - assertEquals(captured + 1, transport.calls.get(), "the operation must not probe another server version"); + assertEquals(captured + 1, transport.calls.get(), "only the operation reached tmux: the version is known"); + assertFalse( + VersionProbes.is(transport.requests.get(captured)), + "the version check must not capture a new handle identity"); } } @@ -328,7 +332,7 @@ private static Server canned(ServerEndpoint endpoint) { private static String last(CountingTransport transport) { List argv = - transport.requests.get(transport.requests.size() - 1).commands().get(0); + transport.requests.get(transport.requests.size() - 1).commands().getLast(); return argv.get(argv.size() - 2); } diff --git a/libtmux/src/test/java/io/github/libtmux/LayoutsTest.java b/libtmux/src/test/java/io/github/libtmux/LayoutsTest.java index b377a8458..bb9c0b1c9 100644 --- a/libtmux/src/test/java/io/github/libtmux/LayoutsTest.java +++ b/libtmux/src/test/java/io/github/libtmux/LayoutsTest.java @@ -150,7 +150,7 @@ void namesUseTheRunningDaemonBeforeConsideringTheClient() { assertEquals("main-horizontal", Layouts.require("main-h", server, 1)); assertThrows( UnsupportedFeatureException.class, () -> Layouts.require("main-horizontal-mirrored", server, 1)); - assertEquals(List.of("display-message", "display-message"), transport.commands); + assertEquals(List.of("run-shell", "run-shell"), transport.commands); } try (VersionTransport transport = new VersionTransport(new CommandResult(0, List.of("3.7c"), List.of())); Server server = server(transport)) { @@ -175,7 +175,7 @@ void onlyColdEndpointsUseTheSelectedClientVersion(String reason) { try (VersionTransport transport = new VersionTransport(new CommandResult(1, List.of(), List.of(reason))); Server server = server(transport)) { assertEquals("main-horizontal-mirrored", Layouts.require("main-horizontal-m", server, 1)); - assertEquals(List.of("display-message", "-V"), transport.commands); + assertEquals(List.of("run-shell", "-V"), transport.commands); } } @@ -193,7 +193,7 @@ void probeFailuresDoNotFallBackToANewerClient(String reason) { LibTmuxException failure = assertThrows(LibTmuxException.class, () -> Layouts.require("main-horizontal-mirrored", server, 1)); assertTrue(String.valueOf(failure.getMessage()).contains(reason)); - assertEquals(List.of("display-message"), transport.commands); + assertEquals(List.of("run-shell"), transport.commands); } } @@ -223,7 +223,7 @@ void invalidDaemonVersionRepliesNeverSelectTheClient(String reported) { try (VersionTransport transport = new VersionTransport(new CommandResult(0, List.of(reported), List.of())); Server server = server(transport)) { assertThrows(LibTmuxException.class, () -> Layouts.require("main-horizontal-mirrored", server, 1)); - assertEquals(List.of("display-message"), transport.commands); + assertEquals(List.of("run-shell"), transport.commands); } } @@ -310,24 +310,12 @@ private static final class VersionTransport implements TmuxTransport { public CommandResult execute(CommandRequest request) { List command = request.commands().getFirst(); commands.add(command.getFirst()); - if (command.getFirst().equals("display-message")) { - assertEquals( - List.of( - "display-message", - "-p", - io.github.libtmux.format.RowFormat.of("pid", "version", "start_time") - .template()), - command); - if (!daemon.succeeded()) return daemon; - return new CommandResult( - 0, - List.of(String.join( - io.github.libtmux.format.RowFormat.of("pid", "version", "start_time") - .separator(), - "4242", - String.join("\n", daemon.stdout()), - "1790000000")), - List.of()); + if (command.getFirst().equals("run-shell")) { + assertTrue(VersionProbes.is(request), String.valueOf(command)); + assertTrue(request.endpoint().contains("-N"), "a version probe must not start tmux"); + return daemon.succeeded() + ? VersionProbes.reply(request, daemon.stdout().getFirst()) + : daemon; } assertEquals(List.of("-V"), command); return new CommandResult(0, List.of("tmux 3.7c"), List.of()); diff --git a/libtmux/src/test/java/io/github/libtmux/MinimumTmuxTest.java b/libtmux/src/test/java/io/github/libtmux/MinimumTmuxTest.java new file mode 100644 index 000000000..2dff2537c --- /dev/null +++ b/libtmux/src/test/java/io/github/libtmux/MinimumTmuxTest.java @@ -0,0 +1,595 @@ +package io.github.libtmux; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.github.libtmux.exception.CommandRejectedException; +import io.github.libtmux.exception.DispatchException; +import io.github.libtmux.exception.MalformedResponseException; +import io.github.libtmux.exception.TargetGoneException; +import io.github.libtmux.exception.UnsupportedFeatureException; +import io.github.libtmux.internal.support.Versions; +import io.github.libtmux.transport.CommandRequest; +import io.github.libtmux.transport.CommandResult; +import io.github.libtmux.transport.DispatchOutcome; +import io.github.libtmux.transport.Idempotence; +import io.github.libtmux.transport.TmuxTransport; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +final class MinimumTmuxTest { + + @ParameterizedTest + @ValueSource(strings = {"3.2a", "3.3", "next-3.3", "3.3-rc"}) + void oldDaemonsRejectRawReadsWritesAndGroups(String version) { + FakeTmux tmux = new FakeTmux(version); + try (Server server = Server.using(ServerConfig.builder().build(), tmux)) { + assertThrows(UnsupportedFeatureException.class, () -> server.cmd("list-sessions")); + assertThrows( + UnsupportedFeatureException.class, () -> server.cmd("set-option", "-g", "@sentinel", "changed")); + assertThrows( + UnsupportedFeatureException.class, + () -> server.batch().add("kill-server").run()); + assertThrows(UnsupportedFeatureException.class, server::killServer); + assertThrows( + UnsupportedFeatureException.class, + () -> server.globalOptions().get("@sentinel")); + assertEquals(0, tmux.effects.get()); + } + } + + @ParameterizedTest + @ValueSource(strings = {"3.3a", "3.3b", "3.4", "3.7a", "3.8", "next-3.9"}) + void supportedDaemonWinsOverAnOlderClient(String version) { + FakeTmux tmux = new FakeTmux(version); + tmux.binaryVersion = "3.2a"; + try (Server server = Server.using(ServerConfig.builder().build(), tmux)) { + assertTrue(server.cmd("set-option", "-g", "@sentinel", "changed").succeeded()); + assertEquals(1, tmux.effects.get()); + assertEquals(0, tmux.binaryProbes.get()); + } + } + + @Test + void freshSocketChecksTheBinaryBeforeStartingADaemon() { + FakeTmux tmux = new FakeTmux("3.3a"); + tmux.absent = true; + tmux.binaryVersion = "3.3"; + try (Server server = Server.using(ServerConfig.builder().build(), tmux)) { + assertThrows(UnsupportedFeatureException.class, () -> server.cmd("new-session", "-d")); + assertEquals(0, tmux.effects.get()); + assertEquals(1, tmux.binaryProbes.get()); + } + } + + @ParameterizedTest + @ValueSource(strings = {"tmux", "configured client"}) + void coldVersionBannerMayNameTheConfiguredExecutable(String name) { + FakeTmux tmux = new FakeTmux("3.3a"); + tmux.absent = true; + tmux.binaryVersion = "3.3a"; + tmux.binaryName = name; + try (Server server = Server.using( + ServerConfig.builder().binary("/tmp/configured client").build(), tmux)) { + server.requireCompatibleTmux(); + assertEquals(1, tmux.binaryProbes.get()); + assertEquals(0, tmux.effects.get()); + } + } + + @ParameterizedTest + @ValueSource(strings = {"other", "configured", "configured client-helper"}) + void coldVersionBannerMustNameTmuxOrTheExactConfiguredExecutable(String name) { + FakeTmux tmux = new FakeTmux("3.3a"); + tmux.absent = true; + tmux.binaryVersion = "3.3a"; + tmux.binaryName = name; + try (Server server = Server.using( + ServerConfig.builder().binary("/tmp/configured client").build(), tmux)) { + assertThrows(MalformedResponseException.class, server::requireCompatibleTmux); + assertEquals(0, tmux.effects.get()); + } + } + + @ParameterizedTest + @ValueSource(strings = {"3.3rc1", "3.3a+unknown", "3.3a trailing"}) + void renamedExecutableStillRequiresAnExactVersionToken(String version) { + FakeTmux tmux = new FakeTmux("3.3a"); + tmux.absent = true; + tmux.binaryVersion = version; + tmux.binaryName = "configured-client"; + try (Server server = + Server.using(ServerConfig.builder().binary("configured-client").build(), tmux)) { + assertThrows(MalformedResponseException.class, server::requireCompatibleTmux); + assertEquals(0, tmux.effects.get()); + } + } + + @Test + void anExecutableBannerNeverBecomesADaemonVersionToken() { + FakeTmux tmux = new FakeTmux("configured-client 3.3a"); + try (Server server = + Server.using(ServerConfig.builder().binary("configured-client").build(), tmux)) { + assertThrows(MalformedResponseException.class, server::requireCompatibleTmux); + assertEquals(0, tmux.binaryProbes.get()); + assertEquals(0, tmux.effects.get()); + } + } + + @Test + void aDaemonReplacementCannotPassTheVersionProbeThenReceiveAMutation() { + FakeTmux tmux = new FakeTmux("3.3a"); + tmux.liveVersion = "3.2a"; + try (Server server = Server.using(ServerConfig.builder().build(), tmux)) { + assertThrows(TargetGoneException.class, () -> server.cmd("set-option", "-g", "@sentinel", "changed")); + assertEquals(0, tmux.effects.get()); + } + } + + @Test + void theProbeRunsNoCommandThatFiresAHook() { + FakeTmux tmux = new FakeTmux("3.3a"); + List probes = new ArrayList<>(); + TmuxTransport recording = new TmuxTransport() { + @Override + public CommandResult execute(CommandRequest request) { + if (VersionProbes.is(request)) probes.add(request); + return tmux.execute(request); + } + + @Override + public void close() {} + }; + try (Server server = Server.using(ServerConfig.builder().build(), recording)) { + assertTrue(server.cmd("list-sessions").succeeded()); + } + assertEquals(1, probes.size()); + List probe = probes.getFirst().commands().getFirst(); + assertEquals(List.of("run-shell", "-C"), probe.subList(0, 2)); + assertTrue(probes.getFirst().endpoint().contains("-N"), "a probe must never start a daemon"); + assertTrue(tmux.sawNoCommand("display-message"), "the probe must not use display-message"); + } + + @Test + void anotherRequestsMarkerCannotAnswerTheProbe() { + FakeTmux tmux = new FakeTmux("3.8"); + tmux.probeFailure = + new CommandResult(1, List.of(), List.of("unknown command: libtmux-version-0000000000000000-3.8")); + try (Server server = Server.using(ServerConfig.builder().build(), tmux)) { + assertThrows(CommandRejectedException.class, () -> server.cmd("set-option", "-g", "@sentinel", "x")); + assertEquals(0, tmux.effects.get()); + } + } + + @ParameterizedTest + @ValueSource(strings = {"master", "next-3.9x", "3.8-beta"}) + void aDevelopmentSpellingTheParserDoesNotKnowIsRefused(String version) { + FakeTmux tmux = new FakeTmux(version); + try (Server server = Server.using(ServerConfig.builder().build(), tmux)) { + assertThrows(MalformedResponseException.class, () -> server.cmd("set-option", "-g", "@sentinel", "x")); + assertEquals(0, tmux.effects.get()); + } + } + + @Test + void theNextDevelopmentBuildIsSupported() { + FakeTmux tmux = new FakeTmux("next-3.9"); + try (Server server = Server.using(ServerConfig.builder().build(), tmux)) { + assertTrue(server.cmd("set-option", "-g", "@sentinel", "x").succeeded()); + assertEquals(1, tmux.effects.get()); + } + } + + @Test + void versionIsAskedOncePerDaemonAndReadAgainWhenTheFenceRefuses() { + FakeTmux tmux = new FakeTmux("3.3a"); + List probes = new ArrayList<>(); + TmuxTransport recording = new TmuxTransport() { + @Override + public CommandResult execute(CommandRequest request) { + if (VersionProbes.is(request)) probes.add(request); + return tmux.execute(request); + } + + @Override + public void close() {} + }; + try (Server server = Server.using(ServerConfig.builder().build(), recording)) { + assertTrue(server.cmd("set-option", "-g", "@sentinel", "one").succeeded()); + assertTrue(server.cmd("set-option", "-g", "@sentinel", "two").succeeded()); + assertEquals(1, probes.size(), "a known daemon is not asked again"); + + tmux.liveVersion = "3.8"; + tmux.reportedVersion = "3.8"; + assertTrue(server.cmd("set-option", "-g", "@sentinel", "three").succeeded()); + assertEquals(2, probes.size(), "a refused fence reads the daemon again"); + assertEquals(3, tmux.effects.get()); + } + } + + @Test + void aDaemonReplacedByAnOlderOneRaisesTheVersionError() { + FakeTmux tmux = new FakeTmux("3.8"); + try (Server server = Server.using(ServerConfig.builder().build(), tmux)) { + assertTrue(server.cmd("set-option", "-g", "@sentinel", "one").succeeded()); + tmux.liveVersion = "3.2a"; + tmux.reportedVersion = "3.2a"; + assertThrows(UnsupportedFeatureException.class, () -> server.cmd("set-option", "-g", "@sentinel", "two")); + assertEquals(1, tmux.effects.get(), "the refused call ran nothing"); + } + } + + @Test + void aFailedHandshakeNeverFallsBackToTheClientVersion() { + FakeTmux tmux = new FakeTmux("3.3a"); + tmux.probeFailure = new CommandResult(1, List.of(), List.of("server exited unexpectedly")); + try (Server server = Server.using(ServerConfig.builder().build(), tmux)) { + CommandRejectedException failure = + assertThrows(CommandRejectedException.class, () -> server.cmd("new-session", "-d")); + assertTrue(String.valueOf(failure.getMessage()).contains("server exited unexpectedly")); + assertEquals(0, tmux.effects.get()); + assertEquals(0, tmux.binaryProbes.get()); + } + } + + @Test + void aDaemonThatExitsUnderTheProbeIsNotAlive() { + FakeTmux tmux = new FakeTmux("3.3a"); + tmux.probeFailure = new CommandResult(1, List.of(), List.of("server exited unexpectedly")); + try (Server server = Server.using(ServerConfig.builder().build(), tmux)) { + assertFalse(server.isAlive()); + assertThrows(CommandRejectedException.class, () -> server.cmd("list-sessions")); + assertEquals(0, tmux.effects.get()); + } + } + + @Test + void unknownDaemonVersionFailsBeforeEffects() { + FakeTmux tmux = new FakeTmux("unknown"); + try (Server server = Server.using(ServerConfig.builder().build(), tmux)) { + assertThrows( + MalformedResponseException.class, () -> server.cmd("set-option", "-g", "@sentinel", "changed")); + assertEquals(0, tmux.effects.get()); + } + } + + @ParameterizedTest + @ValueSource(strings = {"3.3rc1", "next-3.3a", "3.3a+unknown", "3.3a trailing", "3.3abc"}) + void malformedFullTokensNeverInheritAPrefixsMinimum(String version) { + FakeTmux tmux = new FakeTmux(version); + try (Server server = Server.using(ServerConfig.builder().build(), tmux)) { + assertThrows( + MalformedResponseException.class, () -> server.cmd("set-option", "-g", "@sentinel", "changed")); + assertEquals(0, tmux.effects.get()); + } + } + + @Test + void aCandidateForTheMinimumPatchIsStillOlderThanItsRelease() { + FakeTmux tmux = new FakeTmux("3.3a-rc1"); + try (Server server = Server.using(ServerConfig.builder().build(), tmux)) { + assertThrows( + UnsupportedFeatureException.class, () -> server.cmd("set-option", "-g", "@sentinel", "changed")); + assertEquals(0, tmux.effects.get()); + } + } + + @Test + void rawIfShellFlagsRemainLiteral() { + List dispatched = new ArrayList<>(); + TmuxTransport transport = new TmuxTransport() { + @Override + public CommandResult execute(CommandRequest request) { + if (VersionProbes.is(request)) { + return VersionProbes.reply(request, "3.3a"); + } + dispatched.add(request); + return new CommandResult(0, List.of(), List.of()); + } + + @Override + public void close() {} + }; + List original = List.of("if-shell", "-F", "-b", "1", "set-option -g @sentinel changed", ""); + try (Server server = Server.using(ServerConfig.builder().build(), transport)) { + assertTrue(server.cmd(original).succeeded()); + assertTrue(dispatched.getFirst().commands().contains(original)); + } + } + + @ParameterizedTest + @ValueSource(strings = {"", " "}) + void aVersionCheckNoOpDoesNotTurnReadsIntoMutations(String branch) { + CommandRequest request = new CommandRequest( + List.of("tmux"), + List.of(List.of("if-shell", "-F", "1", branch, "libtmux-stale-version"), List.of("list-sessions")), + Duration.ofSeconds(1), + ""); + assertEquals(Idempotence.IDEMPOTENT, request.idempotence()); + } + + @Test + void aRawCommandFailureCannotImpersonateTheVersionMarker() { + TmuxTransport transport = new TmuxTransport() { + @Override + public CommandResult execute(CommandRequest request) { + return VersionProbes.is(request) + ? VersionProbes.reply(request, "3.3a") + : new CommandResult(1, List.of(), List.of("unknown command: libtmux-stale-version")); + } + + @Override + public void close() {} + }; + try (Server server = Server.using(ServerConfig.builder().build(), transport)) { + CommandResult result = server.cmd("libtmux-stale-version"); + assertFalse(result.succeeded()); + assertEquals(List.of("unknown command: libtmux-stale-version"), result.stderr()); + } + } + + @Test + void clientVersionDiagnosticsRemainAvailableBelowTheCoreFloor() { + FakeTmux tmux = new FakeTmux("3.3"); + tmux.binaryVersion = "3.2a"; + try (Server server = Server.using(ServerConfig.builder().build(), tmux)) { + assertEquals(List.of("tmux 3.2a"), server.cmd("-V").stdout()); + assertEquals(1, tmux.binaryProbes.get()); + assertEquals(0, tmux.effects.get()); + } + } + + @Test + void compatibilityPreflightDoesNotTreatADiagnosticAsAVersionCheck() { + FakeTmux tmux = new FakeTmux("3.3a"); + tmux.absent = true; + tmux.binaryVersion = "3.3"; + try (Server server = Server.using(ServerConfig.builder().build(), tmux)) { + assertEquals(List.of("tmux 3.3"), server.cmd("-V").stdout()); + UnsupportedFeatureException failure = + assertThrows(UnsupportedFeatureException.class, server::requireCompatibleTmux); + assertTrue(String.valueOf(failure.getMessage()).contains("configured binary")); + assertEquals(0, tmux.effects.get()); + } + } + + @Test + void compatibilityPreflightUsesTheLiveDaemonAndNeverStartsOne() { + FakeTmux live = new FakeTmux("3.3a"); + live.binaryVersion = "3.3"; + try (Server server = Server.using(ServerConfig.builder().build(), live)) { + server.requireCompatibleTmux(); + assertEquals(0, live.binaryProbes.get()); + assertEquals(0, live.effects.get()); + } + FakeTmux fresh = new FakeTmux("3.3a"); + fresh.absent = true; + fresh.binaryVersion = "3.3a"; + try (Server server = Server.using(ServerConfig.builder().build(), fresh)) { + server.requireCompatibleTmux(); + assertEquals(1, fresh.binaryProbes.get()); + assertEquals(0, fresh.effects.get()); + } + } + + @Test + void aSupportedDaemonDoesNotTurnClientVersionInspectionIntoAClientMinimum() { + FakeTmux tmux = new FakeTmux("3.3a"); + tmux.binaryVersion = "3.3"; + try (Server server = Server.using(ServerConfig.builder().build(), tmux)) { + assertEquals(List.of("tmux 3.3"), server.cmd("-V").stdout()); + } + } + + @Test + void standaloneHelpPreservesTheNativeDiagnosticBelowTheFloor() { + List seen = new ArrayList<>(); + CommandResult help = new CommandResult(1, List.of(), List.of("usage: tmux [-hV]")); + TmuxTransport transport = new TmuxTransport() { + @Override + public CommandResult execute(CommandRequest request) { + seen.add(request); + return VersionProbes.is(request) ? VersionProbes.reply(request, "3.3") : help; + } + + @Override + public void close() {} + }; + try (Server server = Server.using(ServerConfig.builder().build(), transport)) { + assertEquals(help, server.cmd("-h")); + } + assertEquals(1, seen.size()); + assertEquals(List.of(List.of("-h")), seen.getFirst().commands()); + } + + @Test + void aDisappearingDaemonCannotRestartAnOlderConfiguredBinary() { + AtomicInteger starts = new AtomicInteger(); + TmuxTransport transport = new TmuxTransport() { + @Override + public CommandResult execute(CommandRequest request) { + if (VersionProbes.is(request)) { + return VersionProbes.reply(request, "3.3a"); + } + if (request.endpoint().contains("-N")) { + return new CommandResult(1, List.of(), List.of("no server running")); + } + starts.incrementAndGet(); + return new CommandResult(0, List.of(), List.of()); + } + + @Override + public void close() {} + }; + try (Server server = + Server.using(ServerConfig.builder().binary("older-tmux").build(), transport)) { + assertFalse(server.cmd("new-session", "-d", "-s", "blocked").succeeded()); + } + assertEquals(0, starts.get(), "a live version check must not authorize a fresh daemon"); + } + + @Test + void aSocketNameEqualToANoStartFlagDoesNotChangeTheVersionCheck() { + List seen = new ArrayList<>(); + TmuxTransport transport = new TmuxTransport() { + @Override + public CommandResult execute(CommandRequest request) { + seen.add(request); + if (VersionProbes.is(request)) { + assertEquals("-N", request.endpoint().get(1), "the probe needs a real no-start flag"); + return new CommandResult(1, List.of(), List.of("no server running")); + } + if (request.commands().equals(List.of(List.of("-V")))) { + return new CommandResult(0, List.of("tmux 3.3a"), List.of()); + } + assertEquals( + List.of("tmux", "-u", "-L", "-N"), request.endpoint(), "cold creation retains its selector"); + return new CommandResult(0, List.of("created"), List.of()); + } + + @Override + public void close() {} + }; + try (Server server = Server.using( + ServerConfig.builder() + .endpoint(ServerEndpoint.namedSocket("-N")) + .build(), + transport)) { + assertEquals(List.of("created"), server.cmd("new-session", "-d").stdout()); + } + assertEquals(3, seen.size(), "daemon probe, binary check and creation"); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void anAbsentProbeCannotAdmitANewlyAppearedOldDaemon(boolean explicitNoStart) { + AtomicInteger effects = new AtomicInteger(); + TmuxTransport transport = new TmuxTransport() { + @Override + public CommandResult execute(CommandRequest request) { + if (VersionProbes.is(request)) { + return new CommandResult(1, List.of(), List.of("no server running")); + } + return GroupedTmux.execute(request, 4242, "3.2a", command -> { + effects.incrementAndGet(); + return new CommandResult(0, List.of(), List.of()); + }); + } + + @Override + public void close() {} + }; + List endpoint = explicitNoStart ? List.of("tmux", "-N") : List.of("tmux"); + List command = + explicitNoStart ? List.of("set-option", "-g", "@sentinel", "changed") : List.of("list-sessions"); + CommandRequest request = CommandRequest.of(endpoint, command, Duration.ofSeconds(1)); + assertThrows(TargetGoneException.class, () -> Versions.execute(transport, request, false)); + assertEquals(0, effects.get()); + } + + @Test + void absentDaemonReadsAndCleanupPreserveNativeResults() { + FakeTmux tmux = new FakeTmux("3.3a"); + tmux.absent = true; + try (Server server = Server.using(ServerConfig.builder().build(), tmux)) { + CommandResult result = server.cmd("list-sessions"); + assertFalse(result.succeeded()); + assertEquals(List.of("no server running"), result.stderr()); + server.killServer(); + assertEquals(0, tmux.effects.get()); + assertEquals(0, tmux.binaryProbes.get()); + } + } + + @Test + void versionCheckConsumesTheSameDeadlineAsDispatch() { + FakeTmux tmux = new FakeTmux("3.3a"); + tmux.probeDelay = Duration.ofMillis(30); + try (Server server = Server.using(ServerConfig.builder().build(), tmux)) { + DispatchException.TimedOut failure = assertThrows( + DispatchException.TimedOut.class, + () -> server.cmd(List.of("set-option", "-g", "@sentinel", "changed"), Duration.ofMillis(10))); + assertEquals(DispatchOutcome.NOT_DISPATCHED, failure.outcome()); + assertTrue(failure.safeToRetry()); + assertEquals(0, tmux.effects.get()); + } + } + + @Test + void probeTimeoutDoesNotClaimTheRequestedMutationWasDispatched() { + FakeTmux tmux = new FakeTmux("3.3a") { + @Override + public CommandResult execute(CommandRequest request) { + throw new DispatchException.TimedOut("probe timed out", DispatchOutcome.UNKNOWN, null); + } + }; + try (Server server = Server.using(ServerConfig.builder().build(), tmux)) { + DispatchException.TimedOut failure = + assertThrows(DispatchException.TimedOut.class, () -> server.cmd("kill-server")); + assertEquals(DispatchOutcome.NOT_DISPATCHED, failure.outcome()); + assertTrue(failure.safeToRetry()); + } + } + + private static class FakeTmux implements TmuxTransport { + private String reportedVersion; + private String liveVersion; + private String binaryVersion = "3.8"; + private String binaryName = "tmux"; + private boolean absent; + private CommandResult probeFailure = new CommandResult(0, List.of(), List.of()); + private Duration probeDelay = Duration.ZERO; + private final AtomicInteger effects = new AtomicInteger(); + private final AtomicInteger binaryProbes = new AtomicInteger(); + private final List verbs = new ArrayList<>(); + + boolean sawNoCommand(String verb) { + return !verbs.contains(verb); + } + + FakeTmux(String version) { + reportedVersion = version; + liveVersion = version; + } + + @Override + public CommandResult execute(CommandRequest request) { + request.commands().forEach(command -> verbs.add(command.getFirst())); + if (VersionProbes.is(request)) { + try { + Thread.sleep(probeDelay); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw new AssertionError(interrupted); + } + if (!probeFailure.succeeded()) { + return probeFailure; + } + return absent + ? new CommandResult(1, List.of(), List.of("no server running")) + : VersionProbes.reply(request, reportedVersion); + } + if (request.commands().equals(List.of(List.of("-V")))) { + binaryProbes.incrementAndGet(); + return new CommandResult(0, List.of(binaryName + " " + binaryVersion), List.of()); + } + if (absent) { + return new CommandResult(1, List.of(), List.of("no server running")); + } + return GroupedTmux.execute(request, 4242, liveVersion, command -> { + effects.incrementAndGet(); + return new CommandResult(0, List.of("ok"), List.of()); + }); + } + + @Override + public void close() {} + } +} diff --git a/libtmux/src/test/java/io/github/libtmux/NamesTest.java b/libtmux/src/test/java/io/github/libtmux/NamesTest.java new file mode 100644 index 000000000..a5d46ec9e --- /dev/null +++ b/libtmux/src/test/java/io/github/libtmux/NamesTest.java @@ -0,0 +1,26 @@ +package io.github.libtmux; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +final class NamesTest { + + @ParameterizedTest + @ValueSource(strings = {"1.0", "a:b", "x.", ":y"}) + void exactly37RefusesADelimiterButEveryOtherReleaseKeepsIt(String name) { + assertThrows(IllegalArgumentException.class, () -> Names.require(name, TmuxVersion.parse("3.7"))); + assertThrows(IllegalArgumentException.class, () -> Names.require(name, TmuxVersion.parse("3.7-rc2"))); + for (String other : new String[] {"3.3a", "3.6b", "3.7a", "3.7d", "3.8", "next-3.7", "next-3.9"}) { + assertEquals(name, Names.require(name, TmuxVersion.parse(other)), other); + } + } + + @ParameterizedTest + @ValueSource(strings = {"plain", "with space", "under_score"}) + void aPlainNameIsAlwaysAccepted(String name) { + assertEquals(name, Names.require(name, TmuxVersion.parse("3.7"))); + } +} diff --git a/libtmux/src/test/java/io/github/libtmux/NarrowLookupTest.java b/libtmux/src/test/java/io/github/libtmux/NarrowLookupTest.java index cce5b53d6..1d80b4738 100644 --- a/libtmux/src/test/java/io/github/libtmux/NarrowLookupTest.java +++ b/libtmux/src/test/java/io/github/libtmux/NarrowLookupTest.java @@ -24,6 +24,9 @@ void anAbsentSessionCostsOneFencedRead() { TmuxTransport transport = new TmuxTransport() { @Override public CommandResult execute(CommandRequest request) { + if (VersionProbes.is(request)) { + return VersionProbes.reply(request, "3.6"); + } String flat = request.commands().stream() .flatMap(List::stream) .reduce("", (left, right) -> left + " " + right); @@ -57,7 +60,7 @@ void matchingSessionsAreReadInOneFencedCall() { TmuxTransport transport = new TmuxTransport() { @Override public CommandResult execute(CommandRequest request) { - requests.add(request); + if (!VersionProbes.is(request)) requests.add(request); return GroupedTmux.execute(request, 4242L, argv -> { String template = argv.get(argv.indexOf("-F") + 1); return switch (argv.get(0)) { @@ -122,7 +125,7 @@ private static TmuxTransport twoSessions(List requests) { return new TmuxTransport() { @Override public CommandResult execute(CommandRequest request) { - requests.add(request); + if (!VersionProbes.is(request)) requests.add(request); return GroupedTmux.execute(request, 4242L, argv -> { String template = argv.get(argv.indexOf("-F") + 1); return switch (argv.get(0)) { @@ -197,7 +200,7 @@ void aSafePaneExpressionIsSentAsAFormat() { assertTrue(server.panes(Pane_.command().is("nvim")).isEmpty()); } String all = String.join("\n", sent); - assertTrue(all.contains("-f"), all); + assertTrue(all.contains("'-f'"), all); assertTrue(all.contains("#{==:#{pane_current_command},nvim}"), all); } @@ -230,7 +233,7 @@ void aWindowRelationStaysLocal() { assertTrue(server.windows(Window_.panes().any(Pane_.command().is("nvim"))) .isEmpty()); } - assertFalse(String.join("\n", sent).contains("-f")); + assertFalse(String.join("\n", sent).contains("'-f'")); } @Test @@ -239,7 +242,7 @@ void anUnsafePaneExpressionStaysLocal() { try (Server server = Server.using(ServerConfig.builder().build(), answering(sent))) { assertTrue(server.panes(Pane_.command().is("a,b")).isEmpty()); } - assertFalse(String.join("\n", sent).contains("-f")); + assertFalse(String.join("\n", sent).contains("'-f'")); } private static List commands() { @@ -252,6 +255,9 @@ private static TmuxTransport answering(List sent) { return new TmuxTransport() { @Override public CommandResult execute(CommandRequest request) { + if (VersionProbes.is(request)) { + return VersionProbes.reply(request, "3.6"); + } String flat = request.commands().stream() .flatMap(List::stream) .reduce("", (left, right) -> left + " " + right); diff --git a/libtmux/src/test/java/io/github/libtmux/PaneWaitTest.java b/libtmux/src/test/java/io/github/libtmux/PaneWaitTest.java index 9e5ad75b5..07cc46906 100644 --- a/libtmux/src/test/java/io/github/libtmux/PaneWaitTest.java +++ b/libtmux/src/test/java/io/github/libtmux/PaneWaitTest.java @@ -166,7 +166,9 @@ public CommandResult execute(CommandRequest request) { var _ = server.within(bound).panes().get(0).capture(); assertTrue(seen.size() >= 2, "a listing and a capture both reached tmux"); - assertTrue(seen.stream().allMatch(bound::equals), "every command carried the chosen deadline: " + seen); + assertTrue( + seen.stream().allMatch(timeout -> timeout.isPositive() && timeout.compareTo(bound) <= 0), + "the version check and command share the chosen deadline: " + seen); assertThrows(IllegalArgumentException.class, () -> server.within(Duration.ZERO)); } } diff --git a/libtmux/src/test/java/io/github/libtmux/ServerTest.java b/libtmux/src/test/java/io/github/libtmux/ServerTest.java index 6f2493a08..5d654b046 100644 --- a/libtmux/src/test/java/io/github/libtmux/ServerTest.java +++ b/libtmux/src/test/java/io/github/libtmux/ServerTest.java @@ -56,7 +56,7 @@ void optionReadsUseDaemonEncodingAndPreserveMetadataFailures(@TempDir Path direc @Override public CommandResult execute(CommandRequest request) { requests.add(request); - return GroupedTmux.execute(request, 4242L, argv -> switch (argv.getFirst()) { + return GroupedTmux.execute(request, 4242L, "3.4", argv -> switch (argv.getFirst()) { case "display-message" -> argv.getLast().equals("#{version}") ? version.get() @@ -71,7 +71,10 @@ public void close() {} }; try (Server server = Server.using(config(directory), transport)) { assertEquals(java.util.Optional.of("a\rb"), server.globalOptions().get("@value")); - assertEquals(1, requests.size(), "the version and the value come from one invocation"); + assertEquals( + 2, + requests.size(), + "the version check precedes the invocation that reads version and value together"); assertTrue(requests.getLast().commands().contains(List.of("show-options", "-g", "-A", "--", "@value"))); value.set(new CommandResult(0, List.of("@value a\\377\\376é"), List.of())); assertEquals( @@ -88,9 +91,7 @@ public void close() {} assertThrows( ServerUnavailableException.class, () -> server.globalOptions().get("@value")); - assertEquals( - List.of("display-message", "-p", "#{version}"), - requests.getLast().commands().getFirst()); + assertTrue(requests.getLast().commands().contains(List.of("display-message", "-p", "#{version}"))); } } @@ -227,9 +228,12 @@ void aChosenDeadlineBindsOnlyTheCallThatChoseIt(@TempDir Path directory) throws server.killServer(chosen); server.isAlive(); - assertEquals( - List.of(chosen, chosen, server.config().defaultTimeout()), - transport.requests.stream().map(CommandRequest::timeout).toList()); + List budgets = List.of(chosen, chosen, server.config().defaultTimeout()); + assertEquals(budgets.size(), transport.requests.size()); + for (int index = 0; index < budgets.size(); index++) { + Duration sent = transport.requests.get(index).timeout(); + assertTrue(sent.isPositive() && sent.compareTo(budgets.get(index)) <= 0); + } } } @@ -249,10 +253,10 @@ void theConfirmingLookIsBoundedToo(@TempDir Path directory) throws IOException { try (Server server = Server.using(config(directory), refusing)) { server.killServer(chosen); - assertEquals( - List.of(chosen, chosen), - refusing.requests.stream().map(CommandRequest::timeout).toList(), - "the kill and the look that confirms it"); + assertEquals(2, refusing.requests.size(), "the kill and the look that confirms it"); + for (CommandRequest request : refusing.requests) { + assertTrue(request.timeout().isPositive() && request.timeout().compareTo(chosen) <= 0); + } } } @@ -297,7 +301,10 @@ void aWaitPropagatesTransportFailuresThatAreNotItsDeadline(@TempDir Path directo TmuxTransport transport = new TmuxTransport() { @Override public CommandResult execute(CommandRequest request) { - if (request.commands().get(0).contains("wait-for")) { + if (VersionProbes.is(request)) { + return VersionProbes.reply(request, "3.6"); + } + if (request.commands().stream().anyMatch(command -> command.contains("wait-for"))) { throw failure; } return new CommandResult(0, List.of("4242"), List.of()); @@ -325,7 +332,10 @@ void aWaitWithSignalCapacityPreservesAPredispatchTimeout(@TempDir Path directory TmuxTransport transport = new TmuxTransport() { @Override public CommandResult execute(CommandRequest request) { - if (request.commands().get(0).contains("wait-for")) { + if (VersionProbes.is(request)) { + return VersionProbes.reply(request, "3.6"); + } + if (request.commands().stream().anyMatch(command -> command.contains("wait-for"))) { throw failure; } return new CommandResult(1, List.of(), List.of("no server running")); @@ -354,6 +364,61 @@ public void close() {} } } + @Test + void waitingGroupsReserveAdmissionAndPreserveTransportFailure(@TempDir Path directory) throws IOException { + AtomicInteger ordinary = new AtomicInteger(); + AtomicInteger waiting = new AtomicInteger(); + DispatchException failure = + new DispatchException.Failed("admission refused", DispatchOutcome.NOT_DISPATCHED, null); + TmuxTransport transport = new TmuxTransport() { + @Override + public CommandResult execute(CommandRequest request) { + if (VersionProbes.is(request)) { + return VersionProbes.reply(request, "3.6"); + } + ordinary.incrementAndGet(); + throw failure; + } + + @Override + public CommandResult executeWaiting(CommandRequest request) { + waiting.incrementAndGet(); + throw failure; + } + + @Override + public void close() {} + }; + try (Server server = Server.using(config(directory), transport)) { + assertSame( + failure, + assertThrows( + DispatchException.class, + () -> server.batch().add("display-message", "one").run())); + assertSame( + failure, + assertThrows( + DispatchException.class, + () -> server.chain().then("display-message", "two").run())); + assertSame( + failure, + assertThrows( + DispatchException.class, + () -> server.batchReservingCapacity() + .add("wait-for", "one") + .run())); + assertSame( + failure, + assertThrows( + DispatchException.class, + () -> server.chainReservingCapacity() + .then("wait-for", "two") + .run())); + assertEquals(2, ordinary.get()); + assertEquals(2, waiting.get()); + } + } + @Test void liveReadsPreserveMalformedOrInconsistentCaptures(@TempDir Path directory) throws IOException { String separator = RowFormat.of("field").separator(); @@ -422,8 +487,14 @@ public CommandResult execute(CommandRequest request) { public void close() {} }; try (Server server = Server.using(config(directory), transport)) { - assertAll(liveReads(server).stream() - .map(read -> () -> assertSame(failure, assertThrows(DispatchException.class, read)))); + assertAll(liveReads(server).stream().map(read -> () -> { + DispatchException seen = assertThrows(DispatchException.class, read); + assertEquals(failure.getClass(), seen.getClass()); + assertEquals(failure.getMessage(), seen.getMessage()); + assertSame(cause, seen.getCause()); + assertEquals(DispatchOutcome.NOT_DISPATCHED, seen.outcome()); + assertTrue(seen.safeToRetry()); + })); } } } @@ -500,6 +571,7 @@ void showingABufferDistinguishesAnAbsentDaemonFromAMissingName(@TempDir Path dir private static List liveReads(Server server) { return List.of( server::snapshot, + server::captureServer, server::sessions, server::windows, server::panes, @@ -520,11 +592,11 @@ void snapshotKeepsTheIdentityOfALiveServerWithNoSessions(@TempDir Path directory @Override public CommandResult execute(CommandRequest request) { requests.incrementAndGet(); - return GroupedTmux.execute(request, 4242L, "3.2a", argv -> switch (argv.getFirst()) { + return GroupedTmux.execute(request, 4242L, "3.3a", argv -> switch (argv.getFirst()) { case "display-message" -> new CommandResult( 0, - List.of(String.join(RowFormat.of("field").separator(), "4242", "3.2a", "1790000000")), + List.of(String.join(RowFormat.of("field").separator(), "4242", "3.3a", "1790000000")), List.of()); case "list-sessions" -> new CommandResult(0, List.of(), List.of()); // tmux has no current target to list children against, and says so. @@ -540,12 +612,12 @@ public void close() {} var snapshot = server.snapshot(); assertEquals(4242L, snapshot.serverPid().orElseThrow()); - assertEquals(TmuxVersion.parse("3.2a"), snapshot.serverVersion().orElseThrow()); + assertEquals(TmuxVersion.parse("3.3a"), snapshot.serverVersion().orElseThrow()); assertTrue(snapshot.sessions().isEmpty()); assertTrue(snapshot.windows().isEmpty()); assertTrue(snapshot.panes().isEmpty()); assertTrue(snapshot.clients().isEmpty()); - assertEquals(2, requests.get(), "tmux refused the rest of the group, which cost no further request"); + assertEquals(3, requests.get(), "two data invocations share one version probe"); assertEquals(List.of(), server.sessions()); assertEquals(List.of(), server.windows()); assertEquals(List.of(), server.panes()); @@ -749,13 +821,14 @@ void controlRefusesATransportThatStartsNoClient(@TempDir Path directory) throws TmuxTransport transport = new TmuxTransport() { @Override public CommandResult execute(CommandRequest request) { - List argv = request.commands().get(0); - if (argv.stream().anyMatch(word -> word.contains("#{pid} #{version}"))) { - return new CommandResult(0, List.of("4242 3.6"), List.of()); + if (request.commands().stream() + .flatMap(List::stream) + .anyMatch(word -> word.contains("#{pid} #{version}"))) { + return new CommandResult(0, List.of("4242 3.7c"), List.of()); } - return GroupedTmux.execute(request, 4242L, "3.6", command -> switch (command.get(0)) { + return GroupedTmux.execute(request, 4242L, "3.7c", command -> switch (command.get(0)) { case "display-message" -> - new CommandResult(0, List.of(String.join(separator, "4242", "3.6", "1790000000")), List.of()); + new CommandResult(0, List.of(String.join(separator, "4242", "3.7c", "1790000000")), List.of()); case "list-sessions" -> new CommandResult(0, List.of(String.join(separator, "$0", "only", "1", "1")), List.of()); case "list-windows" -> @@ -768,7 +841,7 @@ public CommandResult execute(CommandRequest request) { 0, List.of(String.join( separator, "$0", "@0", "0", "%0", "0", "1", "zsh", "80", "24", "0", "0", "", - "/tmp", "", "0", "0", "0", "0")), + "/tmp", "", "0", "0", "0", "0", "0")), List.of()); default -> new CommandResult(0, List.of(), List.of()); }); @@ -790,31 +863,12 @@ public void close() {} // --------------------------------------------------------------------------- version gates - /** - * The guard asks the daemon rather than trusting a version string, so it cannot fail the way a - * version predicate can: a floor fitted to the two adjacent releases a matrix happens to run, - * one lettered patch off from what tmux's own history says. Proven by making the two disagree - - * a version the constant would refuse, with {@code list-commands} naming the command anyway, and - * the reverse - and checking the daemon's answer is the one that wins. - */ @Test - void promptHistoryAsksTheDaemonRatherThanTrustingAVersionString(@TempDir Path directory) throws IOException { - try (Server server = - Server.using(config(directory), listingCommands("3.2a", "show-prompt-history (showphist) [-T type]"))) { - assertDoesNotThrow( - () -> server.prompt().history(), "list-commands names it, so a low version must not refuse"); + void promptHistoryUsesTheSupportedCommandDirectly(@TempDir Path directory) throws IOException { + try (Server server = Server.using(config(directory), listingCommands("3.3a"))) { + assertDoesNotThrow(() -> server.prompt().history()); assertDoesNotThrow(() -> server.prompt().clear()); } - try (Server server = Server.using(config(directory), listingCommands("99.0"))) { - UnsupportedFeatureException refused = assertThrows( - UnsupportedFeatureException.class, () -> server.prompt().history()); - - assertTrue( - String.valueOf(refused.getMessage()).contains("3.3"), - "the refusal still names the release most callers will recognise: " + refused.getMessage()); - assertThrows( - UnsupportedFeatureException.class, () -> server.prompt().clear()); - } } /** @@ -1014,19 +1068,15 @@ void aSilentExitUnderAMissingSocketDirectoryNamesTheDirectory(@TempDir Path dire * with {@code lines}, whatever the real relationship between the two would be. */ private static TmuxTransport listingCommands(String version, String... lines) { - String separator = RowFormat.of("field").separator(); return new TmuxTransport() { @Override public CommandResult execute(CommandRequest request) { - List argv = request.commands().get(0); - if (argv.get(0).equals("display-message")) { - return new CommandResult( - 0, List.of(String.join(separator, "4242", version, "1790000000")), List.of()); - } - if (argv.get(0).equals("list-commands")) { - return new CommandResult(0, List.of(lines), List.of()); - } - return new CommandResult(0, List.of(), List.of()); + return GroupedTmux.execute( + request, + 4242L, + version, + command -> new CommandResult( + 0, command.getFirst().equals("list-commands") ? List.of(lines) : List.of(), List.of())); } @Override @@ -1088,7 +1138,9 @@ void killingThroughAnotherTransportWaitsOnNoLocalProcess(@TempDir Path directory TmuxTransport transport = new TmuxTransport() { @Override public CommandResult execute(CommandRequest request) { - return new CommandResult(0, List.of(Long.toString(sleeper.pid())), List.of()); + return VersionProbes.is(request) + ? VersionProbes.reply(request, "3.6") + : new CommandResult(0, List.of(Long.toString(sleeper.pid())), List.of()); } @Override @@ -1135,6 +1187,9 @@ private record RefusingTransport(String stderr) implements TmuxTransport { @Override public CommandResult execute(CommandRequest request) { + if (VersionProbes.is(request)) { + return VersionProbes.reply(request, "3.6"); + } return new CommandResult(1, List.of(), List.of(stderr)); } @@ -1147,6 +1202,9 @@ private static final class SurvivingTransport implements TmuxTransport { @Override public CommandResult execute(CommandRequest request) { + if (VersionProbes.is(request)) { + return VersionProbes.reply(request, "3.6"); + } return request.commands().stream().anyMatch(command -> command.contains("kill-server")) ? new CommandResult(1, List.of(), List.of("permission denied")) : new CommandResult(0, List.of("4242"), List.of()); @@ -1175,6 +1233,9 @@ private static final class RecordingTransport implements TmuxTransport { @Override public CommandResult execute(CommandRequest request) { + if (VersionProbes.is(request)) { + return VersionProbes.reply(request, "3.6"); + } executions.incrementAndGet(); requests.add(request); return new CommandResult(exitCode, List.of("ok"), List.of()); @@ -1223,14 +1284,15 @@ private static final class SnapshotRaceTransport implements TmuxTransport { */ @Override public CommandResult execute(CommandRequest request) { - if (request.commands().get(0).get(0).equals("display-message")) { - return identity(at(2 * identityReads.getAndIncrement())); + if (VersionProbes.is(request)) { + return at(2 * identityReads.get()).isEmpty() ? identity("") : VersionProbes.reply(request, "3.6"); } - String live = at(2 * (identityReads.get() - 1) + 1); + String live = at(Math.max(0, 2 * (identityReads.get() - 1) + 1)); if (live.isEmpty()) { - return new CommandResult(1, List.of(), List.of("no server running on /tmp/s")); + return identity(""); } return GroupedTmux.execute(request, Long.parseLong(live), argv -> switch (argv.get(0)) { + case "display-message" -> identity(at(2 * identityReads.getAndIncrement())); case "list-sessions" -> new CommandResult(0, List.of(sessionRows.get(sessionReads.getAndIncrement())), List.of()); default -> new CommandResult(0, List.of(), List.of()); diff --git a/libtmux/src/test/java/io/github/libtmux/TmuxVersionTest.java b/libtmux/src/test/java/io/github/libtmux/TmuxVersionTest.java index f05a605d5..0e4b8cbc0 100644 --- a/libtmux/src/test/java/io/github/libtmux/TmuxVersionTest.java +++ b/libtmux/src/test/java/io/github/libtmux/TmuxVersionTest.java @@ -68,12 +68,12 @@ void aReleaseCandidateRoundTrips() { } /** - * A release candidate has what its release has — tmux freezes features at the candidate, and a - * 3.8 candidate behaves as 3.8 does on every lane-tested gate — so it compares equal to it, above - * the development build tracking toward it. It is still not the release: not {@code equals} to it. + * A release candidate orders above the development build tracking toward it and below its release, + * so the order agrees with {@code equals}. A feature gate still counts it as the release, because + * tmux freezes features at the candidate. */ @Test - void aReleaseCandidateComparesAsItsRelease() { + void aReleaseCandidateOrdersBelowItsReleaseButMeetsItsGates() { TmuxVersion previous = TmuxVersion.parse("3.7c"); TmuxVersion development = TmuxVersion.parse("next-3.8"); TmuxVersion candidate = TmuxVersion.parse("3.8-rc"); @@ -82,7 +82,7 @@ void aReleaseCandidateComparesAsItsRelease() { assertTrue(previous.compareTo(development) < 0); assertTrue(development.compareTo(candidate) < 0, "a development build precedes the candidate"); - assertEquals(0, candidate.compareTo(release), "features are frozen at the candidate"); + assertTrue(candidate.compareTo(release) < 0, "the order agrees with equals"); assertTrue(candidate.compareTo(patch) < 0); assertTrue(candidate.atLeast(release), "a gate on the release is met by its candidate"); assertTrue(!candidate.equals(release), "and the candidate is still not the release"); diff --git a/libtmux/src/test/java/io/github/libtmux/VersionGrammarTest.java b/libtmux/src/test/java/io/github/libtmux/VersionGrammarTest.java new file mode 100644 index 000000000..72161a9fa --- /dev/null +++ b/libtmux/src/test/java/io/github/libtmux/VersionGrammarTest.java @@ -0,0 +1,110 @@ +package io.github.libtmux; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.github.libtmux.exception.MalformedResponseException; +import io.github.libtmux.exception.UnsupportedFeatureException; +import io.github.libtmux.internal.support.Versions; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * The version strings tmux has ever reported, and what a support policy does with each: the + * canonical vectors every port is held to, with floor 3.3a. + */ +final class VersionGrammarTest { + + @ParameterizedTest + @ValueSource( + strings = { + "3.3a", + "3.4", + "3.7d", + "3.8", + "3.10", + "4.0", + "3.4-rc", + "3.8-rc", + "3.8-rc2", + "next-3.4", + "next-3.9" + }) + void supportedAtTheFloor(String reported) { + assertEquals(reported, Versions.require(reported).toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"3.3", "3.2a", "3.3-rc", "3.2-rc5", "next-3.3"}) + void belowTheFloorIsRefusedAsUnsupported(String reported) { + assertThrows(UnsupportedFeatureException.class, () -> Versions.require(reported)); + } + + @ParameterizedTest + @ValueSource( + strings = { + "master", + "openbsd-7.6", + "", + "3", + "garbage", + "3.3a garbage", + "3.3a-master", + "3.3a-dev", + "next-3.3a", + "next-3.9-master" + }) + void unparseableReportsAreRefusedAsMalformed(String reported) { + assertThrows(MalformedResponseException.class, () -> Versions.require(reported)); + } + + @Test + void orderingFollowsTheReleaseTrain() { + assertAscending("next-3.7", "3.7-rc", "3.7-rc3", "3.7", "3.7a"); + assertAscending("next-3.4", "3.4-rc", "3.4"); + assertAscending("3.8-rc2", "3.8-rc10", "3.8"); + assertAscending("3.9", "3.10", "4.0"); + } + + @Test + void aCandidateOrDevelopmentBuildOfAReleaseIsNotThatRelease() { + TmuxVersion control = new TmuxVersion(3, 7, ""); + for (String below : List.of("next-3.7", "3.7-rc", "3.7-rc2")) { + assertTrue(TmuxVersion.parse(below).compareTo(control) < 0, below); + } + for (String atOrAbove : List.of("3.7", "3.7a", "3.8-rc2", "next-3.8")) { + assertTrue(TmuxVersion.parse(atOrAbove).compareTo(control) >= 0, atOrAbove); + } + } + + @ParameterizedTest + @ValueSource(strings = {"3.3a garbage", "3.3a-master", "3.3a-dev", "3.3abc", "next-3.3a-x", "", "3", "master"}) + void parseIsAnchoredToTheCanonicalGrammar(String text) { + assertThrows(IllegalArgumentException.class, () -> TmuxVersion.parse(text)); + } + + @Test + void theNaturalOrderAgreesWithEqualsButAFeatureGateCountsACandidateAsItsRelease() { + TmuxVersion release = TmuxVersion.parse("3.8"); + TmuxVersion candidate = TmuxVersion.parse("3.8-rc2"); + assertTrue(candidate.compareTo(release) < 0); + assertTrue(!candidate.equals(release)); + assertTrue(candidate.atLeast(release), "a candidate ships what its release ships"); + assertTrue(candidate.sameRelease(release)); + assertTrue(!TmuxVersion.parse("next-3.8").sameRelease(release)); + assertTrue(!TmuxVersion.parse("3.8a").sameRelease(release)); + assertEquals(0, release.compareTo(TmuxVersion.parse("3.8"))); + } + + private static void assertAscending(String... versions) { + for (int index = 1; index < versions.length; index++) { + TmuxVersion lower = TmuxVersion.parse(versions[index - 1]); + TmuxVersion higher = TmuxVersion.parse(versions[index]); + assertTrue(lower.compareTo(higher) < 0, versions[index - 1] + " < " + versions[index]); + assertTrue(higher.compareTo(lower) > 0, versions[index] + " > " + versions[index - 1]); + } + } +} diff --git a/libtmux/src/test/java/io/github/libtmux/VersionProbes.java b/libtmux/src/test/java/io/github/libtmux/VersionProbes.java new file mode 100644 index 000000000..5eaa99ae7 --- /dev/null +++ b/libtmux/src/test/java/io/github/libtmux/VersionProbes.java @@ -0,0 +1,39 @@ +package io.github.libtmux; + +import io.github.libtmux.transport.CommandRequest; +import io.github.libtmux.transport.CommandResult; +import java.util.List; + +/** + * The version probe as a test double sees it, and tmux's answer to it. + * + *

The probe is {@code run-shell -C '#{version}'}: the daemon expands the format, fails to + * parse the result as a command, and spells the version in {@code unknown command: }. + */ +final class VersionProbes { + + private static final String MARKER = "libtmux-version-"; + private static final String FORMAT = "#{version}"; + + private VersionProbes() {} + + /** Whether this request is the version probe and nothing else. */ + static boolean is(CommandRequest request) { + return request.commands().size() == 1 && is(request.commands().getFirst()); + } + + static boolean is(List command) { + return command.size() == 3 + && command.get(0).equals("run-shell") + && command.get(1).equals("-C") + && command.get(2).startsWith(MARKER) + && command.get(2).endsWith(FORMAT); + } + + /** The reply of a daemon that reports this version. */ + static CommandResult reply(CommandRequest request, String version) { + String argument = request.commands().getFirst().get(2); + String marker = argument.substring(0, argument.length() - FORMAT.length()); + return new CommandResult(1, List.of(), List.of("unknown command: " + marker + version)); + } +} diff --git a/libtmux/src/test/java/io/github/libtmux/control/ControlClientTest.java b/libtmux/src/test/java/io/github/libtmux/control/ControlClientTest.java index c9383eda7..d9fe0eedb 100644 --- a/libtmux/src/test/java/io/github/libtmux/control/ControlClientTest.java +++ b/libtmux/src/test/java/io/github/libtmux/control/ControlClientTest.java @@ -12,6 +12,7 @@ import io.github.libtmux.exception.ControlEndedException; import io.github.libtmux.exception.DispatchException; import io.github.libtmux.exception.LibTmuxException; +import io.github.libtmux.transport.ControlCarrier; import io.github.libtmux.transport.DispatchOutcome; import java.nio.ByteBuffer; import java.nio.file.Files; @@ -20,10 +21,13 @@ import java.time.Duration; import java.util.List; import java.util.Optional; +import java.util.OptionalLong; import java.util.concurrent.CountDownLatch; import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -36,6 +40,32 @@ */ final class ControlClientTest { + @Test + void aCarrierProbeTimeoutDoesNotAcquireControl() { + AtomicInteger starts = new AtomicInteger(); + AtomicReference probe = new AtomicReference<>(); + ControlCarrier carrier = command -> { + starts.incrementAndGet(); + Process started = new ProcessBuilder("/bin/sh", "-c", "exec sleep 30").start(); + probe.set(started); + return started; + }; + DispatchException.TimedOut failure = assertThrows( + DispatchException.TimedOut.class, + () -> ControlClient.attach( + carrier, + ServerConfig.builder().build(), + new SessionId("$0"), + 4242L, + OptionalLong.empty(), + "3.7c", + Duration.ofMillis(250))); + + assertEquals(DispatchOutcome.NOT_DISPATCHED, failure.outcome()); + assertEquals(1, starts.get(), "only the version probe may start"); + assertFalse(probe.get().isAlive(), "the timed-out version probe survived"); + } + /** A pipe write on the calling virtual thread pins the only carrier and starves the sentinel. */ @Test @Tag("carrier") @@ -48,7 +78,7 @@ void aBlockedControlWriteDoesNotOccupyTheCallersCarrier(@TempDir Path directory) sleep 5 """); - try (ControlClient client = ControlClient.attachUnfenced(config, new SessionId("$0"))) { + try (ControlClient client = attachFake(config)) { Thread blocked = Thread.ofVirtual().start(() -> { try { client.send(List.of("display-message", "x".repeat(1_048_576)), Duration.ofSeconds(4)); @@ -119,7 +149,7 @@ private static String capturedWatchRequest(Path directory, int attempt, String t sleep 5 """); - try (ControlClient client = ControlClient.attachUnfenced(config, new SessionId("$0"))) { + try (ControlClient client = attachFake(config)) { client.watch("javatest", target, "#{session_name}"); assertTrue(awaitFile(captured), "the watch request never reached the fake server"); return Files.readString(captured).strip(); @@ -137,7 +167,7 @@ void aTimedOutReplyMakesTheStreamUnavailableForLaterRequests(@TempDir Path direc sleep 1 """); - try (ControlClient client = ControlClient.attachUnfenced(config, new SessionId("$0")); + try (ControlClient client = attachFake(config); EventSubscription events = client.subscribeEvents(1)) { DispatchException.TimedOut failure = assertThrows( DispatchException.TimedOut.class, @@ -159,7 +189,7 @@ void closingTheClientWakesAWaitingSubscriber(@TempDir Path directory) throws Exc answer IFS= read -r never """); - ControlClient client = ControlClient.attachUnfenced(config, new SessionId("$0")); + ControlClient client = attachFake(config); EventSubscription output = client.subscribeOutput(1); CountDownLatch entered = new CountDownLatch(1); FutureTask>> waiting = new FutureTask<>(() -> { @@ -191,7 +221,7 @@ void aNonPositiveTimeoutIsRejectedBeforeDispatch(@TempDir Path directory) throws done """); - try (ControlClient client = ControlClient.attachUnfenced(config, new SessionId("$0"))) { + try (ControlClient client = attachFake(config)) { assertThrows(IllegalArgumentException.class, () -> client.send(List.of("list-windows"), Duration.ZERO)); assertTrue(client.send("list-panes").succeeded(), "rejection wrote nothing to the stream"); } @@ -206,7 +236,7 @@ void aRejectedNulDoesNotStealTheNextRequestsReply(@TempDir Path directory) throw done """); - try (ControlClient client = ControlClient.attachUnfenced(config, new SessionId("$0"))) { + try (ControlClient client = attachFake(config)) { assertThrows( IllegalArgumentException.class, () -> client.send(List.of("display-message", "contains\0nul"), Duration.ofMillis(200))); @@ -231,7 +261,7 @@ void interruptionIsNotReportedAsADeadlineExpiry(@TempDir Path directory) throws IFS= read -r never """); - try (ControlClient client = ControlClient.attachUnfenced(config, new SessionId("$0"))) { + try (ControlClient client = attachFake(config)) { CountDownLatch entered = new CountDownLatch(1); FutureTask waiting = new FutureTask<>(() -> { entered.countDown(); @@ -263,16 +293,15 @@ void anAttachErrorIsNotAcceptedAsAReadyClient(@TempDir Path directory) throws Ex printf '%%begin 100 1 0\nattach refused\n%%error 100 1 0\n' """); - assertThrows(LibTmuxException.class, () -> ControlClient.attachUnfenced(config, new SessionId("$0"))); + assertThrows(LibTmuxException.class, () -> attachFake(config)); } @Test void anAttachDeadlineKeepsItsTimeoutType(@TempDir Path directory) throws Exception { ServerConfig config = fakeTmux(directory, "sleep 5\n"); - DispatchException.TimedOut timeout = assertThrows( - DispatchException.TimedOut.class, - () -> ControlClient.attachUnfenced(config, new SessionId("$0"), Duration.ofMillis(100))); + DispatchException.TimedOut timeout = + assertThrows(DispatchException.TimedOut.class, () -> attachFake(config, Duration.ofMillis(100))); assertEquals(DispatchOutcome.UNKNOWN, timeout.outcome()); } @@ -289,7 +318,7 @@ void repeatedTimeoutAndCloseDoNotLeaveClients(@TempDir Path directory) throws Ex """); int before = controlThreads(); for (int cycle = 0; cycle < 12; cycle++) { - ControlClient client = ControlClient.attachUnfenced(config, new SessionId("$0")); + ControlClient client = attachFake(config); DispatchException.TimedOut timeout = assertThrows( DispatchException.TimedOut.class, () -> client.send(List.of("list-windows"), Duration.ofMillis(80))); @@ -319,7 +348,7 @@ void aClientWhoseOutputEndedIsNotAliveWhileItsProcessLingers(@TempDir Path direc sleep 5 """.replace("SUBSCRIBED", subscribed.toString())); - try (ControlClient client = ControlClient.attachUnfenced(config, new SessionId("$0"), Duration.ofSeconds(2))) { + try (ControlClient client = attachFake(config, Duration.ofSeconds(2))) { EventSubscription output = client.subscribeOutput(4); // Runs on the thread that delivers the end, at the moment it is delivered. var aliveAtTheEnd = new java.util.concurrent.CompletableFuture(); @@ -344,7 +373,7 @@ void stderrCannotBlockTheOpeningControlReply(@TempDir Path directory) throws Exc while read_request; do answer; done """); - try (ControlClient client = ControlClient.attachUnfenced(config, new SessionId("$0"), Duration.ofSeconds(2))) { + try (ControlClient client = attachFake(config, Duration.ofSeconds(2))) { assertTrue(client.isAlive()); } } @@ -361,7 +390,7 @@ void closingAControlClientReclaimsDescendantsThatInheritedItsPipes(@TempDir Path """); long child = -1; try { - ControlClient client = ControlClient.attachUnfenced(config, new SessionId("$0")); + ControlClient client = attachFake(config); assertTrue(awaitFile(childFile), "the fake control client never started its descendant"); child = Long.parseLong(Files.readString(childFile).trim()); @@ -392,7 +421,7 @@ void aWriterFailureReclaimsDescendantsBeforeKillingTheControlProcess(@TempDir Pa """); long child = -1; try { - try (ControlClient client = ControlClient.attachUnfenced(config, new SessionId("$0"))) { + try (ControlClient client = attachFake(config)) { assertTrue(awaitFile(childFile), "the fake control client never started its descendant"); assertTrue(awaitFile(ready), "the fake control client never closed its request pipe"); child = Long.parseLong(Files.readString(childFile).trim()); @@ -423,7 +452,7 @@ void aCharacterSplitAcrossOutputLinesArrivesWhole(@TempDir Path directory) throw answer IFS= read -r never """); - try (ControlClient client = ControlClient.attachUnfenced(config, new SessionId("$0")); + try (ControlClient client = attachFake(config); EventSubscription output = client.subscribeOutput(8)) { client.send("display-message"); @@ -453,7 +482,7 @@ void extendedOutputIsOutput(@TempDir Path directory) throws Exception { answer IFS= read -r never """); - try (ControlClient client = ControlClient.attachUnfenced(config, new SessionId("$0")); + try (ControlClient client = attachFake(config); EventSubscription output = client.subscribeOutput(8)) { client.send("display-message"); @@ -472,7 +501,24 @@ void extendedOutputIsOutput(@TempDir Path directory) throws Exception { */ private static final String PRELUDE = """ #!/bin/sh - read_request() { IFS= read -r request && IFS= read -r marker; } + for argument in "$@"; do + case "$argument" in + libtmux-version-*'#{version}') + printf 'unknown command: %s3.7d\\n' "${argument%'#{version}'}" >&2 + exit 1;; + esac + case "$argument" in + libtmux-identity-*) + printf 'unknown command: %s3.7d,4242,10,/fake-control\\n' "${argument%%'#{version}'*}" >&2 + exit 1;; + esac + done + read_request() { + while IFS= read -r request && IFS= read -r marker; do + return 0 + done + return 1 + } answer() { printf '%%begin 1 1 1\\n' for line in "$@"; do printf '%s\\n' "$line"; done @@ -482,6 +528,34 @@ void extendedOutputIsOutput(@TempDir Path directory) throws Exception { } """; + private static ControlClient attachFake(ServerConfig config) { + return attachFake(config, Duration.ofSeconds(30)); + } + + private static ControlClient attachFake(ServerConfig config, Duration timeout) { + ControlCarrier carrier = new ControlCarrier() { + @Override + public Process start(List command) throws java.io.IOException { + return new ProcessBuilder(command).start(); + } + + @Override + public PinnedSocket pinSocket(String socketPath, Duration remaining) { + assertEquals("/fake-control", socketPath); + return new PinnedSocket() { + @Override + public String socketPath() { + return "/fake-control-pin"; + } + + @Override + public void close() {} + }; + } + }; + return ControlClient.attach(carrier, config, new SessionId("$0"), 4242, OptionalLong.of(10), "3.7d", timeout); + } + private static ServerConfig fakeTmux(Path directory, String body) throws Exception { Path fakeTmux = directory.resolve("tmux"); Files.writeString(fakeTmux, PRELUDE + body); diff --git a/libtmux/src/test/java/io/github/libtmux/transport/ProcessTransportTest.java b/libtmux/src/test/java/io/github/libtmux/transport/ProcessTransportTest.java index d0fefdaf2..66f14d50b 100644 --- a/libtmux/src/test/java/io/github/libtmux/transport/ProcessTransportTest.java +++ b/libtmux/src/test/java/io/github/libtmux/transport/ProcessTransportTest.java @@ -194,6 +194,39 @@ void anExecutableThatDoesNotExistIsNotDispatched() { // --------------------------------------------------------------------------------- deadlines + @Test + void aOneShotCarrierKeepsCallerOwnershipAndTimeoutCertainty() throws Exception { + AtomicReference latest = new AtomicReference<>(); + AtomicInteger starts = new AtomicInteger(); + ControlCarrier carrier = command -> { + starts.incrementAndGet(); + Process started = new ProcessBuilder(command).start(); + latest.set(started); + return started; + }; + Process borrowed = carrier.start(List.of("/bin/sh", "-c", "exec sleep 30")); + try { + DispatchException.TimedOut failure = assertThrows( + DispatchException.TimedOut.class, + () -> ProcessTransport.executeThroughCarrier(carrier, hangingAs(List.of("kill-server")))); + + assertEquals(2, starts.get(), "the request must use the supplied carrier"); + assertEquals(DispatchOutcome.UNKNOWN, failure.outcome()); + assertFalse(failure.safeToRetry(), "the dispatched mutation may have run"); + assertFalse(latest.get().isAlive(), "the request's child survived its deadline"); + assertTrue(borrowed.isAlive(), "one request reclaimed a caller-owned process"); + assertEquals( + List.of("reused"), + ProcessTransport.executeThroughCarrier(carrier, shell("echo reused", Duration.ofSeconds(2))) + .stdout()); + assertEquals(3, starts.get()); + assertTrue(borrowed.isAlive()); + } finally { + borrowed.destroyForcibly(); + borrowed.waitFor(5, TimeUnit.SECONDS); + } + } + @Test void aChildThatOutlivesItsDeadlineIsKilledAndReportedUnknown() { try (ProcessTransport transport = new ProcessTransport()) { diff --git a/module-tests/scala/cats/src/main/scala/io/github/libtmux/consumer/OxConsumer.scala b/module-tests/scala/cats/src/main/scala/io/github/libtmux/consumer/OxConsumer.scala index 065f8a299..16ba05676 100644 --- a/module-tests/scala/cats/src/main/scala/io/github/libtmux/consumer/OxConsumer.scala +++ b/module-tests/scala/cats/src/main/scala/io/github/libtmux/consumer/OxConsumer.scala @@ -4,7 +4,8 @@ import io.github.libtmux.ServerConfig import io.github.libtmux.scaladsl.* import io.github.libtmux.scaladsl.live.LiveView import io.github.libtmux.scaladsl.ox.Flows -import scala.util.Using +import io.github.libtmux.exception.UnsupportedFeatureException +import scala.util.{Failure, Success, Try, Using} /** Ox is direct-style: a session's live view as a `Flow`, read once. Its own * file, because the direct-style and Cats facades name their extensions alike. @@ -13,10 +14,14 @@ object OxConsumer { def readOneView(config: ServerConfig, name: String): Unit = Using.resource(Server.open(config)) { server => val session = server.session(name).getOrElse(throw new AssertionError(s"no session $name")) - val views = Using.resource(LiveView.attach(session)) { live => + // A live view is managed control, which tmux older than 3.7 gets refused before it attaches. + Try(Using.resource(LiveView.attach(session)) { live => Flows.liveView(live).take(1).runToList() + }) match { + case Success(views) => assert(views.size == 1) + case Failure(_: UnsupportedFeatureException) => () + case Failure(other) => throw other } - assert(views.size == 1) server.asJava.killServer() } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 3be99c871..e503ca774 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -21,6 +21,8 @@ include("libtmux") include("libtmux-bom") include("libtmux-jackson") include("libtmux-kotlin") +include("libtmux-clojure") +include("libtmux-clojure-core-async") include("libtmux-junit5") include("libtmux-workspace") include("libtmux-mcp")