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
//! Tests for `tm repair session-store` (#5007).
//!
//! Why: this command deletes bytes from the operator's only copy of the fleet's
//! state, so the two properties worth pinning are that `--dry-run` writes
//! nothing at all and that a real run leaves a loadable store plus a backup.
//! What: the healthy no-op, the dry run, and the real repair.
//! Test: this file IS the test module.
use super::*;
use tempfile::TempDir;
/// One session record carrying every field `SessionRecord` requires.
///
/// Why (#5027 review): `{"sessions":{"<id>":{"task":"t"}}}` is valid JSON that
/// `SessionStore` cannot load, so a repair driven by it never proved the
/// command leaves a WORKING store — only a parseable one. The repair now
/// refuses to cut down to a head the daemon could not load, which is exactly
/// the case this fixture used to be.
/// What: the six no-default fields, serialized the way the store writes them.
const RECORD: &str = concat!(
r#"{"id":"3d1b0c8e-1111-2222-3333-444444444444","tmux_name":"tm-5007","#,
r#""cwd":"/tmp","task":"t","state":"active","#,
r#""created_at":"2026-08-06T09:01:17Z"}"#
);
/// Build a store file that is a complete, LOADABLE document followed by a stale
/// tail — the 2026-08-06 shape.
fn corrupt_store(dir: &TempDir) -> (std::path::PathBuf, String) {
let path = dir.path().join("sessions.json");
let doc = format!(r#"{{"sessions":{{"3d1b0c8e-1111-2222-3333-444444444444":{RECORD}}}}}"#);
let bytes = format!("{doc}{doc}");
std::fs::write(&path, &bytes).expect("write");
(path, bytes)
}
/// Why: the default has to be the file the daemon writes, or the command
/// repairs nothing anyone uses.
/// What: asserts the default lands under `session-manager/` and that `--path`
/// overrides it verbatim.
/// Test: this test.
#[test]
fn resolve_store_path_defaults_under_the_framework_root() {
let default = resolve_store_path(None);
assert!(
default.ends_with("session-manager/sessions.json"),
"{}",
default.display()
);
assert_eq!(
resolve_store_path(Some("/tmp/x.json".into())),
std::path::PathBuf::from("/tmp/x.json")
);
}
/// Why: re-running the command after a successful repair must be a safe no-op,
/// not an error an operator has to interpret.
/// What: asserts a parseable store succeeds and is left byte-identical.
/// Test: this test.
#[test]
fn repair_session_store_reports_a_healthy_store() {
let dir = TempDir::new().expect("tempdir");
let path = dir.path().join("sessions.json");
let good = r#"{"sessions":{}}"#;
std::fs::write(&path, good).expect("write");
repair_session_store(Some(path.display().to_string()), false, false).expect("healthy is Ok");
assert_eq!(std::fs::read_to_string(&path).expect("re-read"), good);
}
/// Why: `--dry-run` exists so an operator can see the cut before authorising
/// it. A dry run that wrote anything — including a backup — would defeat that.
/// What: asserts the store is unchanged and no backup was created.
/// Test: this test.
#[test]
fn repair_session_store_dry_run_writes_nothing() {
let dir = TempDir::new().expect("tempdir");
let (path, before) = corrupt_store(&dir);
repair_session_store(Some(path.display().to_string()), true, false).expect("dry run");
assert_eq!(
std::fs::read_to_string(&path).expect("re-read"),
before,
"--dry-run must not modify the store"
);
let siblings: Vec<_> = std::fs::read_dir(dir.path())
.expect("readdir")
.filter_map(|e| e.ok().map(|e| e.file_name().to_string_lossy().into_owned()))
.collect();
assert_eq!(
siblings,
vec!["sessions.json".to_string()],
"--dry-run must not write a backup either"
);
}
/// Why: the point of the command is that a wedged store becomes a working one
/// without a human editing JSON.
/// What: asserts the repaired file LOADS (#5027 review — "parses" was the
/// weaker claim that let a fixture the daemon rejects look like a success) and
/// that the original bytes survive in a timestamped backup.
/// Test: this test.
#[tokio::test]
async fn repair_session_store_truncates_and_backs_up() {
let dir = TempDir::new().expect("tempdir");
let (path, before) = corrupt_store(&dir);
repair_session_store(Some(path.display().to_string()), false, false).expect("repair");
let after = std::fs::read_to_string(&path).expect("re-read");
assert!(
trusty_mpm::session_manager::SessionStore::load(dir.path())
.await
.is_ok(),
"the repaired store must be one the daemon can load, not merely parse"
);
assert!(after.len() < before.len(), "the stale tail was discarded");
let backup = std::fs::read_dir(dir.path())
.expect("readdir")
.filter_map(|e| e.ok())
.find(|e| e.file_name().to_string_lossy().contains(".corrupt-backup-"))
.expect("a timestamped backup must exist");
assert_eq!(
std::fs::read_to_string(backup.path()).expect("read backup"),
before,
"the backup holds the original bytes verbatim"
);
}