Skip to content

Require tmux 3.3a for core operations - #779

Draft
tony wants to merge 36 commits into
masterfrom
bump-min-tmux-000
Draft

tony wants to merge 36 commits into
masterfrom
bump-min-tmux-000

Conversation

@tony

@tony tony commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

Require tmux 3.3a before ordinary reads, writes, batches and public raw command dispatch. Existing endpoints are checked against the connected daemon's version, preserving patch letters; a newer client executable cannot vouch for an older daemon. Cold creation validates the executable. A native command prefix checks the daemon's release before the requested commands, and live dispatch uses -N so losing a daemon cannot autostart an older executable or load its configuration.

The version query is hook-free. It runs -N run-shell -C '<marker>#{version}', which exits 1 and reports the version in unknown command: <marker><version>; run-shell has no after-hook and, with -C, no command-error hook fires, so a user's after-display-message hook (which display-message fires on every release) never runs for the version check. A failure that does not carry the marker line stays an error, and only no server running, a missing socket or a refused connection mean an absent endpoint. The fence keeps if-shell -F; on tmux 3.5 and newer a replaced-daemon refusal fires command-error once, which is an accurate error report.

Each Server probes its endpoint once and reuses the checked release for later commands and optional-flag decisions: the if-shell -F fence in every command already catches a replaced daemon, so a refusal clears the cached release and the next command probes again. A command that finds the cached daemon gone clears it and checks again (validating the executable for an absent endpoint, as the first call does). The first call, an absent endpoint and Server.kill() always read the endpoint directly, and an absent endpoint is never cached.

The version check parses only the forms tmux has emitted: X.Y with an optional patch letter, X.Y-rc and X.Y-rcN, and next-X.Y, ordered next-X.Y < X.Y-rc < X.Y-rcN < X.Y < X.Ya. Bare 3.8-rc is accepted; 3.3a-master, 3.3a-dev, next-3.3a, leading zeros and master are refused.

Optional flags (-E, -k, -C and the rest) follow the connected daemon's release through Server._has_gte_version, so a newer client cannot send a flag an older daemon rejects. Programs built on libtmux, such as libtmux-mcp and tmuxp, reuse the decision through the small libtmux._internal.support module (MINIMUM, VersionTooLow, require_version, is_at_least, server_version, check_fence) instead of private names. There is no public libtmux.support yet.

Choose format fields from the daemon's numeric release, including numbered development reports, and remove obsolete pre-3.3 hook, popup and prompt adaptations. List accessors and query traversal propagate unsupported-version refusals while retaining ordinary command-error leniency. An absent Server.kill() stays a no-op; standalone version/help diagnostics remain available. Undated master, synthetic OpenBSD diagnostic values and malformed reports cannot pass the version check. The hook table tracks tmux 3.8 (six added hooks, after-queue removed) and skips hooks it does not know instead of failing the parse, and new_pane documents the bordered footprint tmux 3.8 reports.

The executable-version helpers in libtmux.common (get_version, get_version_str, has_version, has_gt_version, has_gte_version, has_lte_version, has_lt_version, has_minimum_version, and the TMUX_MIN_VERSION and TMUX_MAX_VERSION constants) now live in libtmux._internal.version. They read the client executable, not the daemon, so each emits a DeprecationWarning that names the replacement, e.g. is_at_least(server_version(server), "3.4"). They stay importable from libtmux.common. libtmux's own doctests, docs and tests no longer call them (a pytest filterwarnings error enforces this outside tests/legacy_api, which keeps testing them and asserts the warnings).

CI, installation and compatibility documentation use the same minimum. The unreleased changelog is isolated in its own commit. Upgrade the executable and restart existing older daemons after saving their sessions.

