trusty-mpm 1.3.5

trusty-mpm: unified multi-agent orchestration platform (core, daemon, CLI, TUI, Telegram)
Documentation
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
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
//! CLI parse tests for the `tm session` -> `tm sessions` top-level rename
//! (issue #2116, DOC-35 §2.2/§3.2) — extracted from `tests.rs` to keep it
//! under the 1500-SLOC test-file cap, following the existing
//! `tests_behavior_a/b/c/d` split convention.
//!
//! Why: `cli_parses_session_singular` in `tests.rs` already covers the
//! deprecated singular alias parsing unchanged; this file adds the mirror
//! assertions for the new canonical plural (`cli_parses_sessions_plural_canonical`),
//! the full-verb-surface parity check (`cli_session_and_sessions_agree_for_every_verb`),
//! and the pure deprecation-message-text assertion
//! (`top_level_alias_notice_message`) — the process-level "printed exactly
//! once" property is proven separately by the `tm_sessions_alias_notice`
//! integration test, which spawns the real binary.
//! What: parse round-trips for `Command::Sessions` and a full sweep of every
//! `SessionAction` verb asserting `tm session <verb>` and `tm sessions <verb>`
//! parse to the identical action.
//!
//! Also carries the `#1809`/`#3034`/`#3044` decommissioned/deleted-tombstone
//! picker-filter coverage (`is_live_session_state_*`, `picker_filter_*`),
//! moved here from `tests_behavior_c_tests.rs` when the #3044 reconciliation's
//! added tests pushed that file over the 1500-SLOC test cap — this file had
//! headroom under the same `tests_behavior_c/d/e` split convention. #4994 adds
//! the dead-record (`unresumable`) half of the same default-view filter
//! alongside it.
//! Test: `cargo test -p trusty-mpm` runs this file as part of the `tm` binary
//! test suite.

use clap::Parser;

use crate::cli::{Cli, Command, SessionAction};
use crate::commands::managed::{filter_live_sessions, is_live_session_state};

#[test]
fn cli_parses_sessions_plural_canonical() {
    // #2116: `sessions` (plural) is now the canonical top-level spelling —
    // the mirror image of `cli_parses_session_singular` in `tests.rs`, parsing
    // into the new `Command::Sessions` variant.
    let cli = Cli::try_parse_from(["trusty-mpm", "sessions", "list"]).unwrap();
    match cli.command.unwrap() {
        Command::Sessions {
            action: SessionAction::List { dir },
        } => assert_eq!(dir, None),
        other => panic!("expected sessions list, got {other:?}"),
    }
}

#[test]
fn cli_session_and_sessions_agree_for_every_verb() {
    // #2116: proves `tm session <verb>` (deprecated alias) and `tm sessions
    // <verb>` (canonical) resolve to the IDENTICAL `SessionAction` for every
    // verb the enum carries — zero functional difference between the two
    // top-level spellings, only the notice in `emit_top_level_alias_notice`
    // differs. Comparing `{:?}` output sidesteps `SessionAction` not deriving
    // `PartialEq` (it only derives `Debug` via `#[derive(Debug, Subcommand)]`).
    let cases: &[&[&str]] = &[
        &["start"],
        &["stop", "id-1"],
        &["kill", "id-1"],
        &["list"],
        &["tui"],
        &["clean"],
        &["info", "id-1"],
        &["instructions"],
        &["events", "id-1"],
        &["breakers"],
        &["pause", "id-1"],
        &["resume", "id-1"],
        &["run", "id-1", "echo hi"],
        &["output", "id-1"],
        &["new", "https://example.com/o/r.git", "--task", "t"],
        &["ls"],
        &["activity", "id-1"],
        &["send", "id-1", "text"],
        &["answer", "id-1", "yes"],
        &["attach", "id-1"],
        &["managed-stop", "id-1"],
        &["runtime-stop", "id-1"],
        &["managed-resume", "id-1"],
        &["decommission", "id-1"],
        &["delete", "id-1"],
        &["prune-idle"],
        &["decommission-ephemeral"],
        &["catchup"],
        &["prune", "--state", "all"],
        &["prune-worktrees"],
    ];
    for verb_args in cases {
        let mut singular_args = vec!["trusty-mpm", "session"];
        singular_args.extend_from_slice(verb_args);
        let mut plural_args = vec!["trusty-mpm", "sessions"];
        plural_args.extend_from_slice(verb_args);

        let singular_action = match Cli::try_parse_from(singular_args)
            .unwrap_or_else(|e| panic!("`tm session {verb_args:?}` failed to parse: {e}"))
            .command
            .unwrap()
        {
            Command::Session { action } => action,
            other => panic!("expected Command::Session for {verb_args:?}, got {other:?}"),
        };
        let plural_action = match Cli::try_parse_from(plural_args)
            .unwrap_or_else(|e| panic!("`tm sessions {verb_args:?}` failed to parse: {e}"))
            .command
            .unwrap()
        {
            Command::Sessions { action } => action,
            other => panic!("expected Command::Sessions for {verb_args:?}, got {other:?}"),
        };
        assert_eq!(
            format!("{singular_action:?}"),
            format!("{plural_action:?}"),
            "session/sessions parsed to different actions for {verb_args:?}"
        );
    }
}

