1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
//! #2022 coverage: the delete/prune/decommission running-guard is a REAL tmux
//! liveness probe, not a persisted-state check.
//!
//! Why: `session_manager/tests.rs` and `delete_tests.rs` are both close to (or
//! at) the 1500-SLOC test cap; this #2022-specific coverage lives in its own
//! file so none of the three grow past their limit, mirroring the pattern
//! established by `decommission_worktree_tests.rs` / `backfill_tests.rs`.
//! Reuses the sibling `tests` module's `make_manager` / `seed_record` helpers
//! rather than duplicating the scaffolding.
//! What: two tests proving a STALE `Active` record (state says running, but the
//! tmux session backing it has actually died) is removable WITHOUT `--force` —
//! one via `delete_record`, one via `prune_managed` (which also backs `tm
//! session prune`/`tm session decommission`) — while a genuinely LIVE session
//! stays guarded in both paths.
//!
//! #5859 extends the file to the OTHER arm of the same probe: a probe that
//! could not reach tmux at all. Three tests prove `delete_record`,
//! `prune_managed`, and `resume` refuse rather than reading that as death, and
//! a fourth pins the display-only `session_exists` wrapper still answering
//! `false` for it.
//! Test: this file IS the test module; run with `cargo test -p trusty-mpm`.
use tempfile::TempDir;
use super::manager::{ManagedError, ManagedTmuxDriver};
use super::record::{ManagedSessionId, ManagedSessionState};
use super::tests::{make_manager, seed_record};
/// A STALE `Active` record whose tmux session has actually died is deletable
/// WITHOUT `--force` (#2022).
///
/// Why: this IS the bug the ticket fixes. Before the fix, the guard trusted
/// the persisted `state` field: a record that still says `Active` after its
/// tmux session died (crash, `tmux kill-server`, host restart) could never be
/// deleted without `--force`, even though there was nothing left to protect.
/// The guard must track REALITY — a live tmux probe — not a snapshot that can
/// drift from it.
/// What: seeds an `Active` record (which, per `seed_record`, registers a live
/// tmux session on the fake driver), then kills that session directly to
/// simulate the daemon never having observed the crash, and asserts
/// `delete_record(id, force=false)` succeeds and the record is marked `Deleted`.
/// Test: this function IS the test.
#[tokio::test]
async fn delete_record_stale_active_deletable_when_tmux_dead() {
let dir = TempDir::new().unwrap();
let (mgr, fake) = make_manager(&dir).await;
let id = ManagedSessionId::new();
seed_record(&mgr, &dir, id, ManagedSessionState::Active, false).await;
// The record still says Active, but the tmux session backing it is gone —
// e.g. it crashed or was reaped without the daemon observing it.
fake.kill_session(&format!("tmpm-seed-{id}"))
.expect("simulate tmux death");
mgr.delete_record(&id, false).await.expect(
"a stale Active record whose tmux is actually dead must be deletable without --force",
);
// Soft-delete: the record is marked `--deleted--`, kept in the store (#2012).
assert_eq!(
mgr.get(&id).await.expect("record still tracked").state,
ManagedSessionState::Deleted
);
}
/// A STALE `Active` record whose tmux session has actually died IS pruned
/// (decommissioned) by `prune_managed` WITHOUT `--force`/`include_active` —
/// i.e. `prune`/`decommission` honor the same corrected liveness probe as
/// `delete_record` (#2022).
///
/// Why: `prune_managed`'s fail-closed gate must reflect the SAME reality check
/// as `delete_record` — otherwise `tm session prune`/`tm session decommission`
/// would still refuse to reap a genuinely-dead-but-stale-Active record even
/// after `tm session delete` was fixed to allow it, leaving the three verbs
/// inconsistent.
/// What: seeds one `Active` record whose tmux session is killed immediately
/// after seeding (stale-but-dead) and one `Active` record whose tmux session is
/// left alive (genuinely running), then prunes with `PruneFilter::All` and
/// `include_active=false` (the fail-closed default). Asserts the stale record
/// WAS decommissioned (tmux dead → not running by the new probe) while the
/// genuinely live record is untouched (tmux alive → still guarded).
/// Test: this function IS the test.
#[tokio::test]
async fn prune_stale_active_removable_without_force_when_tmux_dead() {
let dir = TempDir::new().unwrap();
let (mgr, fake) = make_manager(&dir).await;
let stale = ManagedSessionId::new();
let live = ManagedSessionId::new();
seed_record(&mgr, &dir, stale, ManagedSessionState::Active, false).await;
seed_record(&mgr, &dir, live, ManagedSessionState::Active, false).await;
// The `stale` record's tmux session dies without the daemon observing it;
// the `live` record's tmux session is left alive (seed_record registered it).
fake.kill_session(&format!("tmpm-seed-{stale}"))
.expect("simulate tmux death");
let outcome = mgr
.prune_managed(
crate::session_manager::PruneFilter::All,
false,
false,
None,
None,
)
.await
.expect("prune all");
assert_eq!(
outcome.count(),
1,
"only the stale (tmux-dead) Active record is reaped"
);
assert_eq!(
mgr.get(&stale).await.unwrap().state,
ManagedSessionState::Decommissioned,
"stale Active with dead tmux must be reaped without --force (#2022)"
);
assert_eq!(
mgr.get(&live).await.unwrap().state,
ManagedSessionState::Active,
"a genuinely live (tmux-alive) session must still be guarded"
);
}
// ---------------------------------------------------------------------------
// #5859: an UNDETERMINABLE liveness probe is not a confirmed-absent session.
// ---------------------------------------------------------------------------
/// `delete_record`'s running-guard REFUSES when the tmux probe itself fails
/// (#5859).
///
/// Why: the guard used to call `session_exists`, which folded a
/// `list-sessions` error into `false`. A transient tmux failure — or the
/// `NoopTmuxDriver` installed whenever `RealTmuxDriver::discover()` fails —
/// therefore read a live, attached session as not-running, and an unforced
/// `tm session delete` dropped it. "Could not tell" must not be spelled the
/// same way as "confirmed dead".
/// What: seeds an `Active` record (whose tmux session the fake driver reports
/// as live), makes `list_sessions` fail, and asserts `delete_record(id,
/// force=false)` returns `TmuxUnavailable` with the record untouched. Pre-fix
/// this returned `Ok` and marked the record `Deleted`.
/// Test: this function IS the test.
#[tokio::test]
async fn delete_record_refuses_when_the_tmux_probe_fails() {
let dir = TempDir::new().unwrap();
let (mgr, fake) = make_manager(&dir).await;
let id = ManagedSessionId::new();
seed_record(&mgr, &dir, id, ManagedSessionState::Active, false).await;
// tmux is live and so is the session — but the probe cannot reach it.
*fake.list_sessions_should_fail.lock().unwrap() = true;
let err = mgr
.delete_record(&id, false)
.await
.expect_err("an unobservable liveness probe must refuse the delete (#5859)");
assert!(
matches!(err, ManagedError::TmuxUnavailable(_)),
"expected TmuxUnavailable, got {err:?}"
);
assert_eq!(
mgr.get(&id).await.expect("record still tracked").state,
ManagedSessionState::Active,
"no record may be touched when liveness could not be established"
);
}
/// `prune_managed`'s running-guard REFUSES the whole sweep when the tmux probe
/// fails, and `include_active` still short-circuits it (#5859).
///
/// Why: the same fail-open as `delete_record`, reached through `tm session
/// prune` / `tm session decommission`, which additionally tear the WORKSPACE
/// down. The short-circuit half matters too: `decommission_all_ephemeral`
/// passes `include_active=true` and ignores liveness by design, so it must keep
/// working on a host where tmux cannot be observed at all.
/// What: seeds an `Active` record, makes `list_sessions` fail, and asserts the
/// `include_active=false` prune returns `TmuxUnavailable` while the record stays
/// `Active`; then asserts the `include_active=true` prune still reaps it.
/// Pre-fix the first call returned `Ok` and decommissioned the live session.
/// Test: this function IS the test.
#[tokio::test]
async fn prune_refuses_when_the_tmux_probe_fails() {
let dir = TempDir::new().unwrap();
let (mgr, fake) = make_manager(&dir).await;
let id = ManagedSessionId::new();
seed_record(&mgr, &dir, id, ManagedSessionState::Active, false).await;
*fake.list_sessions_should_fail.lock().unwrap() = true;
let err = mgr
.prune_managed(
crate::session_manager::PruneFilter::All,
false,
false,
None,
None,
)
.await
.expect_err("an unobservable liveness probe must refuse the prune (#5859)");
assert!(
matches!(err, ManagedError::TmuxUnavailable(_)),
"expected TmuxUnavailable, got {err:?}"
);
assert_eq!(
mgr.get(&id).await.expect("record still tracked").state,
ManagedSessionState::Active,
"a live session must survive a prune whose liveness probe failed"
);
// `include_active` skips the prune's liveness gate, but #8935: the
// decommission's own teardown cannot prove whose the session is while the
// probe fails, so it refuses and the record stays.
let outcome = mgr
.prune_managed(
crate::session_manager::PruneFilter::All,
false,
true,
None,
None,
)
.await
.expect("include_active skips the liveness gate entirely");
assert_eq!(outcome.count(), 0);
assert_eq!(
mgr.get(&id).await.expect("record still tracked").state,
ManagedSessionState::Active,
);
}
/// `resume` REFUSES rather than killing and rebuilding the pane when the tmux
/// probe fails (#5859).
///
/// Why: `resume`'s else-branch is destructive — it `kill_session`s and creates
/// a fresh pane. Reading an unobservable probe as "no live pane" destroyed the
/// pane the operator was looking at, along with any sibling window in that tmux
/// session.
/// What: seeds a `Stopped` record, registers its tmux name as live on the fake
/// driver, makes `list_sessions` fail, and asserts `resume` returns
/// `TmuxUnavailable` with no `kill_session` and no `create_session` issued, and
/// the record still `Stopped`. Pre-fix this killed and recreated the pane.
/// Test: this function IS the test.
#[tokio::test]
async fn resume_refuses_when_the_tmux_probe_fails() {
let dir = TempDir::new().unwrap();
let (mgr, fake) = make_manager(&dir).await;
let id = ManagedSessionId::new();
seed_record(&mgr, &dir, id, ManagedSessionState::Stopped, false).await;
// The pane survived the runtime exit (#2148) — it is genuinely alive.
fake.seeded_names
.lock()
.unwrap()
.push(format!("tmpm-seed-{id}"));
*fake.list_sessions_should_fail.lock().unwrap() = true;
let err = mgr
.resume(&id)
.await
.expect_err("an unobservable liveness probe must refuse the resume (#5859)");
assert!(
matches!(err, ManagedError::TmuxUnavailable(_)),
"expected TmuxUnavailable, got {err:?}"
);
assert!(
fake.kill_calls.lock().unwrap().is_empty(),
"resume must not kill a pane it could not prove dead"
);
assert!(
fake.create_cwd_calls.lock().unwrap().is_empty(),
"resume must not rebuild a pane it could not prove dead"
);
assert_eq!(
mgr.get(&id).await.expect("record still tracked").state,
ManagedSessionState::Stopped
);
}
/// The display-only `session_exists` wrapper still reads an unobservable probe
/// as absent (#5859).
///
/// Why: the fix must not turn every status render, name-collision check, and
/// worktree inventory row into a hard error — those callers act on nothing and
/// already tolerate an unknown. Splitting the probe is only safe while this half
/// keeps its old, lenient answer.
/// What: makes `list_sessions` fail and asserts `session_exists` is `false`
/// while `session_exists_checked` is `Err` for the same name.
/// Test: this function IS the test.
#[test]
fn session_exists_reads_an_unobservable_probe_as_absent() {
let fake = super::tests::FakeTmuxDriver::new();
*fake.list_sessions_should_fail.lock().unwrap() = true;
assert!(!fake.session_exists("tmpm-anything"));
assert!(matches!(
fake.session_exists_checked("tmpm-anything"),
Err(ManagedError::TmuxUnavailable(_))
));
}