Validation:

  • All 1,499 tests and executed documentation examples passed without reruns at the runtime qualification revision on the local Python 3.14t/tmux 3.7d host. CI then exposed a fixed-delay completion example on 3.3a, 3.6 and 3.7b. The corrected example polls an exact marker; all 16 examples in its document pass without reruns on real 3.3a and 3.7d, and changing only the printed marker proves a timeout. The Python 3.15 CI matrix covers tmux 3.3a, 3.4, 3.5, 3.6, 3.7a, 3.7b, 3.8 and master, together with an unsupported-versions job that builds tmux 3.2a, 3.3 and 3.3a and refuses real 3.2a and unpatched 3.3 daemons with no effects.
  • Twenty-five native checks cover 3.2a and unpatched 3.3 refusals with preserved state; real 3.3a creation, reads, split and sizing; distinct older/newer clients within protocol compatibility; quoted batches; daemon replacement; and old-client restart prevention.
  • Deliberate regressions demonstrate the version check, native prefix, release ordering, no-start option values, numbered development format selection, list refusal and natural window naming checks failing for their intended reasons.
  • The configured-executable banner follow-up passes all 52 version-check cases on native 3.3a, including actual executable names with spaces and non-UTF-8 bytes. Restoring the preceding parser makes four intended cases fail. Exact basename and canonical framing preserve strict whole-release validation. Ruff, mypy, strict Sphinx, independent source review and the current Python 3.15 CI matrix pass.
  • Ruff format/lint, mypy and the strict Sphinx documentation build pass. Independent source review found the option-value, development-format and traversal issues now covered by these checks.
  • The hook-free probe follow-up adds a native test that sets after-display-message and, where set-hook accepts it (3.5+), command-error hooks, runs the probe, a prepared command and a dispatched command, then fires a control display-message and a failing command; hooks run in order, so each sentinel holding exactly one letter shows the version check fired neither. It passes on 3.3a, 3.4, 3.5, 3.6b, 3.7d, 3.8 and master (next-3.9). Restoring display-message as the probe fails it on 3.3a, 3.5, 3.8 and master. The 236 version-check, server, common and version tests pass on 3.7d; Ruff and mypy pass.
  • Review follow-ups. The version grammar is one table (supported, below floor, unparseable, ordering); widening the pattern to accept -master/-dev and require rc digits fails seven cases. Moving the flag decisions to the daemon is pinned by a test where the client helper says "newest" and the daemon reports 3.3a; asking the client again fails it. The per-endpoint reuse is covered by tests for one probe across commands, a stale cached release refused with no effect, a daemon killed from outside, and kill() probing afresh; not clearing on a fence refusal fails two tests, skipping the gone-daemon retry fails one, and never storing the release fails four. Seven native cases drive real 3.2a, 3.3 and 3.3a daemons; accepting everything fails the four refusal cases.
  • Spawned tmux processes per call on 3.7d, before and after reuse: Server.cmd("list-sessions") 2 and 1, Server.sessions 3 and 1, Pane.capture_pane with two optional flags 4 and 1. Wall time varied about 30 percent under shared load; best of two runs for 100 list-sessions was 3.02s and 1.94s and for 100 Server.sessions 4.11s and 1.72s.
  • Deprecated helpers. 1,554 tests and executed documentation examples pass on tmux 3.7d and tests/test_versions.py passes on real 3.3a, with Ruff, Ruff format and mypy clean. Removing the warning from has_lt_version fails its test, and putting a deprecated call back in a doctest or in a test outside tests/legacy_api fails the suite.

Merge and release order: this PR and #780 land first and libtmux 0.63.0 is released from them. libtmux-mcp (#139, #140) and tmuxp (#1092, #1093) require libtmux>=0.63.0 and ~=0.63.0, so they merge only after that release; each then drops its development [tool.uv.sources] pin. Nothing here tags or releases.

Other Python versions and platforms remain outside this qualification. Optional engine and feature restrictions are covered by the paired feature change.

The version check assumes that library-owned metadata and guard commands retain native tmux built-in semantics. Deliberate server command-alias redefinitions of those commands are outside this guarantee.

Closes #777.

tony added 9 commits October 16, 2026 20:22
why: A newer client can connect to an older daemon, while normalized
version comparisons lose the patch letter that distinguishes 3.3a.

what:
- Share a bounded daemon-first version check across ordinary raw
  dispatch
