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
//! External (non-trusty-mpm) tmux session model.
//!
//! Why: trusty-mpm should observe and manage *every* tmux session on the host,
//! not just the ones it spawned. The dashboard needs to tell apart sessions
//! the daemon created (`tm-*` / `tmpm-*` / `trusty-mpm-*`) from pre-existing
//! ones so it can offer to "adopt" the latter for oversight without touching
//! them.
//! What: [`SessionOrigin`] classifies a session by name prefix, and
//! [`ExternalSession`] is the structured, origin-tagged view of one tmux
//! session row used by the `GET /tmux/sessions` endpoint.
//! Test: `cargo test -p trusty-mpm-core external_session` covers the prefix
//! classification and the JSON round-trip.
use serde::{Deserialize, Serialize};
/// Where a tmux session came from, by name convention.
///
/// Why: the daemon treats its own sessions and externally-created ones
/// differently — only external sessions need an explicit "adopt" step before
/// oversight applies.
/// What: [`TrustyMpm`](SessionOrigin::TrustyMpm) for `tm-*` / `tmpm-*` /
/// `trusty-mpm-*` names (the current `tm-<leaf>-NN` scheme from #1955, the
/// #1789-era `tmpm-<repo>-<8hex>`/`tmpm-<folder>`/`tmpm-<adjective>-<noun>`
/// forms, and the pre-#1789 `trusty-mpm-<uuid>` form), else
/// [`External`](SessionOrigin::External).
/// Test: `classifies_trusty_mpm_prefixes`, `classifies_external`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum SessionOrigin {
/// A session created by trusty-mpm (name starts with `tm-`/`tmpm-`/`trusty-mpm-`).
TrustyMpm,
/// A session that pre-existed or was created outside trusty-mpm.
External,
}
impl SessionOrigin {
/// Classify a tmux session by its name.
///
/// Why: the daemon's session driver only has the tmux name to work from;
/// a single classifier keeps the prefix convention in one place — it
/// delegates to [`crate::core::names::is_managed_session_name`] rather
/// than duplicating the prefix list, so a future naming-scheme change
/// (like #1955) only needs to update one function.
/// What: returns [`TrustyMpm`](SessionOrigin::TrustyMpm) when `name` is
/// managed per [`crate::core::names::is_managed_session_name`], else
/// [`External`](SessionOrigin::External).
/// Test: `classifies_trusty_mpm_prefixes`, `classifies_external`.
pub fn classify(name: &str) -> Self {
if crate::core::names::is_managed_session_name(name) {
Self::TrustyMpm
} else {
Self::External
}
}
/// Lowercase wire label for this origin (`"trusty_mpm"` / `"external"`).
pub fn label(&self) -> &'static str {
match self {
Self::TrustyMpm => "trusty_mpm",
Self::External => "external",
}
}
}
/// An origin-tagged view of one tmux session.
///
/// Why: the universal-session dashboard lists every tmux session with enough
/// metadata to decide whether trusty-mpm already manages it.
/// What: the session name, its [`SessionOrigin`], whether a client is
/// attached, and the creation epoch.
/// Test: `external_session_json_roundtrip`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)]
pub struct ExternalSession {
/// tmux session name.
pub name: String,
/// Whether trusty-mpm created the session or it is external.
pub origin: SessionOrigin,
/// Whether a tmux client is currently attached.
pub attached: bool,
/// Unix epoch seconds the session was created.
pub created: i64,
}
impl ExternalSession {
/// Build an origin-tagged session view, classifying `name` automatically.
///
/// Why: callers parsing `tmux list-sessions` rows have the raw fields; this
/// constructor classifies the origin so they need not.
/// What: stores the fields and sets `origin` via [`SessionOrigin::classify`].
/// Test: `external_session_classifies_on_construction`.
pub fn new(name: impl Into<String>, attached: bool, created: i64) -> Self {
let name = name.into();
let origin = SessionOrigin::classify(&name);
Self {
name,
origin,
attached,
created,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classifies_trusty_mpm_prefixes() {
assert_eq!(
SessionOrigin::classify("tm-trusty-tools-01"),
SessionOrigin::TrustyMpm
);
// Legacy prefixes (issue #1955): a live session created before the
// daemon upgraded to `tm-` must still classify as ours.
assert_eq!(
SessionOrigin::classify("tmpm-brave-otter"),
SessionOrigin::TrustyMpm
);
assert_eq!(
SessionOrigin::classify("trusty-mpm-abc123"),
SessionOrigin::TrustyMpm
);
}
#[test]
fn classifies_external() {
assert_eq!(SessionOrigin::classify("work"), SessionOrigin::External);
assert_eq!(SessionOrigin::classify("0"), SessionOrigin::External);
// A name that merely contains the substring is still external.
assert_eq!(
SessionOrigin::classify("my-tmpm-thing"),
SessionOrigin::External
);
}
#[test]
fn origin_labels_are_stable() {
assert_eq!(SessionOrigin::TrustyMpm.label(), "trusty_mpm");
assert_eq!(SessionOrigin::External.label(), "external");
}
#[test]
fn external_session_classifies_on_construction() {
let internal = ExternalSession::new("tmpm-x", true, 100);
assert_eq!(internal.origin, SessionOrigin::TrustyMpm);
let external = ExternalSession::new("vim", false, 200);
assert_eq!(external.origin, SessionOrigin::External);
}
#[test]
fn external_session_json_roundtrip() {
let session = ExternalSession::new("trusty-mpm-1", true, 1_700_000_000);
let json = serde_json::to_string(&session).unwrap();
let back: ExternalSession = serde_json::from_str(&json).unwrap();
assert_eq!(back, session);
}
}