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
//! `delete_record` coverage — soft-delete `--deleted--` marker, the fail-closed
//! running-guard, `--force` bypass, and the workspace-dir-untouched invariant (#2012).
//!
//! Why: `session_manager/tests.rs` is at the 1500-SLOC test cap; this
//! #2012-specific coverage lives here so neither file grows past its 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. The #2022
//! fix to the SAME guard (a real tmux liveness probe, not a persisted-state
//! check) is covered separately in `liveness_tests.rs` to keep this file
//! focused and both files under the SLOC cap.
//! What: four tests exercising [`super::manager::SessionManager::delete_record`]
//! — the `--deleted--` soft-delete mark, the running-guard refusal, the
//! `--force` bypass, and proof that the workspace directory is never touched.
//! Test: this file IS the test module; run with `cargo test -p trusty-mpm`.
use tempfile::TempDir;
use super::manager::ManagedError;
use super::record::{ManagedSessionId, ManagedSessionState};
use super::tests::{make_manager, seed_record};
/// `stop` REFUSES a terminal (Deleted/Decommissioned) record — the daemon-side
/// backstop against resurrection (code-critic CRITICAL).
///
/// Why: the zombie-reconcile resume path does `runtime-stop` then `resume`; if
/// `stop` flipped a `Deleted` tombstone to `Stopped`, the follow-up `resume`
/// would bring a DELETED session back to life. This pins that `stop` rejects a
/// terminal record with `InvalidState` and leaves its state unchanged.
/// Test: this function IS the test.
#[tokio::test]
async fn stop_refuses_terminal_record() {
let dir = TempDir::new().unwrap();
let (mgr, _fake) = make_manager(&dir).await;
for terminal in [
ManagedSessionState::Deleted,
ManagedSessionState::Decommissioned,
] {
let id = ManagedSessionId::new();
seed_record(&mgr, &dir, id, terminal.clone(), false).await;
let err = mgr
.stop(&id)
.await
.expect_err("a terminal record must not be stoppable (resurrection guard)");
assert!(
matches!(err, ManagedError::InvalidState(_, _)),
"expected InvalidState for {terminal:?}, got {err:?}"
);
// State is unchanged — the tombstone was not flipped to Stopped.
assert_eq!(mgr.get(&id).await.expect("record present").state, terminal);
}
}
/// `delete_record` marks a non-running record `--deleted--`, keeping it in the
/// store (#2012, soft-delete marker).
///
/// Why: `tm sessions delete` must REFLECT the deletion in the master list
/// (state `Deleted`, rendered `--deleted--`) rather than silently dropping the
/// record — the "fully-tracked lifecycle, no fire-and-forget" standard.
/// What: seeds a `Stopped` (non-running) record, deletes without `--force`, and
/// asserts it is STILL in the store with state `Deleted`.
/// Test: this function IS the test.
#[tokio::test]
async fn delete_record_marks_deleted() {
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;
let prior = mgr.delete_record(&id, false).await.expect("delete");
// The returned snapshot is the PRE-deletion state.
assert_eq!(prior.state, ManagedSessionState::Stopped);
// The record is still tracked, now marked `Deleted` (rendered `--deleted--`).
let after = mgr.get(&id).await.expect("record must still exist");
assert_eq!(after.state, ManagedSessionState::Deleted);
}
/// `delete_record` fail-closed guard refuses a RUNNING session without `--force` (#2012).
///
/// Why: the #2012 safety requirement — an operator must not be able to
/// accidentally hard-delete the record of a live session. The record must
/// still be present afterward (nothing mutated on refusal).
/// What: seeds an `Active` record, calls `delete_record(id, force=false)`,
/// asserts an `InvalidState` error naming the session, and asserts the record
/// is still retrievable (untouched).
/// Test: this function IS the test.
#[tokio::test]
async fn delete_record_refuses_running_without_force() {
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;
let err = mgr
.delete_record(&id, false)
.await
.expect_err("must refuse to delete a running session without --force");
assert!(
matches!(err, ManagedError::InvalidState(ref sid, _) if sid == &id.to_string()),
"expected InvalidState for {id}, got: {err:?}"
);
// The record must be untouched by the refused delete.
let still_there = mgr.get(&id).await.expect("record must still exist");
assert_eq!(still_there.state, ManagedSessionState::Active);
}
/// `--force` bypasses the running-state guard (#2012).
///
/// Why: an operator who explicitly opts in via `--force` must be able to
/// hard-delete a running session's record (e.g. to clear a stuck/duplicate
/// entry) without first stopping it.
/// What: seeds an `Active` record, calls `delete_record(id, force=true)`, and
/// asserts the record is marked `Deleted` (kept in the store).
/// Test: this function IS the test.
#[tokio::test]
async fn delete_record_force_bypasses_running_guard() {
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;
mgr.delete_record(&id, true)
.await
.expect("--force must bypass the running guard");
let after = mgr.get(&id).await.expect("record must still exist");
assert_eq!(after.state, ManagedSessionState::Deleted);
}
/// `delete_record` NEVER removes the workspace directory from disk (#2012).
///
/// Why: hard-deleting the RECORD is deliberately a store-only operation —
/// distinct from `decommission`, which may `remove_dir_all` an owned
/// workspace. A workspace dir that existed before delete must still exist
/// (untouched) after the record is gone.
/// What: seeds a `Stopped` record with a real on-disk workspace dir, deletes
/// the record, and asserts the workspace directory still exists on disk even
/// though the record is now marked `Deleted`.
/// Test: this function IS the test.
#[tokio::test]
async fn delete_record_never_touches_workspace_dir() {
let dir = TempDir::new().unwrap();
let (mgr, _fake) = make_manager(&dir).await;
let id = ManagedSessionId::new();
let ws = seed_record(&mgr, &dir, id, ManagedSessionState::Stopped, false).await;
assert!(ws.exists(), "test invariant: workspace dir must pre-exist");
mgr.delete_record(&id, false).await.expect("delete");
assert_eq!(
mgr.get(&id).await.expect("record still tracked").state,
ManagedSessionState::Deleted,
"record must be marked Deleted (kept in the store)"
);
assert!(
ws.exists(),
"workspace directory must NOT be removed by delete_record (store-only op)"
);
}
/// `delete_record` clears a stale `pending_decision`/`proposed_default` on the
/// persisted record (#4400).
///
/// Why: `Deleted` is terminal — the same reasoning `decommission_with_root`
/// applies (issue #4400): no human will ever act on a `pending_decision`
/// raised before a soft-delete, so leaving it set would let
/// `supervisor_status`'s human-confirmation queue accumulate another
/// phantom-gate source alongside decommissioned records.
/// What: seeds a `Stopped` record, sets a `pending_decision` +
/// `proposed_default` via `set_pending_decision`, deletes it, and asserts the
/// PERSISTED copy (`mgr.get`) has both fields cleared. The returned
/// PRE-deletion snapshot is expected to still carry the original decision
/// (documented behavior — callers render the state it was in before
/// deletion), so only the persisted copy is asserted clear.
/// Test: this function IS the test.
#[tokio::test]
async fn delete_record_clears_pending_decision() {
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;
mgr.set_pending_decision(&id, "merge or rebase?", Some("merge"))
.await
.expect("set_pending_decision");
mgr.delete_record(&id, false).await.expect("delete");
let after = mgr.get(&id).await.expect("record must still exist");
assert_eq!(after.state, ManagedSessionState::Deleted);
assert!(
after.pending_decision.is_none(),
"pending_decision must be cleared by delete_record"
);
assert!(
after.proposed_default.is_none(),
"proposed_default must be cleared by delete_record"
);
}