- Fence the original native command batch on the checked release
- Preserve standalone binary diagnostics without authorizing effects
- Route liveness through the version check and make absent cleanup a
  no-op
- Cover patch ordering, raw batches and replacement before mutation
why: A supported daemon can differ from its client executable. Numeric
format introduction checks must also keep tmux patch letters out of
Python package prerelease ordering.

what:
- Select format fields from fresh endpoint reports
- Preserve native introduction metadata for offline format utilities
- Remove obsolete popup, hook and prompt fallbacks below 3.3a
- Restore the session dimensions test after retiring its 3.2 defect
why: Installation guidance and tests must name the same baseline that
ordinary commands enforce on their connected daemon.

what:
- Retire 3.2a from the ordinary CI matrix and state the 3.3a floor
- Explain daemon authority, diagnostics and absent cleanup
- Document development reports and unversioned OpenBSD refusal
- Describe fresh format selection separately from cached diagnostics
why: Bare `master` names no release. The legacy helpers map it to a
synthetic maximum, which would let an undated development daemon pass
the 3.3a check.

what:
- Require an identifiable release before dispatching to a daemon
- Keep the legacy master normalization for diagnostics only
- Test that a daemon reporting master is refused

The master case fails on the previous check path; the focused version
check suites pass (105 tests), as do ruff and mypy.
why: After a live daemon passes the version check, a command that finds
it gone would let the configured executable start a new daemon, which
may be a different release, before any check runs.

what:
- Pass -N when dispatch follows a successful live-daemon check
- Keep cold startup validating the executable and creating natively
- Test a daemon that disappears between the check and the command

Without the fix the disappearance test restarts a daemon and fails. The
focused suites pass (221 tests), as do ruff, mypy and the strict docs
build.
why: A socket name or config path given to -L, -S or -f can itself be
the literal -N, which must not count as the no-start flag.

what:
- Read -N from the parsed global options, not from any argv word
- Test -N as the value of -L, -S and -f

The regression fails on the previous code and the three value cases
pass with the fix; ruff format and checks pass.
why: A numbered development report such as 3.3a-master fell through to
the bare-master offline convenience, which selects the newest format
catalog, so the daemon was asked for fields later releases introduced.

what:
- Map a numbered development report to its numeric format catalog
  before the bare-master convenience applies
- Test that a 3.3a-master daemon requests no later fields

The 3.3a-master regression fails on the previous code. The version
check, format and break-pane checks pass (66 tests), as do strict
Sphinx, ruff and mypy.
why: A POSIX shell can stay the pane's foreground process while it waits
for a child, so neither the split reply nor a fixed delay shows that
sleep is running, and the automatic-name assertion raced.

what:
- Run exec sleep and wait for its foreground name before asserting the
  window's automatic name

Forcing the exact-3.7 placeholder on a newer daemon fails the intended
name assertion; with the release guard restored the focused tests pass
without reruns.
why: List accessors, winlink discovery and query lookup turned command
errors into empty results, so an unsupported daemon looked like a
server with no sessions instead of raising VersionTooLow.

what:
- Re-raise VersionTooLow from list-shaped accessors, winlink discovery
  and query lookup
- Keep other command errors under the existing lenient list contract
- Test each public traversal route against an unsupported daemon

Every route fails on the previous code and raises the refusal with the
fix. The full suite and the executed documentation examples pass (1499
tests, no reruns), as do strict Sphinx, ruff and mypy.
@codecov

codecov Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 68.25000% with 127 lines in your changes missing coverage. Please review.
✅ Project coverage is 54.24%. Comparing base (e5cb17c) to head (c27dd92).

