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
//! Interactive-picker delete action + shared managed→local delete routing (#2304).
//!
//! Why: Bob's report — "I still don't see a way to delete sessions from the CLI".
//! A non-interactive `tm session delete <id>` exists (#2012) but it is
//! managed-store-only (404s on a project-only session) and undiscoverable
//! (requires knowing a UUID). The `tm ls` picker is the discoverable surface, so
//! deletion belongs there. This module holds the pure decision seams (family
//! routing + running-session force guard) plus the small I/O driver that runs the
//! confirm prompt — kept out of `session_picker.rs` so that file stays under the
//! 500-SLOC production cap.
//!
//! What: [`delete_needs_force`] is the MANAGED-family running-session guard
//! (persisted-state heuristic — the daemon does the real tmux probe);
//! [`local_session_needs_force`] is the analogous guard for the LOCAL
//! (project-session) family, enforced client-side because `DELETE
//! /sessions/{id}` has no server-side running guard of its own;
//! [`classify_managed_delete`] is the family-routing seam (managed 200/404/409
//! → keep / fall back to the project-session store / surface the guard
//! refusal); [`delete_managed_then_local`] is the shared I/O helper both the
//! picker and `tm session delete` call so the deletion endpoints are never
//! duplicated; [`confirm_and_delete`] is the picker's TTY confirm-then-delete
//! driver.
//!
//! IMPORTANT — reachability: the `tm ls` picker only ever lists MANAGED
//! sessions (`list_managed_sessions` is sourced solely from
//! `SessionManager::list()`), so [`delete_managed_then_local`]'s `FallbackLocal`
//! branch is effectively unreachable from the picker (barring a delete-after-list
//! race). The branch exists for the non-interactive `tm session delete
//! <free-text-id>` verb, which accepts ANY id/name and must behave correctly
//! when that id names a project-only (local) session instead of a managed one.
//!
//! Test: `delete_needs_force_*`, `local_session_needs_force_*`,
//! `classify_managed_delete_*`, `confirm_line_*`, and the real-HTTP
//! `local_delete_*` round-trip tests in `tests_behavior_d_tests.rs`. The
//! stdin-driven half of `confirm_and_delete` is inherently side-effect-only and
//! is exercised by manual smoke tests + the e2e suite.
use Write as _;
use ManagedSessionSummary;
use ;
/// Outcome of rendering a routed delete — what the caller should print/return.
///
/// Why: [`delete_managed_then_local`] serves two call sites (the picker and the
/// non-interactive subcommand) that render differently and have different exit
/// semantics; returning a small report rather than printing inside the helper
/// keeps the deletion routing in one place while letting each surface own its UX.
/// What: `Deleted` carries the pre-deletion identity and whether it came from the
/// local (project-session) store; `NotFound` means neither store had the id;
/// `Refused` carries the running-guard message — either the daemon's managed
/// 409 body, or the CLI-side local-session guard's own message (there is no
/// server-side guard for the legacy `DELETE /sessions/{id}` route).
/// Test: constructed by `delete_managed_then_local`; rendering covered by the
/// callers' behaviour.
pub
/// Next step after the managed-delete attempt, keyed on the HTTP status.
///
/// Why: folding the status→action mapping into one pure enum makes the
/// family-routing decision exhaustively unit-testable without a live daemon.
/// What: `Deleted` = 200 (record removed from the managed store); `FallbackLocal`
/// = 404 (not a managed-family session — try the project-session store);
/// `Refused` = 409 (the running-session guard fired); `Error` = any other status.
/// Test: `classify_managed_delete_*` in `tests_behavior_d_tests.rs`.
pub
/// Decide whether hard-deleting a session in state `state` requires `--force`.
///
/// Why: the picker must never silently hard-delete a live session (#2304 item 3).
/// A `stopped`/`errored` session has no live runtime, so a plain confirmation is
/// enough; anything else (`active`/`provisioning`) is treated as running and
/// requires an explicit force-confirm, mirroring `delete_record`'s fail-closed
/// guard. This is the persisted-state heuristic that decides which prompt to show;
/// the daemon still runs the authoritative tmux-liveness probe and can 409 even
/// when this returns false, which the routing surfaces rather than auto-forcing.
/// What: returns `true` for every state except `stopped`/`errored` (via
/// [`super::guided_resume::needs_restart`], the same stopped/errored classifier
/// the resume path uses).
/// Test: `delete_needs_force_running_true`, `delete_needs_force_stopped_false`,
/// `delete_needs_force_errored_false` in `tests_behavior_d_tests.rs`.
pub
/// Map a managed-delete HTTP status to the next routing step.
///
/// Why: pure seam for the family routing so 200/404/409/other are testable
/// without HTTP. A 404 specifically means the id is NOT a managed record. The
/// `tm ls` picker never surfaces this branch in practice (every entry it offers
/// IS a managed record — see the module doc); it exists for the non-interactive
/// `tm session delete <id>`, which accepts free-text ids that may name a
/// project-only (local) session instead, so 404 routes to the local fallback
/// rather than reporting failure.
/// What: 200→`Deleted`, 404→`FallbackLocal`, 409→`Refused`, else→`Error`.
/// Test: `classify_managed_delete_ok`, `classify_managed_delete_not_found`,
/// `classify_managed_delete_conflict`, `classify_managed_delete_other` in
/// `tests_behavior_d_tests.rs`.
pub
/// Interpret a confirmation line for a non-force (safe) delete.
///
/// Why: a single classifier keeps the accepted spellings identical across the
/// picker's prompts and any future call site, and makes the accept/reject rule
/// unit-testable without stdin.
/// What: returns `true` for `y`/`yes` (case-insensitive, trimmed); everything
/// else (including empty — the safe default) is `false`.
/// Test: `confirm_line_yes_variants`, `confirm_line_default_rejects` in
/// `tests_behavior_d_tests.rs`.
pub
/// Interpret a confirmation line for a force (running-session) delete.
///
/// Why: deleting a running session is destructive, so a bare `y` must not be
/// enough — the operator types the word `force` explicitly, mirroring the
/// `--force` flag on the non-interactive verb (#2304 item 3).
/// What: returns `true` only for the exact word `force` (case-insensitive,
/// trimmed); everything else is `false`.
/// Test: `confirm_line_force_accepts`, `confirm_line_force_rejects_yes` in
/// `tests_behavior_d_tests.rs`.
pub
/// Decide whether hard-deleting a LOCAL (project) session requires `--force`.
///
/// Why: `DELETE /sessions/{id}` (`remove_session` in `daemon/api.rs`)
/// unconditionally kills the tmux host for ANY status — unlike the managed
/// family, there is NO server-side running guard on this route (this was the
/// CRITICAL gap: a bare `tm session delete <local-id>` used to 404 harmlessly
/// before this module existed, and after this module's first cut it silently
/// force-killed a live local session). The CLI must therefore enforce the same
/// fail-closed contract client-side, by probing the session's status before
/// ever issuing the DELETE.
/// What: returns `true` for every [`SessionStatus`] except [`SessionStatus::Stopped`]
/// — i.e. `Starting`/`Active`/`AwaitingApproval`/`Detached`/`Paused` all mean a
/// live process still backs the session and require an explicit `--force`
/// (or the picker's force-confirm) to delete.
/// Test: `local_session_needs_force_stopped_false`,
/// `local_session_needs_force_active_and_others_true` in
/// `tests_behavior_d_tests.rs`.
pub
/// Try the managed hard-delete, falling back to the project-session delete on 404.
///
/// Why: the picker and `tm session delete` must route a delete to the correct
/// store WITHOUT re-implementing either deletion endpoint. Both call this one
/// helper: it POSTs the managed hard-delete first (every `tm ls` picker entry IS
/// a managed record, so this is the common — in practice only — path there) and,
/// only when the record is absent there (404 — reached in practice by the
/// non-interactive `tm session delete <free-text-id>` when the id names a
/// project-only session), issues the guarded local delete.
/// What: POSTs `/api/v1/sessions/managed/{id}/delete?force=<force>`, classifies
/// the status via [`classify_managed_delete`], and returns a [`DeleteReport`]. A
/// 409 returns `Refused` (never auto-escalates to force); a 200 parses the
/// flattened pre-deletion summary; a 404 delegates to [`delete_local`] (which
/// applies its OWN running guard) with the same `force` flag threaded through —
/// so a picker force-confirm (`force = true`) still bypasses the local guard,
/// while a plain non-interactive call without `--force` does not. Non-success
/// statuses on the managed leg propagate as an `Err`.
/// Test: side-effect-only HTTP driver — the status→route decision is unit-tested
/// via [`classify_managed_delete`]; the local guard's real-HTTP round trip is
/// covered by `local_delete_*` in `tests_behavior_d_tests.rs`.
pub async
/// Delete a project-session record via `DELETE /sessions/{id}` (the local path),
/// refusing a still-running session unless `force` is set (#2304 CRITICAL fix).
///
/// Why: a session the managed store does not know is a project-session record —
/// reached in practice only via the non-interactive `tm session delete
/// <free-text-id>` (see the module doc: the picker never lists local sessions).
/// `DELETE /sessions/{id}` itself has no server-side running guard (unlike the
/// managed family's 409), so this function is the ONLY place that stands between
/// an operator and force-killing a live local session by accident — it must
/// check liveness BEFORE issuing the DELETE, not rely on the daemon to refuse.
/// What: GETs `/sessions/{id}` first (a 404 there is `NotFound` — nothing to
/// delete, no guard to apply). When [`local_session_needs_force`] finds the
/// fetched status is not `Stopped` and `force` is `false`, returns `Refused`
/// WITHOUT ever issuing the DELETE. Otherwise (force is `true`, or the session
/// is already `Stopped`) issues `DELETE /sessions/{id}` — the same full-removal
/// path `tm session stop` uses for a local session, reused rather than
/// duplicated — and reports `Deleted { local: true }` (name falls back to the
/// id; the local endpoint returns no body to read a display name from).
/// Test: `local_delete_refuses_running_session_without_force`,
/// `local_delete_allows_stopped_session_without_force`,
/// `local_delete_force_bypasses_guard_on_running_session` (real-HTTP round trip
/// against a loopback daemon) in `tests_behavior_d_tests.rs`.
async
/// Picker action: confirm, then delete the selected session (#2304).
///
/// Why: the interactive delete must be explicit and, for a running session,
/// force-confirmed — never a silent destructive default. This is the TTY driver
/// that renders the confirm prompt and calls the shared routing helper.
/// What: computes [`delete_needs_force`] from the session state; for a running
/// session it prints a force warning and requires the operator to type `force`
/// ([`confirm_is_force`]); otherwise it requires `y`/`yes` ([`confirm_is_yes`]).
/// On confirm it calls [`delete_managed_then_local`] with the matching `force`
/// flag and renders the [`DeleteReport`]. Returns `Ok(true)` only when a record
/// was actually removed (so the caller knows the list changed); a cancel, an EOF,
/// a 409 refusal, or a not-found all return `Ok(false)`.
/// Test: stdin/HTTP path is side-effect-only (manual smoke + e2e); the pure
/// confirm/route/guard seams it composes are unit-tested (see module doc).
pub async