#[test]
fn top_level_alias_notice_message() {
    // #2116: the top-level `session` -> `sessions` alias reuses the shared
    // `deprecation_message` builder from the #1205 verb-level precedent — pure
    // message-text assertion, mirroring `deprecation_notice_format` in
    // `tests.rs`, since the `eprintln!` side effect itself is untestable here
    // (covered by the `tm_sessions_alias_notice` integration test spawning the
    // real binary).
    use crate::commands::managed::deprecation_message;
    assert_eq!(
        deprecation_message("session", "sessions"),
        "warning: 'session' is deprecated; use 'sessions'"
    );
}

/// #2577 review (CRITICAL finding 1): `unresumable_remedy_line`'s printed text
/// must cite ONLY real `tm session <verb>` subcommands — this is exactly how a
/// nonexistent `tm session rm <id>` shipped in a prior draft (the reviewer
/// built the branch and confirmed `tm session rm` fails with "unrecognized
/// subcommand 'rm'"). Parsing the literal tokens through the real `Cli` is a
/// stronger guarantee than eyeballing the string: it fails the moment a verb
/// is renamed or removed, not just when a typo is introduced.
///
/// Why: a plain substring assertion (`msg.contains("delete")`) would not have
/// caught the original bug — the text also had to be GRAMMATICALLY a real
/// invocation, which only an actual parse proves.
/// What: for each `reason` branch, extracts the exact `tm session …` argv
/// implied by the remedy text and asserts `Cli::try_parse_from` accepts it;
/// also asserts the literal substring `"session rm"` never appears in ANY
/// branch's output (the specific dead verb that shipped).
/// Test: this function IS the test.
#[test]
fn unresumable_remedy_line_cites_real_subcommands() {
    use crate::commands::guided_resume::unresumable_remedy_line;

    let id = "6ca3950b-90d5-4367-94c7-68576b61dafa";

    // workspace_missing → `tm session delete <id> --force` must parse as a
    // real Delete action with force=true.
    let workspace_missing = unresumable_remedy_line(id, Some("workspace_missing"));
    assert!(
        !workspace_missing.contains("session rm"),
        "must never cite the nonexistent `tm session rm` verb, got: {workspace_missing:?}"
    );
    let cli = Cli::try_parse_from(["trusty-mpm", "session", "delete", id, "--force"])
        .expect("the exact verb cited by the workspace_missing remedy must parse");
    match cli.command.unwrap() {
        Command::Session {
            action:
                SessionAction::Delete {
                    id: parsed_id,
                    force,
                },
        } => {
            assert_eq!(parsed_id, id);
            assert!(force, "the cited invocation must pass --force");
        }
        other => panic!("expected session delete, got {other:?}"),
    }

    // pane_gone → must warn before `tm session decommission <id>` (a REAL verb)
    // without instructing the operator to run it outright, and must never
    // recommend the destructive delete/decommission verbs unconditionally.
    let pane_gone = unresumable_remedy_line(id, Some("pane_gone"));
    assert!(
        !pane_gone.contains("session rm"),
        "must never cite the nonexistent `tm session rm` verb, got: {pane_gone:?}"
    );
    assert!(
        pane_gone.contains("sibling window"),
        "pane_gone remedy must explain the sibling-window hazard, got: {pane_gone:?}"
    );
    assert!(
        pane_gone.contains("tmux list-panes") || pane_gone.contains("session info"),
        "pane_gone remedy must point at an inspection step before any teardown, got: {pane_gone:?}"
    );
    let cli = Cli::try_parse_from(["trusty-mpm", "session", "decommission", id])
        .expect("the decommission verb cited by the pane_gone remedy must parse");
    assert!(matches!(
        cli.command.unwrap(),
        Command::Session {
            action: SessionAction::Decommission { .. }
        }
    ));
    let cli_info = Cli::try_parse_from(["trusty-mpm", "session", "info", id])
        .expect("the info verb cited by the pane_gone remedy must parse");
    assert!(matches!(
        cli_info.command.unwrap(),
        Command::Session {
            action: SessionAction::Info { .. }
        }
    ));

    // Unknown/absent reason (e.g. an older daemon) → conservative fallback that
    // names only read-only verbs, never delete/decommission.
    let fallback = unresumable_remedy_line(id, None);
    assert!(
        !fallback.contains("session rm"),
        "must never cite the nonexistent `tm session rm` verb, got: {fallback:?}"
    );
    assert!(
        !fallback.contains("delete") && !fallback.contains("decommission"),
        "the no-reason fallback must not suggest a destructive verb, got: {fallback:?}"
    );
}