Files with missing lines Patch % Lines
src/libtmux/_internal/support/versions.py 73.23% 32 Missing and 6 partials ⚠️
src/libtmux/_internal/version.py 55.42% 35 Missing and 2 partials ⚠️
src/libtmux/server.py 62.00% 6 Missing and 13 partials ⚠️
src/libtmux/pane.py 64.00% 3 Missing and 6 partials ⚠️
src/libtmux/_internal/constants.py 25.00% 6 Missing ⚠️
src/libtmux/common.py 76.19% 5 Missing ⚠️
tests/conftest.py 85.29% 3 Missing and 2 partials ⚠️
src/libtmux/_internal/support/__init__.py 72.72% 3 Missing ⚠️
src/libtmux/_internal/query_list.py 50.00% 2 Missing ⚠️
src/libtmux/hooks.py 80.00% 1 Missing ⚠️
... and 2 more
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #779      +/-   ##
==========================================
+ Coverage   52.37%   54.24%   +1.87%     
==========================================
  Files          26       30       +4     
  Lines        3729     3969     +240     
  Branches      747      767      +20     
==========================================
+ Hits         1953     2153     +200     
- Misses       1472     1519      +47     
+ Partials      304      297       -7     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

why: The completion-marker example typed a nonexistent command and
slept 200 ms before reading the pane. It failed in the 3.3a, 3.6 and
3.7b CI lanes and could also match the echo of the typed input.

what:
- Start a real command with the window and keep the pane alive
- Wait with retry_until for the marker as a complete output line

The doctest passes without retries on 3.3a and 3.7d; changing only the
printed marker makes the unchanged predicate fail with WaitTimeout.
Strict docs pass.
why: tmux 3.3a prints its executable's basename in the -V banner, so a
3.3a build installed under another name was refused at the floor.

what:
- Match the configured executable's basename before the tmux prefix,
  including names with spaces and non-UTF-8 bytes
- Keep full release validation and daemon framing unchanged

The version check cases pass on 3.3a (52 tests); the previous parser
fails four banner cases, three of them real renamed executables. Ruff,
mypy and strict Sphinx pass.
tony added 10 commits October 17, 2026 03:37
why: The version probe ran display-message -p '#{version}', which fires
a user's after-display-message hook on every release before the check
can refuse anything.

what:
- Probe with -N run-shell -C '<marker>#{version}'. run-shell has no
  after-hook, and with -C the expanded string is parsed in a timer
  callback after the command returned, so no command-error fires. tmux
  exits 1 with `unknown command: <marker><version>` on stderr and the
  version is read from that line; any other failure stays an error
- Answer the mocked probes in test_versions.py in run-shell format
- Add a native test that arms after-display-message and, from 3.5 where
  set-hook accepts it, command-error, then runs the probe, a prepared
  command, a dispatched command, a control display-message and a
  failing command; each sentinel holding exactly one letter shows no
  probe fired a hook

The native test passes on 3.3a, 3.4, 3.5, 3.6b, 3.7d, 3.8 and master
(next-3.9). Restoring display-message as the probe fails it on 3.3a,
3.5, 3.8 and master. test_versions, test_server, test_common and
test_version pass on 3.7d (236 tests); ruff and mypy are clean.
why: The compatibility page still called the version query read-only,
which says nothing about the user's hooks.

what:
- Say the check queries with run-shell -C, which fires no after-* or
  command-error hook
- Name the one case where command-error fires: a replaced daemon
  failing the fence on tmux 3.5 or newer
why: The check accepted 3.3a-master and 3.3a-dev at the floor while it
refused next-3.3a, rejected the bare 3.8-rc that tmux itself reports,
and let 03.3a through. tmux never reports those suffixes; library
wrappers made them up.

what:
- Parse only what tmux has reported: X.Y with an optional patch letter,
  X.Y-rc and X.Y-rcN, and next-X.Y, with ASCII digits and no leading
  zeros
- Order them next-X.Y < X.Y-rc < X.Y-rcN < X.Y < X.Ya, so a candidate
  or development build passes only above the floor
- Replace the cases with one table of supported, below-floor and
  unparseable vectors, plus the ordering chains and the 3.7 feature
  floor

Widening the pattern to accept -master/-dev and to require rc digits
fails seven cases, including 3.8-rc and 3.3a-master. Ruff and mypy are
clean.
why: Pane, Window, Session and Server chose optional flags such as -E,
-k and -C from the client executable's -V, so a current client talking
to a 3.3a daemon sent flags the daemon rejects with a native error
instead of taking the warn-and-ignore path.

