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
//! `tm sessions rename` — rename a managed session (from the list or in-session).
//!
//! Why: extracted from `commands::managed`/`commands::session` (both near the
//! 500-SLOC production cap) into its own file, mirroring how `commands::delete`
//! separates the `delete` verb. A managed session's identity name is its tmux
//! name, so a rename mutates the record AND the live tmux session — both owned
//! by the daemon, so this issues a daemon PATCH rather than touching state
//! directly.
//! What: [`session_rename`] resolves the target (an explicit id/name, or the
//! CURRENT session from `$TM_MANAGED_SESSION_ID` when only a new name is given),
//! PATCHes `/api/v1/sessions/managed/{id}` with `{ "name": … }`, and renders the
//! 400/404/success outcomes — printing the DAEMON-confirmed name from the
//! response body, which may be an auto-suffixed variant of the requested one
//! when it collided (#3692; see [`rename_success_message`]). The in-session form cross-checks the env var
//! against this process's own tmux pane before trusting it (issue #3600, see
//! [`confirm_rename_target_pane`]) — `$TM_MANAGED_SESSION_ID` is published
//! SESSION-scoped (`tmux set-environment -t <session>`), so a sibling pane can
//! inherit an identical value and would otherwise rename a session it does not
//! own.
//! Test: CLI parse by `cli_parses_sessions_rename_*`; HTTP path by
//! `rename_route_*` in `tests/session_lifecycle.rs`; the pane cross-check by
//! `rename_tests.rs`.
use crateEnvSessionIdentity;
/// Environment variable a managed session exports carrying its own managed id,
/// used to resolve "the session I'm currently in" for the in-session rename form.
const CURRENT_SESSION_ENV: &str = "TM_MANAGED_SESSION_ID";
/// Cross-check the in-session rename target's own `pane_id` against this
/// process's CURRENT tmux pane before trusting `session_id` (issue #3600).
///
/// Why: `TM_MANAGED_SESSION_ID` is tmux SESSION-scoped (`tmux
/// set-environment -t <session>`), not pane-scoped — a sibling pane opened
/// after a runtime-exit heal inherits the SAME env var and, without this
/// check, would rename a session it does not own — possibly another user's
/// live session. `guided.rs`'s nested-session guard already fixed the
/// identical hijack for its own call path; this reuses the shared
/// [`crate::commands::pane_identity::EnvSessionIdentity`] cross-check rather
/// than trusting the bare env var a second time.
/// What: `Ok(())` only when both pane ids are known and equal
/// ([`EnvSessionIdentity::Confirmed`]). `Err(<message>)` naming `session_id`
/// and both pane ids for a genuine mismatch, or explaining that identity
/// could not be verified at all (not inside tmux, a tmux query failure, or
/// `session_id` unknown to the daemon) — refuses in BOTH cases, since
/// silently renaming the wrong session is far worse than an error the
/// operator can retry with an explicit target.
/// Test: `confirm_rename_target_pane_ok_when_confirmed`,
/// `confirm_rename_target_pane_refuses_on_mismatch`,
/// `confirm_rename_target_pane_refuses_when_unavailable` (`rename_tests.rs`).
/// Resolve the in-session rename target: fetch `session_id`'s own record and
/// cross-check its `pane_id` against `current_pane_id` (issue #3600).
///
/// Why: separates the daemon round trip from the pure decision in
/// [`confirm_rename_target_pane`] so each is independently testable — the
/// fetch via a mock daemon, the decision via plain unit tests. Returning the
/// resolved [`ManagedSessionSummary`] on success also lets [`session_rename`]
/// skip a second `resolve_managed_match` round trip for the id it already has.
/// What: `Ok(record)` only when the record is found AND
/// [`confirm_rename_target_pane`] confirms pane identity; `Err(<message>)`
/// otherwise (record not found collapses into the same "unavailable" refusal
/// as an unresolvable pane — both mean identity cannot be proven).
/// Test: `resolve_in_session_rename_target_confirmed`,
/// `resolve_in_session_rename_target_refuses_on_pane_mismatch`,
/// `resolve_in_session_rename_target_refuses_when_record_not_found`
/// (`rename_tests.rs`).
async
/// `tm sessions rename <id-or-name> <new-name>` / `tm sessions rename <new-name>`.
///
/// Why: operators rename a session either by naming it explicitly from the
/// master list, or — from WITHIN a session — by giving just the new name, with
/// the current session resolved from `$TM_MANAGED_SESSION_ID` (the id every
/// managed session exports into its own tmux/runtime environment).
/// What: when `arg2` is `Some`, `arg1` is the target (id or friendly name,
/// resolved to a managed id via the shared resolver) and `arg2` is the new
/// name; when `arg2` is `None`, `arg1` is the new name and the target is the
/// current session read from `$TM_MANAGED_SESSION_ID` (an error if unset — the
/// operator is not inside a managed session), cross-checked against this
/// process's own tmux pane via [`resolve_in_session_rename_target`] (issue
/// #3600) before being trusted. PATCHes `/api/v1/sessions/managed/{id}` with
/// `{ "name": <new> }`; a 404 bails, a 409 (name collision) or 400 (invalid
/// name) prints the daemon's actionable message and returns `Err` (non-zero
/// exit), and success prints a confirmation.
/// Test: `cli_parses_sessions_rename_two_args`,
/// `cli_parses_sessions_rename_in_session`; HTTP path by `rename_route_*`;
/// the pane cross-check by `rename_tests.rs`.
pub async
/// Issue the PATCH and render the 404/409/400/success outcomes — shared by
/// both `session_rename` branches (explicit target and the in-session,
/// pane-cross-checked form). Thin printing shell over [`do_rename_request`]
/// (the testable part).
async
/// Issue the rename PATCH and build the success message from the DAEMON'S
/// confirmed name, not the locally-requested one (issue #3692 review HIGH-1).
///
/// Why: since #3692, `SessionManager::rename` never rejects a collision — it
/// auto-suffixes, so the name the daemon actually applied (in the response
/// body's session summary) can differ from the one the operator asked for.
/// Printing the requested name would tell the operator a rename happened that
/// didn't ("renamed → tm-x" when the session is really `tm-x-2`), poisoning
/// their next `tmux attach`/resume-by-name.
/// What: PATCHes `{ "name": … }`; 404 bails; 400 (and the legacy 409, kept for
/// older daemons) surfaces the daemon's message on stderr and errors. Any
/// OTHER non-success status reads the body and carries it into the error — the
/// daemon writes an actionable reason there (e.g. `store error: session store
/// serialization error: trailing characters at line 3755 column 2` when
/// `sessions.json` is corrupt), and `error_for_status()` used to DISCARD it,
/// leaving the operator with a bare `HTTP status server error (500 …)`. On
/// success, parses the response body's `name` field and returns
/// [`rename_success_message`] built from it — falling back to the requested
/// name only when the body carries none (an older daemon). Returns the
/// message rather than printing so tests can assert it.
/// Test: `do_rename_request_reports_server_confirmed_suffixed_name`,
/// `do_rename_request_reports_plain_success`,
/// `do_rename_request_surfaces_daemon_500_body`,
/// `do_rename_request_reports_bare_status_when_500_body_is_empty`
/// (`rename_tests.rs`).
///
/// `pub(super)` (rather than private): the `tm ls`/bare-`tm` picker's `r<N>
/// <new-name>` rename input mode (`commands::session_picker_rename`) calls
/// this directly — it already holds a resolved `id` from the numbered
/// session menu, so it skips `session_rename`'s name/env-var resolution and
/// #3600 pane cross-check entirely, but reuses this exact hardened PATCH +
/// #3692 auto-suffix-aware response handling rather than reimplementing any
/// of it.
pub async
/// Build the rename success line from the server-confirmed name (issue #3692
/// review HIGH-1) — pure, so the collided-and-suffixed wording is unit-tested.
///
/// Why: see [`do_rename_request`] — the printed name must be the one the
/// daemon applied, and a silent suffix would leave the operator believing the
/// requested name stuck.
/// What: `confirmed = None` (older daemon, no parseable body) falls back to
/// the requested name. When the confirmed name differs from the requested
/// one, the message says so explicitly ("was taken; auto-suffixed").
/// Test: `rename_success_message_plain`,
/// `rename_success_message_notes_suffix_on_collision`,
/// `rename_success_message_falls_back_without_body` (`rename_tests.rs`).