/// #2577 review (optional LOW finding): `truncate_for_display` must leave a
/// normal-length daemon error body untouched.
#[test]
fn truncate_for_display_leaves_short_bodies_unchanged() {
    use crate::commands::guided_resume::truncate_for_display;

    let short = "workspace directory /gone no longer exists";
    assert_eq!(truncate_for_display(short), short);
}

/// #2577 review (optional LOW finding): an oversized daemon error body must be
/// capped rather than flooding the operator's terminal scrollback.
#[test]
fn truncate_for_display_caps_long_bodies() {
    use crate::commands::guided_resume::truncate_for_display;

    let long = "x".repeat(5000);
    let result = truncate_for_display(&long);
    assert!(
        result.chars().count() < long.chars().count(),
        "an oversized body must be shortened"
    );
    assert!(
        result.ends_with("… (truncated)"),
        "a truncated body must be marked as such, got tail: {:?}",
        &result[result.len().saturating_sub(20)..]
    );
}

// ── #1809: decommissioned-tombstone filter ────────────────────────────────────

#[test]
fn picker_filter_live_state_excludes_decommissioned() {
    // Why (#1809): `is_live_session_state` is the canonical predicate for
    // "should this session appear in the picker / sessions list by default?".
    // Test: concrete state → expected bool, not derived from the same expression.
    // None of these are #3034 slot tombstones, so `is_slot_tombstone` is `false`
    // throughout — it is only consulted when `state == "deleted"`.
    assert!(
        !is_live_session_state("decommissioned", false, false),
        "decommissioned must be excluded from default view"
    );
    // Active sessions must always be visible.
    assert!(
        is_live_session_state("active", false, false),
        "active must be included in default view"
    );
    // Stopped/errored sessions can still be resumed — they must show.
    assert!(
        is_live_session_state("stopped", false, false),
        "stopped must be included in default view"
    );
    assert!(
        is_live_session_state("errored", false, false),
        "errored must be included in default view"
    );
    // Provisioning sessions are in-flight — they must show.
    assert!(
        is_live_session_state("provisioning", false, false),
        "provisioning must be included in default view"
    );
}

#[test]
fn is_live_session_state_excludes_soft_deleted_record() {
    // Reconciles the #3302 hardening commit (51243ea5, code-critic CRITICAL)
    // with #3034/#3044: a `"deleted"` row backed by a REAL, still-in-store
    // record (soft-deleted via `tm sessions delete`, #2012 — NOT a #3034
    // numbered-slot tombstone, so `is_slot_tombstone` is `false`) must stay
    // excluded from the default picker/list exactly like `decommissioned`, so
    // it is never offered as a resume target (which would resurrect it).
    assert!(
        !is_live_session_state("deleted", false, false),
        "a soft-deleted, still-in-store record must be excluded from the \
         default picker/list view"
    );
    assert!(!is_live_session_state("decommissioned", false, false));
    assert!(is_live_session_state("active", false, false));
    assert!(is_live_session_state("stopped", false, false));
}