what:
- Add Server._has_gte_version, which compares the connected daemon's
  release, and use it at every optional-flag gate
- Read the daemon's release for exact-3.7 break-pane naming too
- Patch that decision in the warn-on-old-tmux tests
- Test a client that reports the newest release against a 3.3a daemon

Making _has_gte_version consult the client again fails the new test.
The server, pane, capture, window and session modules pass on 3.7d.
why: Every command spawned a version probe plus the command, and each
optional-flag decision spawned another, so a lookup took two to four
tmux processes. The in-band if-shell -F fence already detects a
replaced daemon, so the release only needs probing again after the
fence fires or the daemon disappears.

what:
- Give each Server a VersionCache holding the release last checked for
  its endpoint (binary, selectors, TMUX_TMPDIR and TMUX)
- Read it in prepare() and Server._get_version(), and share it with
  list accessors through fetch_objs
- Clear it when the fence refuses. When no daemon answers, clear it and
  check once more, which validates the executable for an absent
  endpoint exactly as the first call does and cannot have had an effect
- Read the endpoint directly on the first call, for an absent endpoint
  and in Server.kill(); never cache an absent endpoint
- Test one probe across three commands, a list accessor and a flag
  decision; a stale cached release refused with no effect, forgotten
  and re-read; a daemon killed from outside detected and a new session
  started after a fresh check; kill() probing afresh
- Clear the cache in the existing disappearance and changed-report
  tests so they still exercise the probe

tmux spawns per call, before and after, on 3.7d with config /dev/null:
Server.cmd("list-sessions") 2 and 1, Server.sessions 3 and 1, and
Pane.capture_pane with two optional flags 4 and 1. Wall time, best of
two runs on a shared WSL2 host (it varies by 30 percent between runs;
spawn counts do not): 100 list-sessions 3.02s and 1.94s, 100
Server.sessions 4.11s and 1.72s, 50 capture_pane 0.83s and 0.88s.

Breaking each guard fails its tests: not clearing on a fence refusal,
skipping the gone-daemon retry, and never storing the release. The
version check module passes on 3.7d (83 tests).
why: The compatibility page said the version check never caches, which
stopped being true once each Server reused its checked release.

what:
- Say a Server probes its endpoint once and reuses the release
- Say the fence refuses a replaced daemon with no effect
- Say kill() and an absent endpoint read the endpoint directly
why: libtmux-mcp and tmuxp reuse the daemon-first version check for
their own tmux subprocesses, and had to read private Server methods to
do it.

what:
- Add libtmux._internal.support with MINIMUM, VersionTooLow,
  require_version, is_at_least, server_version(server) and check_fence,
  wrapping the existing helpers with no behaviour change
- Test the exported names, an older daemon refused through
  server_version, a checked release returned, and a fence marker turned
  into VersionTooLow while an unrelated line is ignored
- Run the module doctest
why: libtmux-mcp and tmuxp import libtmux._internal.support, so its
names need a reference page beside the other internals.

what:
- Add an internals API page for libtmux._internal.support
- Add its card to the internals index
why: Refusal below the floor was covered only with a monkeypatched
probe.

what:
- Start a real 3.2a or 3.3 daemon and drive it with a 3.3a client: a
  write, a new session, a raw tmux_cmd kill-server and a list accessor
  all raise VersionTooLow, and a sentinel option and the session list
  stay unchanged
- Check that a cold 3.2a or 3.3 executable cannot create its socket,
  and that a real 3.3a daemon passes as the control
- Skip unless the three LIBTMUX_TEST_*_TMUX variables name native
  builds, the convention libtmux-mcp and tmuxp use

The seven native cases pass against 3.2a, 3.3 and 3.3a builds. Making
require_version accept everything fails the four refusal cases.
why: The native refusal tests skip unless real 3.2a, 3.3 and 3.3a
builds are available, so CI needs a job that provides them.

