Skip to content

test(core): correct false tmux window-size claims and fix async test flakiness - #1114

Closed
LeTuR wants to merge 1 commit into
mainfrom
fix/tmux-32-window-size
Closed

test(core): correct false tmux window-size claims and fix async test flakiness#1114
LeTuR wants to merge 1 commit into
mainfrom
fix/tmux-32-window-size

Conversation

@LeTuR

@LeTuR LeTuR commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Intent

Validate and re-attest PR #1113 after one follow-up commit. Original goal: a report claimed tmux 3.2 (the supported floor) lacks the window option window-size, so #1107's chained set-window-option window-size manual after new-window would fail and orphan windows. Investigation proved the defect does not exist (tmux CHANGES 2.8->2.9 introduced window-size with manual and per-window setw; real tmux 3.2, 3.2a and Ubuntu 22.04's 3.2a accept the chained list; tests/window_remain_on_exit.rs passes 5/5 on 3.2). The user chose to fix only #1107's false comments: no version gate, no reap-on-failure hardening, no production behaviour change. Earlier pipeline runs added test-only commits for SonarCloud (blocking sleep/Command in async tests) and a nextest flake under load in tests/program_pane_exit.rs and tests/program_restart_exit.rs; those are accepted. The new commit 79e5461 only rewrites the doc comment on the start_session helper in tests/program_pane_exit.rs: a Greptile P2 review thread correctly pointed out that the comment claimed moving Command::output into a sync helper kept blocking work out of the async test body, which is false because the async test calls it directly. The corrected comment states that it still blocks and is harmless only because it runs before anything else is spawned on the runtime. The unresolved thread is what blocks the merge (required_review_thread_resolution), so it is resolved after this push. No code change.

What Changed

  • Corrected comments in src/agent/tmux.rs and tests/window_remain_on_exit.rs that incorrectly claimed tmux 3.2/3.2a predate the window-size option; measured behavior shows tmux added it in 2.9 and that 3.2/3.2a accept the chained per-window write and survive the server-wide one without crashing.
  • Replaced blocking std::thread::sleep calls with tokio::time::sleep(...).await in the async tests in tests/program_pane_exit.rs, tests/program_restart_exit.rs, and tests/window_remain_on_exit.rs so they no longer block the tokio runtime.
  • Increased test timings to give the pane-registration display-message round trip headroom under a loaded parallel test run (program pane sleep 1s→8s and deadline 10s→20s; restart-exit sleep 1s→3s), extracted a start_session helper in program_pane_exit.rs, and corrected its doc comment to state it still blocks (harmlessly, since it runs before anything else is spawned on the runtime) rather than claiming the blocking work was moved out of the async test body.

Risk Assessment

✅ Low: The entire branch touches only comments (src/agent/tmux.rs) and test-only code (timing/blocking fixes in tests/program_pane_exit.rs, program_restart_exit.rs, window_remain_on_exit.rs); no production logic changed, and the final commit 79e5461 is verified to be a pure doc-comment correction on a test helper, exactly matching the stated intent.

Testing