#[test]
fn is_live_session_state_keeps_slot_tombstone_visible() {
    // Why (#3034/#3044): a stable-numbering SLOT tombstone — the daemon's
    // `tombstone_summary` placeholder for a slot whose record left the store
    // entirely — is ALSO rendered with wire `state == "deleted"`, but it must
    // stay VISIBLE at its slot in the default view, or the entire point of
    // stable numbering (an operator seeing exactly why a captured number no
    // longer resolves) is defeated. `is_slot_tombstone == true` is what
    // distinguishes it from the soft-deleted-record case above.
    assert!(
        is_live_session_state("deleted", true, false),
        "a #3034 slot tombstone must remain visible in the default view"
    );
    // Resurrection-safety for this visible row is NOT this predicate's job —
    // see `guided_resume_tests`/`session_picker.rs`'s `decide_for_index` for
    // the separate guards that keep it non-resumable regardless.
}

#[test]
fn picker_filter_excludes_decommissioned_keeps_active() {
    // Why (#1809): `filter_live_sessions` must drop decommissioned tombstones and
    // retain all other states. We construct a mixed slice and assert concrete counts
    // and membership — not the same expression used to compute the filter.
    let sessions: Vec<trusty_mpm::client::ManagedSessionSummary> =
        serde_json::from_value(serde_json::json!([
            { "id": "a1", "name": "sess-active",        "state": "active" },
            { "id": "b2", "name": "sess-dead-1",        "state": "decommissioned" },
            { "id": "c3", "name": "sess-stopped",       "state": "stopped" },
            { "id": "d4", "name": "sess-dead-2",        "state": "decommissioned" },
            { "id": "e5", "name": "sess-provisioning",  "state": "provisioning" },
        ]))
        .expect("test data must deserialize");

    let filtered = filter_live_sessions(sessions, &std::collections::HashSet::new());

    // Exactly 3 of the 5 sessions survive the filter.
    assert_eq!(
        filtered.len(),
        3,
        "filter must keep exactly 3 live sessions (active, stopped, provisioning)"
    );
    // Active session must be present.
    assert!(
        filtered.iter().any(|s| s.state == "active"),
        "active session must survive filter"
    );
    // Stopped session must be present (can be resumed).
    assert!(
        filtered.iter().any(|s| s.state == "stopped"),
        "stopped session must survive filter"
    );
    // Provisioning session must be present (in-flight).
    assert!(
        filtered.iter().any(|s| s.state == "provisioning"),
        "provisioning session must survive filter"
    );
    // Neither decommissioned session must appear.
    assert!(
        !filtered.iter().any(|s| s.state == "decommissioned"),
        "decommissioned tombstones must be excluded"
    );
}

#[test]
fn picker_filter_all_live_sessions_unchanged() {
    // Why: when no sessions are decommissioned, `filter_live_sessions` must
    // return all sessions unchanged — no unexpected truncation.
    let sessions: Vec<trusty_mpm::client::ManagedSessionSummary> =
        serde_json::from_value(serde_json::json!([
            { "id": "x1", "name": "sess-a", "state": "active" },
            { "id": "x2", "name": "sess-b", "state": "stopped" },
            { "id": "x3", "name": "sess-c", "state": "errored" },
        ]))
        .expect("test data must deserialize");

    let filtered = filter_live_sessions(sessions, &std::collections::HashSet::new());
    assert_eq!(
        filtered.len(),
        3,
        "all-live input must pass through unchanged (3 sessions)"
    );
}

#[test]
fn picker_filter_all_decommissioned_returns_empty() {
    // Why: if every session is decommissioned, the picker must show an empty list
    // (not crash or return some sessions).
    let sessions: Vec<trusty_mpm::client::ManagedSessionSummary> =
        serde_json::from_value(serde_json::json!([
            { "id": "z1", "name": "old-1", "state": "decommissioned" },
            { "id": "z2", "name": "old-2", "state": "decommissioned" },
        ]))
        .expect("test data must deserialize");

    let filtered = filter_live_sessions(sessions, &std::collections::HashSet::new());
    assert!(
        filtered.is_empty(),
        "all-decommissioned input must produce empty list"
    );
}

