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
//! Unit tests for the `session` command helpers.
//!
//! Why: extracted from `session.rs` to keep that production file under the
//! 500-SLOC cap while retaining full test coverage for the private helpers.
//! What: tests for `derive_source_id_from_path` and `matches_session`.
//! Test: this file (budget: 1500 SLOC as a `_tests.rs` file).
use super::{derive_source_id_from_path, matches_session};
#[test]
fn derive_source_id_from_cwd_returns_none_without_git() {
// A fresh TempDir has no git repo, so get_origin_url will return None
// and derive_source_id_from_path must return None (not panic).
let dir = tempfile::TempDir::new().expect("tempdir");
assert!(
derive_source_id_from_path(dir.path()).is_none(),
"non-git directory must yield None"
);
}
#[test]
fn ls_source_id_filter_selects_correct_slug() {
// When source_id = Some("owner/repo") and current = false, the slug
// comes straight from the --source-id flag (no git lookup needed).
// We verify the slug is correctly passed by asserting it would be used
// as-is in the query param (no transformation needed for a valid slug).
let slug = "myorg/myrepo";
// A valid slug contains exactly one '/' and no whitespace.
assert!(slug.contains('/'), "slug must be owner/repo form");
assert_eq!(
slug.split('/').count(),
2,
"slug must have exactly two parts"
);
// derive_source_id_from_path returns the same owner/repo format.
// This test guards the format invariant that the daemon expects.
let (owner, repo) = slug.split_once('/').unwrap();
assert!(!owner.is_empty());
assert!(!repo.is_empty());
}
#[test]
fn info_managed_fallback_matches_by_id_and_name() {
let session = serde_json::json!({
"id": "abc-1234-uuid",
"name": "tmpm-red-owl",
"state": "active"
});
assert!(
matches_session(&session, "abc-1234-uuid"),
"must match by exact id"
);
assert!(
matches_session(&session, "tmpm-red-owl"),
"must match by exact name"
);
assert!(
!matches_session(&session, "tmpm-blue-fox"),
"must not match a different name"
);
assert!(
!matches_session(&session, "xyz-9999"),
"must not match a different id"
);
// Partial matches are not accepted — the lookup is exact.
assert!(
!matches_session(&session, "abc-1234"),
"partial id must not match"
);
}
#[test]
fn scope_for_display_all_keeps_tombstone_in_slot_order() {
// Why (#3034 fix-round LOW): the `--all` sort in
// `session_picker::scope_for_display` sinks ONLY "decommissioned"
// records to the bottom; a "deleted" slot tombstone must stay exactly
// where the daemon placed it (ascending slot order) rather than being
// grouped with decommissioned rows — Bob's directive requires a
// tombstone to render at its ORIGINAL numbered position, not wherever a
// liveness sort would relocate it. Lives here (rather than
// `tests_behavior_c_tests.rs`, which sits at the 1500-SLOC test cap)
// since it needs no fixture that file already provides.
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, "deleted": false },
{ "id": "", "name": "", "state": "deleted", "slot": 2, "deleted": true },
{ "id": "c3", "name": "sess-old", "state": "decommissioned", "slot": 3, "deleted": false },
{ "id": "d4", "name": "sess-stopped", "state": "stopped", "slot": 4, "deleted": false },
]
})
.to_string();
let sessions = scope_for_display(
parse_managed_sessions(&raw).expect("parse must succeed"),
true,
&std::collections::HashSet::new(),
);
// All 4 rows survive `--all` (nothing dropped, unlike the default view).
assert_eq!(
sessions.len(),
4,
"--all must keep every row, tombstones included"
);
// The tombstone (slot 2) must appear BEFORE the decommissioned row (slot 3)
// — i.e. it was NOT sunk to the bottom alongside "decommissioned".
let tombstone_pos = sessions
.iter()
.position(|s| s.deleted)
.expect("tombstone must be present");
let decommissioned_pos = sessions
.iter()
.position(|s| s.state == "decommissioned")
.expect("decommissioned row must be present");
assert!(
tombstone_pos < decommissioned_pos,
"tombstone (slot 2) must stay ahead of the sunk decommissioned row (slot 3), \
got tombstone at {tombstone_pos}, decommissioned at {decommissioned_pos}"
);
// The decommissioned row must be sunk to the very end (pre-existing #1809
// behavior, unchanged): it is the last row despite starting at slot 3.
assert_eq!(
sessions.last().map(|s| s.state.as_str()),
Some("decommissioned"),
"decommissioned row must still sink to the bottom of the --all view"
);
}