Skip to content

Commit fa325cc

Browse files
Danclaude
authored andcommitted
test(tui): pin runtime-context regression for connect-timeout panic
`tokio::time::timeout(...)` registers with the timer driver at **construction**, not at first poll. Pre-#428, `app.rs` called `runtime.block_on(tokio::time::timeout(...))` — the timeout was constructed BEFORE `block_on` entered the runtime, panicking with "no reactor running". Every `pitboss-tui` launch in v0.13.0 hit this. The fix (ddaf1f8) wraps the timeout in `async { ... .await }` so construction happens inside the runtime context. F-TEST-4 (#538) audited that no regression test pinned the working pattern. Two new integration tests in `tests/connect_timeout_regression.rs`: 1. `timeout_constructed_inside_async_block_does_not_panic` — mirrors the exact production pattern from `app.rs:90-102`. A future "simplification" that unwraps the async block would panic here. 2. `naive_block_on_pattern_panics_when_constructed_outside_runtime` — negative coverage. Catches the panic via `catch_unwind` and asserts it fired, pinning the original v0.13.0 failure shape so a tokio behavior change or the test losing its grip on the broken pattern surfaces loudly rather than silently. Closes #538 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent eb97ff3 commit fa325cc

1 file changed

Lines changed: 96 additions & 0 deletions

File tree

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
//! Regression test for #427 / #428 (F-TEST-4): the connect-timeout
2+
//! call in `app.rs::run` must construct `tokio::time::timeout(...)`
3+
//! *inside* the runtime context.
4+
//!
5+
//! `tokio::time::timeout(...)` registers with the timer driver at
6+
//! **construction**, not at first poll. If constructed outside a
7+
//! runtime (e.g. as the argument to `runtime.block_on(...)` directly),
8+
//! it panics with `"there is no reactor running, must be called from
9+
//! the context of a Tokio 1.x runtime"`. Pre-fix, every `pitboss-tui`
10+
//! launch hit this on the connect-control path, making v0.13.0
11+
//! unusable.
12+
//!
13+
//! The fix wraps the timeout in `async { ... .await }` so the timeout
14+
//! is constructed inside the future being polled by `block_on`. This
15+
//! test mirrors the exact pattern from `app.rs:90-102`. A regression
16+
//! that unwraps the async block as a "simplification" would panic in
17+
//! the body of this test.
18+
19+
use std::path::PathBuf;
20+
use std::time::Duration;
21+
22+
#[test]
23+
fn timeout_constructed_inside_async_block_does_not_panic() {
24+
let runtime = tokio::runtime::Builder::new_multi_thread()
25+
.enable_all()
26+
.build()
27+
.unwrap();
28+
29+
// Exact production pattern from `pitboss_tui::app::run`. The future
30+
// passed to `block_on` constructs `tokio::time::timeout` AFTER the
31+
// runtime context is entered. `open_run_session` returns a
32+
// `RunStreamSession` directly (not a `Result`), so the outer
33+
// `Result` is purely the timeout's elapsed-vs-completed flag.
34+
let result: Result<pitboss_cli::stream::RunStreamSession, tokio::time::error::Elapsed> =
35+
runtime.block_on(async {
36+
tokio::time::timeout(
37+
Duration::from_millis(50),
38+
pitboss_cli::stream::open_run_session(
39+
PathBuf::from("/tmp/pitboss-tui-tests-nonexistent-run-dir"),
40+
Some(PathBuf::from(
41+
"/tmp/pitboss-tui-tests-nonexistent-control.sock",
42+
)),
43+
pitboss_cli::stream::LiveStreamMode::LiveOnly,
44+
),
45+
)
46+
.await
47+
});
48+
49+
// The outcome doesn't matter for this regression test — what we're
50+
// proving is that `tokio::time::timeout` construction inside the
51+
// async block doesn't panic with "no reactor running". A missing
52+
// run dir / socket either completes fast (Ok) or elapses (Err)
53+
// within the 50ms budget; both are acceptable.
54+
let _ = result;
55+
}
56+
57+
#[test]
58+
fn naive_block_on_pattern_panics_when_constructed_outside_runtime() {
59+
// Negative coverage: prove the original v0.13.0 pattern panics. If
60+
// someone "simplifies" the wrapper away again, this test pins what
61+
// the failure mode actually was — a panic in the runtime layer.
62+
//
63+
// We catch the panic with `std::panic::catch_unwind` so this test
64+
// reports the regression rather than crashing the harness. The
65+
// closure constructs `tokio::time::timeout` *outside* an `async`
66+
// block — the same shape that broke v0.13.0.
67+
let runtime = tokio::runtime::Builder::new_multi_thread()
68+
.enable_all()
69+
.build()
70+
.unwrap();
71+
72+
// `catch_unwind` requires `UnwindSafe`; both the runtime and the
73+
// pure tokio future satisfy this for the construction-time panic
74+
// we're catching.
75+
let panicked = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
76+
runtime.block_on(tokio::time::timeout(
77+
Duration::from_millis(50),
78+
pitboss_cli::stream::open_run_session(
79+
PathBuf::from("/tmp/pitboss-tui-tests-nonexistent-run-dir"),
80+
Some(PathBuf::from(
81+
"/tmp/pitboss-tui-tests-nonexistent-control.sock",
82+
)),
83+
pitboss_cli::stream::LiveStreamMode::LiveOnly,
84+
),
85+
))
86+
}))
87+
.is_err();
88+
89+
assert!(
90+
panicked,
91+
"tokio::time::timeout constructed outside async block should panic \
92+
with 'no reactor running' — if this assertion fails, either tokio's \
93+
behavior changed or the test is no longer exercising the pre-fix \
94+
shape that v0.13.0 hit."
95+
);
96+
}

0 commit comments

Comments
 (0)