#[test]
fn picker_filter_keeps_slot_tombstone_hides_soft_deleted_record() {
    // Why (#3034/#3044 reconciliation): both rows below serialize wire
    // `state == "deleted"`, but only the slot-tombstone row sets the
    // dedicated `deleted: bool` field — `filter_live_sessions` must tell them
    // apart via that field rather than the state string alone.
    let sessions: Vec<trusty_mpm::client::ManagedSessionSummary> =
        serde_json::from_value(serde_json::json!([
            { "id": "a1", "name": "sess-active", "state": "active" },
            // #3034 numbered-slot tombstone — must stay visible.
            { "id": "", "name": "", "state": "deleted", "slot": 2, "deleted": true },
            // #3302 soft-deleted, still-in-store record — must stay hidden.
            { "id": "b2", "name": "sess-soft-deleted", "state": "deleted" },
        ]))
        .expect("test data must deserialize");

    let filtered = filter_live_sessions(sessions, &std::collections::HashSet::new());

    assert_eq!(
        filtered.len(),
        2,
        "exactly the active session and the slot tombstone must survive"
    );
    assert!(
        filtered.iter().any(|s| s.deleted),
        "the slot tombstone (deleted: true) must survive the filter"
    );
    assert!(
        !filtered.iter().any(|s| s.state == "deleted" && !s.deleted),
        "the soft-deleted, still-in-store record (deleted: false) must be excluded"
    );
}

// ── #4994: the default view hides what the SWEEP classified dead ─────────────
//
// 🔴 Read `classified_dead` literally in every assertion below: it is the
// listing-time prune's own verdict, never the wire's `unresumable` flag. The
// two differ on a live session — see `picker_filter_keeps_a_live_row_flagged_
// unresumable`, which is the case that made PR #4995's first cut a CRITICAL.

#[test]
fn is_live_session_state_hides_a_record_the_prune_classified_dead() {
    // Why (#4994): the reported defect. A record whose tmux pane is gone AND
    // whose workspace directory no longer exists anywhere on disk is classified
    // dead by the sweep and rendered `[dead]`, yet still took the catch-all
    // `true` arm into the default `tm ls`. Six such rows sat in the owner's
    // list. `classified_dead` is the ONLY input that differs between these
    // assertions and `is_live_session_state_keeps_resumable_stopped_record`.
    assert!(
        !is_live_session_state("stopped", false, true),
        "a stopped record the sweep classified dead must be hidden from the default view"
    );
    assert!(
        !is_live_session_state("errored", false, true),
        "an errored record the sweep classified dead must be hidden from the default view"
    );
}

#[test]
fn is_live_session_state_keeps_resumable_stopped_record() {
    // 🔴 The regression guard for the hiding rule above. MOST stopped records
    // are legitimately resumable — 18 on the reporting machine against 6 dead
    // ones. Hiding a stopped record on its STATE rather than on the sweep's
    // verdict would take those 18 with it.
    assert!(
        is_live_session_state("stopped", false, false),
        "a stopped record with an intact workspace is resumable and must stay visible"
    );
    assert!(
        is_live_session_state("errored", false, false),
        "an errored record with an intact workspace must stay visible"
    );
    assert!(
        is_live_session_state("active", false, false),
        "an active record must stay visible"
    );
}

#[test]
fn is_live_session_state_keeps_slot_tombstone_even_when_classified_dead() {
    // Why (#3034 × #4994): the two rules meet on one row. A slot tombstone is a
    // rendering placeholder for a slot whose record left the store, and Bob's
    // #3034 directive keeps it visible at its slot unconditionally. The
    // `classified_dead` arm therefore sits BELOW the `"deleted"` arm —
    // reordering them would silently un-do #3034.
    assert!(
        is_live_session_state("deleted", true, true),
        "a #3034 slot tombstone must stay visible regardless of the dead classification"
    );
    // The soft-deleted-record and decommissioned rules are unmoved by the new
    // input.
    assert!(!is_live_session_state("deleted", false, false));
    assert!(!is_live_session_state("decommissioned", false, false));
}