what:
- Add an unsupported-versions job that builds tmux 3.2a, 3.3 and 3.3a
  from source, exports the three native paths and runs the version
  check module
- Leave the supported matrix starting at 3.3a
tony added 14 commits October 17, 2026 10:50
why: tmux 3.8 is a released tag, so the matrix should cover it as well
as master.

what:
- Add 3.8 to the tmux matrix as a gating lane; only master stays
  continue-on-error

Focused runs of the version check, control and optional-feature modules
pass locally against a 3.8 build.
why: tmux 3.8 adds after-swap-window, client-closed, client-created,
marked-pane-changed, session-added-to-group and
session-removed-from-group, and drops after-queue. Hooks.from_stdout
raised TypeError on the first hook its dataclass lacked, so show-hooks
on 3.8 broke parsing.

what:
- Add the six 3.8 hooks as Hooks fields
- Skip hooks the table does not know instead of failing the parse
- Cover the 3.8 hooks in the set/show/unset grid, gated on 3.8
- Gate the after-queue case to tmux up to 3.7
why: From 3.8, new-pane -x/-y/-X/-Y give the on-screen footprint
including the default border (regress/floating-pane-geometry.sh), so a
pane asked for 80 columns reads pane_width 78. The test and the
floating_panes doctest assumed content-box sizing and failed on 3.8.

what:
- Document the border offset on Pane.new_pane and Window.new_pane
- Expect width/height minus 2 and x/y plus 1 on 3.8+, in the test and
  doctest
why: tmux 3.8 display-popup returns early for control-mode clients
(cmd-display-menu.c: CLIENT_CONTROL check), so the command never ran and
the popup tests timed out waiting on their marker files.

what:
- Add a pty_client fixture that attaches a terminal-backed client
- Use it in the display_popup flag, close_on_success and target_client
  tests
- Note the 3.8 control-client behaviour on Pane.display_popup
why: The per-Server version cache holds a threading.Lock, so
copy.deepcopy(Server(...)) and pickle.dumps(Server(...)) raised
"cannot pickle '_thread.lock' object". Both work on master.

what:
- Give VersionCache __getstate__/__setstate__ that drop the cached
  release and create a fresh lock, so a copy checks its daemon again
  rather than trusting a release checked by another object
- Test deepcopy and pickle round trips keep the endpoint and start
  without a checked release

Removing __getstate__ makes the round-trip test fail with the original
TypeError.
why: server_version(server) returned the release the Server had
cached, so after a 3.3a daemon was replaced by 3.2a on the same socket
it still answered "3.3a". check_fence had no way to tell a Server that
its cached version was stale, so a consumer dispatching its own fenced
subprocess could not invalidate it either.

what:
- Clear the Server's cached version before server_version probes
- Accept an optional server in check_fence and clear its cached version
  when the fence fires, before re-raising VersionTooLow
- Test a real 3.3a daemon replaced by 3.2a: server_version refuses
  it and leaves nothing cached
- Test that a fired fence clears the cache and a foreign marker does not
- Import libtmux._internal.support at module level in the tests

Each guard has a deliberate break that fails its test: server_version
reading the cache, and check_fence keeping it.
why: get_version and the has_*_version helpers read the tmux client
executable, which can be a different release from the daemon a Server
talks to. libtmux checks the daemon through libtmux._internal.support,
so the executable helpers are legacy and belong out of common.

what:
- Add libtmux/_internal/version.py holding get_version, get_version_str,
  has_version, has_gt/gte/lte/lt_version, has_minimum_version, their
  private helpers, TMUX_MIN_VERSION and TMUX_MAX_VERSION
- Each public helper emits a DeprecationWarning (stacklevel 2) saying it
  reads the client executable and naming the libtmux._internal.support
  replacement, e.g. is_at_least(server_version(server), "3.4")
- Keep cache_clear() on get_version and get_version_str, typed so mypy
  accepts it
- Re-export every helper from libtmux.common, and serve the two
  constants through a module __getattr__ that warns, so old imports keep
  working