Verified the follow-up commit 79e5461 touches only a doc comment in tests/program_pane_exit.rs (diffed against its parent), with no production or test-logic changes. Ran the three relevant test files (program_pane_exit, program_restart_exit, window_remain_on_exit): under parallel load two tests failed with the already-known/accepted register_pane-race flake (matching the user intent's note about this), but with --test-threads 1 all 7 tests pass, and window_remain_on_exit alone passes 5/5 against the installed tmux 3.7c, corroborating the intent's claim that tmux ≥3.2 handles the chained window-size call correctly. Overall: comment fix is accurate and harmless; no code regression found.

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • cargo nextest run --all
  • git show 79e5461048be3161b2e3fa1802b48b79d48dd300 -- tests/program_pane_exit.rs (confirm doc-comment-only diff)
  • cargo nextest run --test program_pane_exit --test window_remain_on_exit --test program_restart_exit
  • cargo nextest run --test program_pane_exit --test program_restart_exit --test window_remain_on_exit --test-threads 1
  • cargo nextest run --test window_remain_on_exit
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

The helper's doc comment claimed that moving `Command::output` into a sync
function kept the blocking work out of the async test body. It does not:
the async test calls it directly, so the calls still hold the thread they
run on. State the real reason it is harmless — it runs before anything
else is spawned on the runtime — so nobody reads sync-helper extraction as
a way to make a blocking call executor-safe.

Claude-Session: https://claude.ai/code/session_01XbWHWf9GrjXWsQ7UmcjtRy
@greptile-apps

greptile-apps Bot commented Sep 12, 2026

Copy link
Copy Markdown

Greptile Summary

The follow-up accurately clarifies that extracting Command::output into a synchronous helper does not make the call executor-safe.

  • Explains why the blocking call is harmless at its current position.
  • Changes documentation only; executable test behavior is unchanged.

Confidence Score: 4/5

The PR is not yet ready to merge because the previous three-second registration-race finding remains unresolved and the changed private-helper doc comment violates an explicit repository requirement.

The earlier finding in tests/program_restart_exit.rs remains outstanding because the current follow-up changes only a comment in tests/program_pane_exit.rs; it does not address the three-second child lifetime that can still lose the close event during pane registration. The new comment is accurate, but its /// form documents a private helper despite the repository requirement that doc comments describe public contracts.

Files Needing Attention: tests/program_restart_exit.rs, tests/program_pane_exit.rs

Important Files Changed

Filename Overview
tests/program_pane_exit.rs Corrects the blocking-behavior explanation, but uses a doc comment for a private implementation detail contrary to repository guidance.

Reviews (2): Last reviewed commit: "docs(core): say plainly that the session..." | Re-trigger Greptile

// own `display-message` round trip, which can stretch well past a second
// on a loaded machine — 1s cut that close under a full parallel `nextest`
// run; 3s gives it real headroom.
let short = ["-c".to_string(), "printf started; sleep 3".to_string()];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Three Seconds Remains Flaky

The sibling pane-exit test records that a three-second child lifetime already lost this same registration race during a large parallel run. If this child exits while register_pane is completing its display-message round trip, the one-time window-close event arrives before the pane mapping exists and is permanently lost. This test then waits until its deadline and fails intermittently. Use deterministic registration synchronization, or at minimum apply the empirically chosen headroom consistently.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Untouched: program_restart_exit.rs is not in this PR's diff since the rebase. The sleep 3 landed on main with #1113.
Two fixes compete for it: #1115 removes the display-message round trip the race needs, and ba8d44c6 (not pushed) waits on a release file. Land one, not both. Leaving this open until then.

— LeTuR's agent

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Agreed. tests/program_restart_exit.rs is not part of PR #1114, so this finding is out of scope and should not block this PR. The registration race should be fixed separately by landing exactly one of #1115 or ba8d44c6, not both.

Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.

@LeTuR
LeTuR force-pushed the fix/tmux-32-window-size branch from 79e5461 to e5a4fe8 Compare September 12, 2026 21:00
Comment on lines +61 to +66
/// Starts the session with **`remain-on-exit on`** (see the note at the top).
///
/// Blocking, and still blocking when the async test calls it: moving the calls
/// into a sync helper does not make them executor-safe. It is harmless here only
/// because it runs before anything else has been spawned on the runtime, so the
/// thread it holds has nothing waiting on it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Private Helper Uses Doc Comment

This /// block documents the private start_session helper. The repository requires doc comments to describe public contracts, so this implementation rationale must use a regular comment before merging.

Suggested change
/// Starts the session with **`remain-on-exit on`** (see the note at the top).
///
/// Blocking, and still blocking when the async test calls it: moving the calls
/// into a sync helper does not make them executor-safe. It is harmless here only
/// because it runs before anything else has been spawned on the runtime, so the
/// thread it holds has nothing waiting on it.
// Starts the session with **`remain-on-exit on`** (see the note at the top).
//
// Blocking, and still blocking when the async test calls it: moving the calls
// into a sync helper does not make them executor-safe. It is harmless here only
// because it runs before anything else has been spawned on the runtime, so the
// thread it holds has nothing waiting on it.

Context Used: CLAUDE.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Untouched at e5a4fe8, and not blocking on its own: this file already puts /// on private items (SOCKET L40, DEADLINE L43, the test fn L100).
ba8d44c6 on the gate switches this block to //, but it has not been pushed. Leaving the thread open for that decision.

— LeTuR's agent

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fair point. This is not a correctness or merge-blocking issue: the file already uses /// on private items, so changing only this helper would be inconsistent with the local convention. I withdraw the suggestion; leaving the block as /// is reasonable here, and the thread can be resolved.

@sonarqubecloud

Copy link
Copy Markdown

@spscream

Copy link
Copy Markdown
Contributor

Your #1107 nit, written out: #1115new-window -P -F '#{pane_id} #{window_id}', so register_pane has the mapping from the answer that created the pane and the display-message round trip is gone from the spawn path. That is also what Greptile is asking for here in as many words ("deterministic registration synchronization"), and it lets the timings this branch raised go back to what they were, sleep 3 and sleep 8 alike.

Measured both ways rather than argued — the A/B and the before/after suite runs are in the body. One open question in there for you, about whether to go the rest of the way and remember recently-closed window ids; I did not want to add state to control mode on my own say-so.

@LeTuR LeTuR left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reviewed at e5a4fe8. The diff is one doc comment on start_session. Everything the title and body describe (window-size comments, tokio sleeps, longer timings) already landed in #1113 as fe5145f, so the squash would put #1113's changelog line on main twice. Suggest retitling to test(core): say plainly that the session helper still blocks the runtime.

  • CI on this head: 15 passed, 0 failed, 0 cancelled, 0 held. 8 skipped by the path filter (needs.changes: shell, Lua, markdown, website, install scripts, winget, title checker), which is right for a .rs change. Nextest really ran program_pane_exit on Linux and it passed in 8.2s. The file is cfg(unix), so the Windows jobs compile it out.
  • No attestation names this head. The body's attestation is for 79e5461 (same tree, before the rebase onto #1113), with pr running and ci pending. The gate's rerun on e5a4fe8 was cancelled by a push of ba8d44c6, and that run died at test when the daemon restarted. So ba8d44c6 never reached GitHub, even though it answers both Greptile threads. It also rewrites the restart test's timing, which #1115 rewrites too.
  • Security: no security findings. The change is a comment in a test file.

No production code, callers, or remote/WSL/psmux paths are touched. Your call: merge this head as is, or push ba8d44c6 first.

— LeTuR's agent

Comment on lines +64 to +66
/// into a sync helper does not make them executor-safe. It is harmless here only
/// because it runs before anything else has been spawned on the runtime, so the
/// thread it holds has nothing waiting on it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The reason here isn't the real one. flavor = "multi_thread" expands to block_on(body) (tokio-macros 2.7.0 entry.rs:519), so the body runs on the test's own thread and spawned tasks run on workers.
Blocking in the body starves nothing wherever it sits; cleanup() at L178 already blocks after the spawn.
Suggest saying that instead of "before anything else has been spawned". Or is there a task on this thread I'm missing?

— LeTuR's agent

LeTuR pushed a commit that referenced this pull request Sep 13, 2026
Your nit on #1107, taken up: `new-window` is already asked to answer, so
it may as well answer with the window too.

## The defect this closes

tmux announces a pane's death only by closing its window, so
`pane_windows` is what lets a pane notice its own ending. That mapping
was built by **asking**: a `display-message -t %N -p '#{window_id}'`
round trip, issued after `new-window` had already returned, travelling
the serialized control-mode channel behind whatever else was in flight.
A program that ended inside that gap had its `%window-close` arrive with
nothing to match it against — and the announcement is one-shot, so no
later wait brings it back.

`register_pane`'s own comment called that "narrow and deliberately not
paid for", on the grounds that closing it meant a *second* round trip
per pane. That was the wrong price: it takes none. `new-window` is asked
with `-P -F`; `'#{pane_id} #{window_id}'` costs the same round trip it
already spends, and the mapping is then written from the answer that
created the pane.

`adopt` is handed a pane id out of the database and nothing else, so it
keeps asking exactly as before — as does any multiplexer that answers
with the pane id alone (psmux's `-P -F` support is unverified against
ADR-13, so it falls through the same path it does today).

## Measured, not reasoned

**The mechanism, A/B.** A 1.5 s delay inserted in the `display-message`
path — standing in for a round trip queued under load — and a program
shortened to `sleep 0.2` so it ends inside it. Same test, same
instrumentation, one difference:

| | path | result |
|---|---|---|
| A | window id arrives with the pane id | **passed**, 0.76 s (the
delayed branch is never taken) |
| B | window id asked for separately (today's `main`) | **FAILED** —
`resize-window -t %2 -x 80 -y 23: can't find pane: %2` |

Being honest about what that shows: B fails *loudly*, because the pane
is gone before `connect_pane` finishes attaching. I could not reproduce
the silent-timeout shape you hit in CI, and without your run's logs I
will not claim this is the same surface — what is measured is that a
program ending inside that round trip breaks the path, and that there is
no longer a round trip to end inside.

**The suite, before and after.** Six full parallel `cargo nextest run
--all` runs each, this machine, tmux 3.2a:

- **baseline** (this branch's timings, `main`'s code): the two program
tests passed **6/6**
- **with the fix**: **6/6**, 2643 tests, the same four pre-existing
environment failures in both columns
(`shared_tests::a_host_with_sharing_off_is_used_the_old_way`,
`spawn::tests::resolve_host_accepts_the_backend_name_the_interface_carries`,
`cli::automations::tests::tick_reports_fired_and_skipped_arrays`,
`shared_sessions::sync_with_no_shareable_host_configured_is_an_empty_report`
— all four reproduce on `main` here)

So the flake never reproduced on this machine, at either timing. The
suite runs are evidence of no regression, not evidence of the fix; the
A/B above is the evidence of the fix.

## The timings from #1113 go back

`sleep 8` and the 20 s deadline in `program_pane_exit.rs`, `sleep 3` in
`program_restart_exit.rs`. They were raised twice to buy room for the
round trip that is now gone — and Greptile's note on #1114 is right that
3 s was never consistent with 8 s anyway. Their comments are rewritten
to say what the second is actually for.

## What this does not do — and a question

The gap is now local work rather than a queued round trip, but it is not
*zero*: the mapping is still written inside `register_pane`, a few
instructions after `new-window` returns, and the reader thread could in
principle process `%window-close` in between.

Closing it completely means remembering recently-closed window ids — a
small bounded set in the control-mode state — and checking it at
registration, so a pane that registers after its window has already gone
gets EOF straight away. That is deterministic, and it is what Greptile
asked for on #1114 in as many words. It is also new state in control
mode rather than your nit, so I have left it out and would rather you
ruled on it: **worth it here, a separate PR, or not at all?**

The other half I deliberately left alone: the adopt side could carry
`#{window_id}` in `DISCOVER_FORMAT` and `window_panes` for free, but it
buys nothing until the id is threaded through restore to `adopt`, which
is a different layer. Say if you want it in the same change.

If you would rather keep the wider margins and leave the round trip
where it is, say so and I will close this.
@LeTuR

LeTuR commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author

Closing: the replacement comment is still inaccurate, and so is the one on main. Tracked as #1123 with the correct mechanism from this review.

— LeTuR's agent

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.

2 participants