#[test]
fn picker_filter_hides_only_what_the_prune_classified_dead() {
    // The list-level mirror: a mixed fleet where the two stopped rows differ
    // ONLY by membership in `dead_ids`, so "the row is absent" can only be
    // explained by the sweep's verdict.
    let sessions: Vec<trusty_mpm::client::ManagedSessionSummary> =
        serde_json::from_value(serde_json::json!([
            { "id": "a1", "name": "sess-active",   "state": "active" },
            { "id": "b2", "name": "sess-alive",    "state": "stopped" },
            { "id": "c3", "name": "sess-dead",     "state": "stopped" },
            { "id": "d4", "name": "sess-dead-err", "state": "errored" },
            { "id": "",   "name": "",              "state": "deleted", "slot": 5, "deleted": true },
        ]))
        .expect("test data must deserialize");

    let dead: std::collections::HashSet<String> =
        ["c3".to_string(), "d4".to_string()].into_iter().collect();
    let filtered = filter_live_sessions(sessions, &dead);

    assert_eq!(
        filtered.iter().map(|s| s.id.as_str()).collect::<Vec<_>>(),
        vec!["a1", "b2", ""],
        "exactly the active row, the stopped row the sweep left alone, and the \
         slot tombstone must survive"
    );
}

#[test]
fn picker_filter_keeps_a_live_row_flagged_unresumable() {
    // 🔴 CRITICAL regression guard (PR #4995 review). `unresumable` is computed
    // daemon-side from a record's PERSISTED state; `reconcile_live_state` then
    // overwrites the DISPLAY state without recomputing it, so the wire really
    // does ship `state: "active"` with `unresumable: true` for a session whose
    // pane is running. Both halves of that are pinned daemon-side, in the order
    // `summary.rs` runs them: `checked_summaries_with` probes `unresumable` for
    // persisted-`Stopped`/`Errored` records only, then
    // `reconcile_live_state_flips_stopped_to_active_when_alive` shows the
    // display state flipping to `active` with no second probe.
    //
    // The sweep refuses to reap such a row
    // (`auto_prune_never_touches_a_running_record`,
    // `auto_prune_never_touches_an_errored_record_with_a_live_detached_pane`),
    // so hiding it would leave a running agent hidden AND unreapable.
    //
    // Both rows below carry `unresumable: true` and neither is in `dead_ids`.
    // A filter that consults the flag fails here; one that consults the sweep's
    // verdict passes.
    let sessions: Vec<trusty_mpm::client::ManagedSessionSummary> =
        serde_json::from_value(serde_json::json!([
            { "id": "a1", "name": "live-pane", "state": "active",  "unresumable": true },
            { "id": "b2", "name": "detached",  "state": "errored", "unresumable": true },
        ]))
        .expect("test data must deserialize");

    let filtered = filter_live_sessions(sessions, &std::collections::HashSet::new());

    assert_eq!(
        filtered.iter().map(|s| s.id.as_str()).collect::<Vec<_>>(),
        vec!["a1", "b2"],
        "a row carrying the raw `unresumable` flag that the sweep did NOT \
         classify dead must stay visible — hiding it makes a live session \
         unreachable through `tm`"
    );
}

#[test]
fn scope_for_display_all_keeps_dead_record_visible() {
    // #4994 is a VISIBILITY change, never a deletion: `tm ls --all` must still
    // list every dead record, exactly as it already does for decommissioned
    // ones. This is the operator's path to see and act on them; the remedy verb
    // is `tm sessions delete <id> --force`, the same one
    // `unresumable_remedy_line` cites (there is no `tm sessions rm`).
    use crate::commands::session_picker::{parse_managed_sessions, scope_for_display};

    let raw = serde_json::json!({
        "sessions": [
            { "id": "a1", "name": "sess-active", "state": "active",  "slot": 1 },
            { "id": "c3", "name": "sess-dead",   "state": "stopped", "slot": 2 },
        ]
    })
    .to_string();
    let sessions = parse_managed_sessions(&raw).expect("parse must succeed");
    let dead: std::collections::HashSet<String> = ["c3".to_string()].into_iter().collect();

    assert_eq!(
        scope_for_display(sessions.clone(), true, &dead)
            .iter()
            .map(|s| s.id.as_str())
            .collect::<Vec<_>>(),
        vec!["a1", "c3"],
        "--all must keep the dead record"
    );
    assert_eq!(
        scope_for_display(sessions, false, &dead)
            .iter()
            .map(|s| s.id.as_str())
            .collect::<Vec<_>>(),
        vec!["a1"],
        "the default view must drop the same record --all keeps"
    );
}