- Resolve tmux_cmd through libtmux.common at call time so tests that
  replace it keep working
- Point the tests that patch TMUX_MIN_VERSION and _query_version at the
  module that now owns them
why: The tests that exercise get_version and the has_*_version helpers
test the legacy executable API, so they belong beside the other legacy
tests and not among the suite that libtmux's own behaviour is built on.

what:
- Move the helper tests from tests/test_common.py to
  tests/legacy_api/test_version_helpers.py unchanged: memoization, cache
  keying, comparisons, version parsing and validation
- Silence DeprecationWarning for that module only, since it deliberately
  calls the deprecated helpers
- Leave tmux_cmd, session_check_name and the other common tests in place
why: The helpers now read like deprecated API but nothing proved they
warn, point at the caller, or name the replacement, so dropping a
warning would go unnoticed.

what:
- Parametrize over all eight public helpers: each emits a
  DeprecationWarning that names the client executable, its own
  libtmux.common name and libtmux._internal.support, and points at the
  caller's file
- Check TMUX_MIN_VERSION and TMUX_MAX_VERSION warn through
  libtmux.common and still return their values
- Check an unknown attribute still raises AttributeError
- Check get_version.cache_clear and get_version_str.cache_clear survive
why: The doctests called has_gte_version, which reads the client
executable and now warns. They run against the daemon the server fixture
talks to, so that is the release to gate on.

what:
- Replace has_gte_version("X") with is_at_least(server_version(server),
  "X") in the Pane, Server and Window doctests, importing both from
  libtmux._internal.support
why: The floating-pane examples called has_gte_version, which reads the
client executable and now warns, and the format-token page described
the legacy helpers as if they were the way to read a version.

what:
- Use is_at_least(server_version(server), "X") from
  libtmux._internal.support in the floating-pane examples
- Say on the format-token page that get_version and get_version_str are
  deprecated and point at server_version for the daemon's release
why: The suite gates tests on the release of the tmux it launches, which
is the executable. The deprecated libtmux.common helpers warn on every
call, so the suite needs its own small gate to keep libtmux's tests free
of deprecation warnings.

what:
- Add tests/tmux_client.py with client_gte, client_lte and clear_caches,
  built on the private helpers in libtmux._internal.version so the
  letter-stripping comparison stays what the tests were written against
- Flush the caches through clear_caches in the autouse conftest fixture
- Read the daemon's release in the non-UTF-8 locale test through
  Server._get_version
- Make the newer-client-executable test fake the executable's release
  where the helper now lives
why: These tests called has_gte_version and has_lte_version from
libtmux.common, which now warn that they read the client executable.

what:
- Replace the helper calls in the hooks, options, pane, capture-pane,
  server, session and window tests with client_gte and client_lte from
  tests/tmux_client.py, with the same version comparisons
- Reword the window-name test docstring that cited get_version
why: libtmux must not call the deprecated executable version helpers
itself, but nothing stopped a doctest or a test from reintroducing one;
the warning would only scroll past in the summary.

what:
- Turn a DeprecationWarning whose message starts with libtmux.common.
  into an error when it is raised from libtmux or from tests outside
  tests/legacy_api, which keeps exercising the helpers on purpose

Each break fails and the restored tree passes: a doctest calling
has_gte_version and a test outside legacy_api calling it.
@tony
tony force-pushed the bump-min-tmux-000 branch 2 times, most recently from 71f1bb5 to f6e4630 Compare October 17, 2026 17:39
why: Users upgrading need to know that tmux servers below 3.3a are now
refused, which version helpers warn and what replaces them, and what was
fixed for newer tmux releases.

what:
- Record the tmux 3.3a minimum, checked against the running server
- Record the deprecated executable version helpers with a migration
- Record flags following the server's release and the tmux 3.8 hooks
- Record the completion example that waits for real output
@tony
tony force-pushed the bump-min-tmux-000 branch from f6e4630 to c27dd92 Compare October 17, 2026 18:10

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Require tmux 3.3a for core operations

1 participant